From aa6031c75885c0d396d4ac70d65af11d425f026c Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 16:28:51 +0000 Subject: [PATCH 1/6] Add first-party PostgreSQL query analyzer Extend scim-sql with ai.singlr.postgresql, a generic PostgreSQL query-analysis API that parses complete SQL and reports structural facts: statement kind and count, relations (physical/CTE/function, with aliases), columns, functions, named parameters, policy-relevant features, and a deterministic normalized form for hashing and audit. It parses and describes SQL only - no execution, catalog resolution, authorization, or policy. The grammar is the ANTLR grammars-v4 PostgreSQL grammar vendored at commit 76093c04af6a51f38a67d14f7e71ff0a9b4400da with one deliberate extension: named parameters (:name) are first-class expression values and can never be identifiers, aliases, or relations. Provenance, verbatim upstream licenses, local modifications, and the upgrade path are recorded in NOTICE.md and shipped in the jars under META-INF. Analysis walks all nested scopes with progressive CTE scoping (non-recursive CTE bodies do not see their own name; siblings see earlier names; inner scopes shadow outer). Input is bounded by length, token count, and nesting depth; exceptions carry only a stable reason plus line/column and never echo SQL content. Grammar warnings are pinned (two benign upstream warning-146 lexer rules) and any new warning or error fails the build. An upstream regression corpus, adversarial/hostile input tests, and module-export tests protect behavior. Existing SCIM behavior and API are unchanged; the artifact gains no runtime dependency beyond the existing ANTLR runtime. Version bumped to 1.1.0-SNAPSHOT. --- NOTICE.md | 96 + README.md | 33 +- pom.xml | 32 +- .../ai/singlr/postgresql/parser/COPYRIGHT | 23 + .../postgresql/parser/PostgreSQLLexer.g4 | 1476 +++++ .../postgresql/parser/PostgreSQLParser.g4 | 5472 +++++++++++++++++ .../singlr/postgresql/AnalysisCollector.java | 463 ++ .../ai/singlr/postgresql/ColumnReference.java | 31 + .../singlr/postgresql/FunctionReference.java | 28 + .../postgresql/PostgresQueryAnalyzer.java | 192 + .../ai/singlr/postgresql/QueryAnalysis.java | 57 + .../postgresql/QueryAnalysisException.java | 42 + .../ai/singlr/postgresql/QueryFeature.java | 36 + .../singlr/postgresql/RelationReference.java | 39 + .../ai/singlr/postgresql/StatementKind.java | 26 + .../ai/singlr/postgresql/package-info.java | 24 + .../parser/LexerDispatchingErrorListener.java | 79 + .../ParserDispatchingErrorListener.java | 79 + .../parser/PostgreSQLLexerBase.java | 92 + .../parser/PostgreSQLParserBase.java | 138 + src/main/java/module-info.java | 17 +- .../postgresql/FeatureDetectionTest.java | 186 + .../postgresql/GrammarWarningsTest.java | 90 + .../singlr/postgresql/HostileInputTest.java | 177 + .../postgresql/ModuleDescriptorTest.java | 35 + .../singlr/postgresql/NamedParameterTest.java | 133 + .../singlr/postgresql/NormalizationTest.java | 79 + .../postgresql/PostgresQueryAnalyzerTest.java | 356 ++ .../StatementClassificationTest.java | 86 + .../singlr/postgresql/UpstreamCorpusTest.java | 61 + .../postgresql-corpus/aggregates.sql | 1218 ++++ src/test/resources/postgresql-corpus/case.sql | 254 + .../resources/postgresql-corpus/delete.sql | 25 + .../resources/postgresql-corpus/insert.sql | 608 ++ src/test/resources/postgresql-corpus/join.sql | 2173 +++++++ .../resources/postgresql-corpus/limit.sql | 198 + .../resources/postgresql-corpus/select.sql | 264 + .../postgresql-corpus/select_distinct.sql | 137 + .../postgresql-corpus/select_having.sql | 50 + .../resources/postgresql-corpus/subselect.sql | 859 +++ .../postgresql-corpus/transactions.sql | 585 ++ .../resources/postgresql-corpus/union.sql | 442 ++ .../resources/postgresql-corpus/update.sql | 614 ++ .../resources/postgresql-corpus/window.sql | 1306 ++++ src/test/resources/postgresql-corpus/with.sql | 1041 ++++ 45 files changed, 19444 insertions(+), 8 deletions(-) create mode 100644 NOTICE.md create mode 100644 src/main/antlr4/ai/singlr/postgresql/parser/COPYRIGHT create mode 100644 src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 create mode 100644 src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 create mode 100644 src/main/java/ai/singlr/postgresql/AnalysisCollector.java create mode 100644 src/main/java/ai/singlr/postgresql/ColumnReference.java create mode 100644 src/main/java/ai/singlr/postgresql/FunctionReference.java create mode 100644 src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java create mode 100644 src/main/java/ai/singlr/postgresql/QueryAnalysis.java create mode 100644 src/main/java/ai/singlr/postgresql/QueryAnalysisException.java create mode 100644 src/main/java/ai/singlr/postgresql/QueryFeature.java create mode 100644 src/main/java/ai/singlr/postgresql/RelationReference.java create mode 100644 src/main/java/ai/singlr/postgresql/StatementKind.java create mode 100644 src/main/java/ai/singlr/postgresql/package-info.java create mode 100644 src/main/java/ai/singlr/postgresql/parser/LexerDispatchingErrorListener.java create mode 100644 src/main/java/ai/singlr/postgresql/parser/ParserDispatchingErrorListener.java create mode 100644 src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java create mode 100644 src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java create mode 100644 src/test/java/ai/singlr/postgresql/FeatureDetectionTest.java create mode 100644 src/test/java/ai/singlr/postgresql/GrammarWarningsTest.java create mode 100644 src/test/java/ai/singlr/postgresql/HostileInputTest.java create mode 100644 src/test/java/ai/singlr/postgresql/ModuleDescriptorTest.java create mode 100644 src/test/java/ai/singlr/postgresql/NamedParameterTest.java create mode 100644 src/test/java/ai/singlr/postgresql/NormalizationTest.java create mode 100644 src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java create mode 100644 src/test/java/ai/singlr/postgresql/StatementClassificationTest.java create mode 100644 src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java create mode 100644 src/test/resources/postgresql-corpus/aggregates.sql create mode 100644 src/test/resources/postgresql-corpus/case.sql create mode 100644 src/test/resources/postgresql-corpus/delete.sql create mode 100644 src/test/resources/postgresql-corpus/insert.sql create mode 100644 src/test/resources/postgresql-corpus/join.sql create mode 100644 src/test/resources/postgresql-corpus/limit.sql create mode 100644 src/test/resources/postgresql-corpus/select.sql create mode 100644 src/test/resources/postgresql-corpus/select_distinct.sql create mode 100644 src/test/resources/postgresql-corpus/select_having.sql create mode 100644 src/test/resources/postgresql-corpus/subselect.sql create mode 100644 src/test/resources/postgresql-corpus/transactions.sql create mode 100644 src/test/resources/postgresql-corpus/union.sql create mode 100644 src/test/resources/postgresql-corpus/update.sql create mode 100644 src/test/resources/postgresql-corpus/window.sql create mode 100644 src/test/resources/postgresql-corpus/with.sql diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..e0a391f --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,96 @@ +# Third-Party Notices + +## ANTLR grammars-v4 PostgreSQL grammar + +The PostgreSQL grammar and its Java support sources under +`src/main/antlr4/ai/singlr/postgresql/parser/` and +`src/main/java/ai/singlr/postgresql/parser/` are vendored from the +[ANTLR grammars-v4](https://github.com/antlr/grammars-v4) project. + +| | | +|---|---| +| Upstream repository | `https://github.com/antlr/grammars-v4` | +| Upstream path | `sql/postgresql` | +| Pinned commit | `76093c04af6a51f38a67d14f7e71ff0a9b4400da` (2026-06-20) | + +Vendored files: + +- `PostgreSQLLexer.g4` (from `sql/postgresql/PostgreSQLLexer.g4`) +- `PostgreSQLParser.g4` (from `sql/postgresql/PostgreSQLParser.g4`) +- `COPYRIGHT` (from `sql/postgresql/COPYRIGHT`, verbatim) +- `PostgreSQLLexerBase.java`, `PostgreSQLParserBase.java`, + `LexerDispatchingErrorListener.java`, `ParserDispatchingErrorListener.java` + (from `sql/postgresql/Java/`) + +The test corpus under `src/test/resources/postgresql-corpus/` is vendored from +`sql/postgresql/examples/` at the same commit. Generated lexer/parser sources are +build output under `target/generated-sources/` and are never hand-edited. + +### Local modifications + +Every deviation from upstream is listed here and marked with a +`scim-sql local modification` comment at the change site: + +1. `PostgreSQLParser.g4` — added `plsqlvariablename` as a labeled `c_expr` + alternative (`# c_expr_namedparam`) so named parameters (`:start_at`, + `:user_id`) are first-class expression values. +2. `PostgreSQLParser.g4` — removed `PLSQLVARIABLENAME` from the `identifier` + rule so `:name` can never be an identifier, alias, or relation name. +3. `*.java` support files — added a + `package ai.singlr.postgresql.parser;` declaration (upstream files have no + package). No other changes; the files are excluded from code formatting to + keep them diffable against upstream. + +Known consequences, inherited from upstream lexing of `:name`: + +- Array slices with identifier bounds (`arr[lo:hi]`) do not parse because + `:hi` lexes as a named parameter. Numeric bounds (`arr[1:2]`) are unaffected. +- The `:"identifier"` PL/SQL form is rejected. + +### Expected grammar warnings + +Upstream ships two benign ANTLR warning-146 lexer rules +(`AfterEscapeStringConstantMode_NotContinued` and +`AfterEscapeStringConstantWithNewlineMode_NotContinued`). These are pinned by +`GrammarWarningsTest`; the build fails on any new grammar warning or error. + +### Upgrading the grammar + +1. Pick a new upstream commit and re-copy the files listed above. +2. Re-apply the local modifications (search for `scim-sql local modification`). +3. Update the pinned commit here and in `UpstreamCorpusTest`, refresh the + corpus files, and run `mvn clean verify`. Review any change in + `GrammarWarningsTest` expectations deliberately. + +### Licenses + +The grammar files carry the MIT license of their authors (Tunnel Vision +Laboratories, LLC and Oleksii Kovalov) in their headers, preserved verbatim. +The upstream `COPYRIGHT` file is preserved verbatim next to the grammars and +reproduced here as required: + +``` +PostgreSQL Database Management System +(formerly known as Postgres, then as Postgres95) + +Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + +Portions Copyright (c) 1994, The Regents of the University of California + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose, without fee, and without a written agreement +is hereby granted, provided that the above copyright notice and this +paragraph and the following two paragraphs appear in all copies. + +IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING +LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +``` diff --git a/README.md b/README.md index 5df7b5c..c8d6685 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # scim-sql -SCIM filter expression to parameterized SQL converter for **PostgreSQL**. Parses [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.2) filter strings and produces SQL WHERE clauses with named parameters — safe from injection by design. +Two independent capabilities for **PostgreSQL** in one small library: + +1. **SCIM filter → SQL** (`ai.singlr.scimsql`): parses [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.2) filter strings and produces SQL WHERE clauses with named parameters — safe from injection by design. It parses SCIM filter expressions only; it does not validate full SQL. +2. **PostgreSQL query analysis** (`ai.singlr.postgresql`): parses complete SQL statements and reports structural facts — statement kind and count, relations, columns, functions, named parameters, and policy-relevant features — without executing SQL or resolving catalog objects. See [PostgreSQL Query Analysis](#postgresql-query-analysis). The generated SQL uses PostgreSQL-specific syntax for typed values (`CAST(… AS UUID)`, `CAST(… AS timestamptz)`, `CAST(… AS jsonb)`, `@>` for JSON containment). Standard comparisons (`=`, `!=`, `LIKE`, `IN`, `IS NOT NULL`) are portable across databases. The `compareFilterBuilder` extension point allows overriding SQL generation for other databases. @@ -121,6 +124,34 @@ var filter = engine.parseFilter( This lets you intercept `ComparisonFilter` instances and return a subclass with custom `toClause()` or `paramKey()` behavior. +## PostgreSQL Query Analysis + +`PostgresQueryAnalyzer.analyze(String)` parses a complete PostgreSQL statement (or script) through EOF and returns an immutable `QueryAnalysis`: + +```java +var analysis = PostgresQueryAnalyzer.analyze( + "SELECT u.id, count(*) FROM users u WHERE u.created_at >= :start_at GROUP BY u.id"); + +analysis.statementKind(); // SELECT +analysis.statementCount(); // 1 +analysis.relations(); // [RelationReference[schema=null, name=users, alias=u, kind=PHYSICAL]] +analysis.columns(); // [u.id, u.created_at, u.id] +analysis.functions(); // [count at 1:17] +analysis.parameters(); // [start_at] +analysis.features(); // [] +analysis.normalizedSql(); // deterministic form for hashing/audit +``` + +Key properties: + +- **Named parameters** (`:start_at`, `:user_id`) are first-class expression values via a deliberate, documented grammar extension. Names are reported exactly; values are never bound or substituted. +- **Statement kinds**: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DDL`, `UTILITY`, `UNKNOWN`. Prohibited-but-valid statements analyze successfully so callers can raise precise policy errors. +- **Features** flag CTEs (plain/recursive/writable), subqueries, set operations, window functions, `SELECT INTO`, row locks, `LATERAL`, function/VALUES relations, star projections, and multiple statements — at any nesting depth. +- **Relations** distinguish physical tables, CTE references, and function relations, preserving aliases and schema qualification. Classification is syntactic; the analyzer does not resolve catalog objects, authorize access, execute SQL, or decide policy. +- **Safety**: input is bounded (length, tokens, nesting depth), errors carry only a stable reason plus line/column, and SQL text is never logged or echoed. + +The grammar is the [ANTLR grammars-v4](https://github.com/antlr/grammars-v4) PostgreSQL grammar, vendored at a pinned commit — see [NOTICE.md](NOTICE.md) for provenance, licenses, and the exact local modifications. + ## Building Requires JDK 25+ and Maven. diff --git a/pom.xml b/pom.xml index 1e46fd8..0a3b5cb 100644 --- a/pom.xml +++ b/pom.xml @@ -6,11 +6,11 @@ ai.singlr scim-sql - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT jar scim-sql - SCIM filter expression to parameterized SQL converter + SCIM filter expression to parameterized SQL converter and PostgreSQL query analyzer https://github.com/singlr-ai/scim-sql @@ -69,9 +69,32 @@ junit-jupiter test + + + org.antlr + antlr4 + ${antlr4-version} + test + + + + ${project.basedir} + + NOTICE.md + + META-INF + + + src/main/antlr4/ai/singlr/postgresql/parser + + COPYRIGHT + + META-INF + + org.antlr @@ -80,6 +103,7 @@ true false + false @@ -109,6 +133,9 @@ 3.2.1 + + src/main/java/ai/singlr/postgresql/parser/** + 1.27.0 @@ -143,6 +170,7 @@ ai/singlr/scimsql/ScimVisitor* ai/singlr/scimsql/ScimListener* ai/singlr/scimsql/ScimBaseListener* + ai/singlr/postgresql/parser/** diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/COPYRIGHT b/src/main/antlr4/ai/singlr/postgresql/parser/COPYRIGHT new file mode 100644 index 0000000..0fc523a --- /dev/null +++ b/src/main/antlr4/ai/singlr/postgresql/parser/COPYRIGHT @@ -0,0 +1,23 @@ +PostgreSQL Database Management System +(formerly known as Postgres, then as Postgres95) + +Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + +Portions Copyright (c) 1994, The Regents of the University of California + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose, without fee, and without a written agreement +is hereby granted, provided that the above copyright notice and this +paragraph and the following two paragraphs appear in all copies. + +IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING +LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 new file mode 100644 index 0000000..f23b70f --- /dev/null +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 @@ -0,0 +1,1476 @@ +/* +based on +https://github.com/tunnelvisionlabs/antlr4-grammar-postgresql/blob/master/src/com/tunnelvisionlabs/postgresql/PostgreSqlLexer.g4 +*/ + +/* + * [The "MIT license"] + * Copyright (C) 2014 Sam Harwell, Tunnel Vision Laboratories, LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * 2. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * 3. Except as contained in this notice, the name of Tunnel Vision + * Laboratories, LLC. shall not be used in advertising or otherwise to + * promote the sale, use or other dealings in this Software without prior + * written authorization from Tunnel Vision Laboratories, LLC. + */ + +// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine +// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true + +lexer grammar PostgreSQLLexer; +/* Reference: + * http://www.postgresql.org/docs/9.3/static/sql-syntax-lexical.html + */ + +options { + superClass = PostgreSQLLexerBase; + caseInsensitive = true; +} + + +// Insert here @header for C++ lexer. + + +Dollar: '$'; + +OPEN_PAREN: '('; + +CLOSE_PAREN: ')'; + +OPEN_BRACKET: '['; + +CLOSE_BRACKET: ']'; + +COMMA: ','; + +SEMI: ';'; + +COLON: ':'; + +STAR: '*'; + +EQUAL: '='; + +DOT: '.'; +//NamedArgument : ':='; + +PLUS: '+'; + +MINUS: '-'; + +SLASH: '/'; + +CARET: '^'; + +LT: '<'; + +GT: '>'; + +LESS_LESS: '<<'; + +GREATER_GREATER: '>>'; + +COLON_EQUALS: ':='; + +LESS_EQUALS: '<='; + +EQUALS_GREATER: '=>'; + +GREATER_EQUALS: '>='; + +DOT_DOT: '..'; + +NOT_EQUALS: '<>'; + +TYPECAST: '::'; + +PERCENT: '%'; + +PARAM: '$' ([0-9])+; +// + +// OPERATORS (4.1.3) + +// + +// this rule does not allow + or - at the end of a multi-character operator + +Operator: + ( + ( + OperatorCharacter + | ('+' | '-' {this.CheckLaMinus()}? )+ (OperatorCharacter | '/' {this.CheckLaStar()}? ) + | '/' {this.CheckLaStar()}? + )+ + | // special handling for the single-character operators + and - + [+-] + ) + //TODO somehow rewrite this part without using Actions + {this.HandleLessLessGreaterGreater();} +; +/* This rule handles operators which end with + or -, and sets the token type to Operator. It is comprised of four + * parts, in order: + * + * 1. A prefix, which does not contain a character from the required set which allows + or - to appear at the end of + * the operator. + * 2. A character from the required set which allows + or - to appear at the end of the operator. + * 3. An optional sub-token which takes the form of an operator which does not include a + or - at the end of the + * sub-token. + * 4. A suffix sequence of + and - characters. + */ + +OperatorEndingWithPlusMinus: + (OperatorCharacterNotAllowPlusMinusAtEnd | '-' {this.CheckLaMinus()}? | '/' {this.CheckLaStar()}? )* OperatorCharacterAllowPlusMinusAtEnd Operator? ( + '+' + | '-' {this.CheckLaMinus()}? + )+ -> type (Operator) +; +// Each of the following fragment rules omits the +, -, and / characters, which must always be handled in a special way + +// by the operator rules above. + +fragment OperatorCharacter: [*<>=~!@%^&|`?#]; +// these are the operator characters that don't count towards one ending with + or - + +fragment OperatorCharacterNotAllowPlusMinusAtEnd: [*<>=+]; +// an operator may end with + or - if it contains one of these characters + +fragment OperatorCharacterAllowPlusMinusAtEnd: [~!@%^&|`?#]; +// + +// KEYWORDS (Appendix C) + + + +JSON: 'JSON'; +JSON_ARRAY: 'JSON_ARRAY'; +JSON_ARRAYAGG: 'JSON_ARRAYAGG'; +JSON_EXISTS: 'JSON_EXISTS'; +JSON_OBJECT: 'JSON_OBJECT'; +JSON_OBJECTAGG: 'JSON_OBJECTAGG'; +JSON_QUERY: 'JSON_QUERY'; +JSON_SCALAR: 'JSON_SCALAR'; +JSON_SERIALIZE: 'JSON_SERIALIZE'; +JSON_TABLE: 'JSON_TABLE'; +JSON_VALUE: 'JSON_VALUE'; +MERGE_ACTION: 'MERGE_ACTION'; + +SYSTEM_USER: 'SYSTEM_USER'; + +ABSENT: 'ABSENT'; +ASENSITIVE: 'ASENSITIVE'; +ATOMIC: 'ATOMIC'; +BREADTH: 'BREATH'; +COMPRESSION: 'COMPRESSION'; +CONDITIONAL: 'CONDITIONAL'; +DEPTH: 'DEPTH'; +EMPTY_P: 'EMPTY'; +FINALIZE: 'FINALIZE'; +INDENT: 'INDENT'; +KEEP: 'KEEP'; +KEYS: 'KEYS'; +NESTED: 'NESTED'; +OMIT: 'OMIT'; +PARAMETER: 'PARAMETER'; +PATH: 'PATH'; +PLAN: 'PLAN'; +QUOTES: 'QUOTES'; +SCALAR: 'SCALAR'; +SOURCE: 'SOURCE'; +STRING_P: 'STRING'; +TARGET: 'TARGET'; +UNCONDITIONAL: 'UNCONDITIONAL'; + +PERIOD: 'PERIOD'; + +FORMAT_LA: 'FORMAT_LA'; + +// + +// + +// reserved keywords + +// + +ALL: 'ALL'; + +ANALYSE: 'ANALYSE'; + +ANALYZE: 'ANALYZE'; + +AND: 'AND'; + +ANY: 'ANY'; + +ARRAY: 'ARRAY'; + +AS: 'AS'; + +ASC: 'ASC'; + +ASYMMETRIC: 'ASYMMETRIC'; + +BOTH: 'BOTH'; + +CASE: 'CASE'; + +CAST: 'CAST'; + +CHECK: 'CHECK'; + +COLLATE: 'COLLATE'; + +COLUMN: 'COLUMN'; + +CONSTRAINT: 'CONSTRAINT'; + +CREATE: 'CREATE'; + +CURRENT_CATALOG: 'CURRENT_CATALOG'; + +CURRENT_DATE: 'CURRENT_DATE'; + +CURRENT_ROLE: 'CURRENT_ROLE'; + +CURRENT_TIME: 'CURRENT_TIME'; + +CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP'; + +CURRENT_USER: 'CURRENT_USER'; + +DEFAULT: 'DEFAULT'; + +DEFERRABLE: 'DEFERRABLE'; + +DESC: 'DESC'; + +DISTINCT: 'DISTINCT'; + +DO: 'DO'; + +ELSE: 'ELSE'; + +EXCEPT: 'EXCEPT'; + +FALSE_P: 'FALSE'; + +FETCH: 'FETCH'; + +FOR: 'FOR'; + +FOREIGN: 'FOREIGN'; + +FROM: 'FROM'; + +GRANT: 'GRANT'; + +GROUP_P: 'GROUP'; + +HAVING: 'HAVING'; + +IN_P: 'IN'; + +INITIALLY: 'INITIALLY'; + +INTERSECT: 'INTERSECT'; + +INTO: 'INTO'; + +LATERAL_P: 'LATERAL'; + +LEADING: 'LEADING'; + +LIMIT: 'LIMIT'; + +LOCALTIME: 'LOCALTIME'; + +LOCALTIMESTAMP: 'LOCALTIMESTAMP'; + +NOT: 'NOT'; + +NULL_P: 'NULL'; + +OFFSET: 'OFFSET'; + +ON: 'ON'; + +ONLY: 'ONLY'; + +OR: 'OR'; + +ORDER: 'ORDER'; + +PLACING: 'PLACING'; + +PRIMARY: 'PRIMARY'; + +REFERENCES: 'REFERENCES'; + +RETURNING: 'RETURNING'; + +SELECT: 'SELECT'; + +SESSION_USER: 'SESSION_USER'; + +SOME: 'SOME'; + +SYMMETRIC: 'SYMMETRIC'; + +TABLE: 'TABLE'; + +THEN: 'THEN'; + +TO: 'TO'; + +TRAILING: 'TRAILING'; + +TRUE_P: 'TRUE'; + +UNION: 'UNION'; + +UNIQUE: 'UNIQUE'; + +USER: 'USER'; + +USING: 'USING'; + +VARIADIC: 'VARIADIC'; + +WHEN: 'WHEN'; + +WHERE: 'WHERE'; + +WINDOW: 'WINDOW'; + +WITH: 'WITH'; + +// + +// reserved keywords (can be function or type) + +// + +AUTHORIZATION: 'AUTHORIZATION'; + +BINARY: 'BINARY'; + +COLLATION: 'COLLATION'; + +CONCURRENTLY: 'CONCURRENTLY'; + +CROSS: 'CROSS'; + +CURRENT_SCHEMA: 'CURRENT_SCHEMA'; + +FREEZE: 'FREEZE'; + +FULL: 'FULL'; + +ILIKE: 'ILIKE'; + +INNER_P: 'INNER'; + +IS: 'IS'; + +ISNULL: 'ISNULL'; + +JOIN: 'JOIN'; + +LEFT: 'LEFT'; + +LIKE: 'LIKE'; + +NATURAL: 'NATURAL'; + +NOTNULL: 'NOTNULL'; + +OUTER_P: 'OUTER'; + +OVER: 'OVER'; + +OVERLAPS: 'OVERLAPS'; + +RIGHT: 'RIGHT'; + +SIMILAR: 'SIMILAR'; + +VERBOSE: 'VERBOSE'; +// + +// non-reserved keywords + +// + +ABORT_P: 'ABORT'; + +ABSOLUTE_P: 'ABSOLUTE'; + +ACCESS: 'ACCESS'; + +ACTION: 'ACTION'; + +ADD_P: 'ADD'; + +ADMIN: 'ADMIN'; + +AFTER: 'AFTER'; + +AGGREGATE: 'AGGREGATE'; + +ALSO: 'ALSO'; + +ALTER: 'ALTER'; + +ALWAYS: 'ALWAYS'; + +ASSERTION: 'ASSERTION'; + +ASSIGNMENT: 'ASSIGNMENT'; + +AT: 'AT'; + +ATTRIBUTE: 'ATTRIBUTE'; + +BACKWARD: 'BACKWARD'; + +BEFORE: 'BEFORE'; + +BEGIN_P: 'BEGIN'; + +BY: 'BY'; + +CACHE: 'CACHE'; + +CALLED: 'CALLED'; + +CASCADE: 'CASCADE'; + +CASCADED: 'CASCADED'; + +CATALOG: 'CATALOG'; + +CHAIN: 'CHAIN'; + +CHARACTERISTICS: 'CHARACTERISTICS'; + +CHECKPOINT: 'CHECKPOINT'; + +CLASS: 'CLASS'; + +CLOSE: 'CLOSE'; + +CLUSTER: 'CLUSTER'; + +COMMENT: 'COMMENT'; + +COMMENTS: 'COMMENTS'; + +COMMIT: 'COMMIT'; + +COMMITTED: 'COMMITTED'; + +CONFIGURATION: 'CONFIGURATION'; + +CONNECTION: 'CONNECTION'; + +CONSTRAINTS: 'CONSTRAINTS'; + +CONTENT_P: 'CONTENT'; + +CONTINUE_P: 'CONTINUE'; + +CONVERSION_P: 'CONVERSION'; + +COPY: 'COPY'; + +COST: 'COST'; + +CSV: 'CSV'; + +CURSOR: 'CURSOR'; + +CYCLE: 'CYCLE'; + +DATA_P: 'DATA'; + +DATABASE: 'DATABASE'; + +DAY_P: 'DAY'; + +DEALLOCATE: 'DEALLOCATE'; + +DECLARE: 'DECLARE'; + +DEFAULTS: 'DEFAULTS'; + +DEFERRED: 'DEFERRED'; + +DEFINER: 'DEFINER'; + +DELETE_P: 'DELETE'; + +DELIMITER: 'DELIMITER'; + +DELIMITERS: 'DELIMITERS'; + +DICTIONARY: 'DICTIONARY'; + +DISABLE_P: 'DISABLE'; + +DISCARD: 'DISCARD'; + +DOCUMENT_P: 'DOCUMENT'; + +DOMAIN_P: 'DOMAIN'; + +DOUBLE_P: 'DOUBLE'; + +DROP: 'DROP'; + +EACH: 'EACH'; + +ENABLE_P: 'ENABLE'; + +ENCODING: 'ENCODING'; + +ENCRYPTED: 'ENCRYPTED'; + +ENUM_P: 'ENUM'; + +ESCAPE: 'ESCAPE'; + +EVENT: 'EVENT'; + +EXCLUDE: 'EXCLUDE'; + +EXCLUDING: 'EXCLUDING'; + +EXCLUSIVE: 'EXCLUSIVE'; + +EXECUTE: 'EXECUTE'; + +EXPLAIN: 'EXPLAIN'; + +EXTENSION: 'EXTENSION'; + +EXTERNAL: 'EXTERNAL'; + +FAMILY: 'FAMILY'; + +FIRST_P: 'FIRST'; + +FOLLOWING: 'FOLLOWING'; + +FORCE: 'FORCE'; + +FORWARD: 'FORWARD'; + +FUNCTION: 'FUNCTION'; + +FUNCTIONS: 'FUNCTIONS'; + +GLOBAL: 'GLOBAL'; + +GRANTED: 'GRANTED'; + +HANDLER: 'HANDLER'; + +HEADER_P: 'HEADER'; + +HOLD: 'HOLD'; + +HOUR_P: 'HOUR'; + +IDENTITY_P: 'IDENTITY'; + +IF_P: 'IF'; + +IMMEDIATE: 'IMMEDIATE'; + +IMMUTABLE: 'IMMUTABLE'; + +IMPLICIT_P: 'IMPLICIT'; + +INCLUDING: 'INCLUDING'; + +INCREMENT: 'INCREMENT'; + +INDEX: 'INDEX'; + +INDEXES: 'INDEXES'; + +INHERIT: 'INHERIT'; + +INHERITS: 'INHERITS'; + +INLINE_P: 'INLINE'; + +INSENSITIVE: 'INSENSITIVE'; + +INSERT: 'INSERT'; + +INSTEAD: 'INSTEAD'; + +INVOKER: 'INVOKER'; + +ISOLATION: 'ISOLATION'; + +KEY: 'KEY'; + +LABEL: 'LABEL'; + +LANGUAGE: 'LANGUAGE'; + +LARGE_P: 'LARGE'; + +LAST_P: 'LAST'; +//LC_COLLATE : 'LC'_'COLLATE; + +//LC_CTYPE : 'LC'_'CTYPE; + +LEAKPROOF: 'LEAKPROOF'; + +LEVEL: 'LEVEL'; + +LISTEN: 'LISTEN'; + +LOAD: 'LOAD'; + +LOCAL: 'LOCAL'; + +LOCATION: 'LOCATION'; + +LOCK_P: 'LOCK'; + +MAPPING: 'MAPPING'; + +MATCH: 'MATCH'; + +MATCHED: 'MATCHED'; + +MATERIALIZED: 'MATERIALIZED'; + +MAXVALUE: 'MAXVALUE'; + +MERGE: 'MERGE'; + +MINUTE_P: 'MINUTE'; + +MINVALUE: 'MINVALUE'; + +MODE: 'MODE'; + +MONTH_P: 'MONTH'; + +MOVE: 'MOVE'; + +NAME_P: 'NAME'; + +NAMES: 'NAMES'; + +NEXT: 'NEXT'; + +NO: 'NO'; + +NOTHING: 'NOTHING'; + +NOTIFY: 'NOTIFY'; + +NOWAIT: 'NOWAIT'; + +NULLS_P: 'NULLS'; + +OBJECT_P: 'OBJECT'; + +OF: 'OF'; + +OFF: 'OFF'; + +OIDS: 'OIDS'; + +OPERATOR: 'OPERATOR'; + +OPTION: 'OPTION'; + +OPTIONS: 'OPTIONS'; + +OWNED: 'OWNED'; + +OWNER: 'OWNER'; + +PARSER: 'PARSER'; + +PARTIAL: 'PARTIAL'; + +PARTITION: 'PARTITION'; + +PASSING: 'PASSING'; + +PASSWORD: 'PASSWORD'; + +PLANS: 'PLANS'; + +PRECEDING: 'PRECEDING'; + +PREPARE: 'PREPARE'; + +PREPARED: 'PREPARED'; + +PRESERVE: 'PRESERVE'; + +PRIOR: 'PRIOR'; + +PRIVILEGES: 'PRIVILEGES'; + +PROCEDURAL: 'PROCEDURAL'; + +PROCEDURE: 'PROCEDURE'; + +PROGRAM: 'PROGRAM'; + +QUOTE: 'QUOTE'; + +RANGE: 'RANGE'; + +READ: 'READ'; + +REASSIGN: 'REASSIGN'; + +RECHECK: 'RECHECK'; + +RECURSIVE: 'RECURSIVE'; + +REF: 'REF'; + +REFRESH: 'REFRESH'; + +REINDEX: 'REINDEX'; + +RELATIVE_P: 'RELATIVE'; + +RELEASE: 'RELEASE'; + +RENAME: 'RENAME'; + +REPEATABLE: 'REPEATABLE'; + +REPLACE: 'REPLACE'; + +REPLICA: 'REPLICA'; + +RESET: 'RESET'; + +RESTART: 'RESTART'; + +RESTRICT: 'RESTRICT'; + +RETURNS: 'RETURNS'; + +REVOKE: 'REVOKE'; + +ROLE: 'ROLE'; + +ROLLBACK: 'ROLLBACK'; + +ROWS: 'ROWS'; + +RULE: 'RULE'; + +SAVEPOINT: 'SAVEPOINT'; + +SCHEMA: 'SCHEMA'; + +SCROLL: 'SCROLL'; + +SEARCH: 'SEARCH'; + +SECOND_P: 'SECOND'; + +SECURITY: 'SECURITY'; + +SEQUENCE: 'SEQUENCE'; + +SEQUENCES: 'SEQUENCES'; + +SERIALIZABLE: 'SERIALIZABLE'; + +SERVER: 'SERVER'; + +SESSION: 'SESSION'; + +SET: 'SET'; + +SHARE: 'SHARE'; + +SHOW: 'SHOW'; + +SIMPLE: 'SIMPLE'; + +SNAPSHOT: 'SNAPSHOT'; + +STABLE: 'STABLE'; + +STANDALONE_P: 'STANDALONE'; + +START: 'START'; + +STATEMENT: 'STATEMENT'; + +STATISTICS: 'STATISTICS'; + +STDIN: 'STDIN'; + +STDOUT: 'STDOUT'; + +STORAGE: 'STORAGE'; + +STRICT_P: 'STRICT'; + +STRIP_P: 'STRIP'; + +SYSID: 'SYSID'; + +SYSTEM_P: 'SYSTEM'; + +TABLES: 'TABLES'; + +TABLESPACE: 'TABLESPACE'; + +TEMP: 'TEMP'; + +TEMPLATE: 'TEMPLATE'; + +TEMPORARY: 'TEMPORARY'; + +TEXT_P: 'TEXT'; + +TRANSACTION: 'TRANSACTION'; + +TRIGGER: 'TRIGGER'; + +TRUNCATE: 'TRUNCATE'; + +TRUSTED: 'TRUSTED'; + +TYPE_P: 'TYPE'; + +TYPES_P: 'TYPES'; + +UNBOUNDED: 'UNBOUNDED'; + +UNCOMMITTED: 'UNCOMMITTED'; + +UNENCRYPTED: 'UNENCRYPTED'; + +UNKNOWN: 'UNKNOWN'; + +UNLISTEN: 'UNLISTEN'; + +UNLOGGED: 'UNLOGGED'; + +UNTIL: 'UNTIL'; + +UPDATE: 'UPDATE'; + +VACUUM: 'VACUUM'; + +VALID: 'VALID'; + +VALIDATE: 'VALIDATE'; + +VALIDATOR: 'VALIDATOR'; +//VALUE : 'VALUE; + +VARYING: 'VARYING'; + +VERSION_P: 'VERSION'; + +VIEW: 'VIEW'; + +VOLATILE: 'VOLATILE'; + +WHITESPACE_P: 'WHITESPACE'; + +WITHOUT: 'WITHOUT'; + +WORK: 'WORK'; + +WRAPPER: 'WRAPPER'; + +WRITE: 'WRITE'; + +XML_P: 'XML'; + +YEAR_P: 'YEAR'; + +YES_P: 'YES'; + +ZONE: 'ZONE'; +// + +// non-reserved keywords (can not be function or type) + +// + +BETWEEN: 'BETWEEN'; + +BIGINT: 'BIGINT'; + +BIT: 'BIT'; + +BOOLEAN_P: 'BOOLEAN'; + +CHAR_P: 'CHAR'; + +CHARACTER: 'CHARACTER'; + +COALESCE: 'COALESCE'; + +DEC: 'DEC'; + +DECIMAL_P: 'DECIMAL'; + +EXISTS: 'EXISTS'; + +EXTRACT: 'EXTRACT'; + +FLOAT_P: 'FLOAT'; + +GREATEST: 'GREATEST'; + +INOUT: 'INOUT'; + +INT_P: 'INT'; + +INTEGER: 'INTEGER'; + +INTERVAL: 'INTERVAL'; + +LEAST: 'LEAST'; + +NATIONAL: 'NATIONAL'; + +NCHAR: 'NCHAR'; + +NONE: 'NONE'; + +NULLIF: 'NULLIF'; + +NUMERIC: 'NUMERIC'; + +OVERLAY: 'OVERLAY'; + +POSITION: 'POSITION'; + +PRECISION: 'PRECISION'; + +REAL: 'REAL'; + +ROW: 'ROW'; + +SETOF: 'SETOF'; + +SMALLINT: 'SMALLINT'; + +SUBSTRING: 'SUBSTRING'; + +TIME: 'TIME'; + +TIMESTAMP: 'TIMESTAMP'; + +TREAT: 'TREAT'; + +TRIM: 'TRIM'; + +VALUES: 'VALUES'; + +VARCHAR: 'VARCHAR'; + +XMLATTRIBUTES: 'XMLATTRIBUTES'; + +XMLCOMMENT: 'XMLCOMMENT'; + +XMLAGG: 'XMLAGG'; + +XML_IS_WELL_FORMED: 'XML_IS_WELL_FORMED'; + +XML_IS_WELL_FORMED_DOCUMENT: 'XML_IS_WELL_FORMED_DOCUMENT'; + +XML_IS_WELL_FORMED_CONTENT: 'XML_IS_WELL_FORMED_CONTENT'; + +XPATH: 'XPATH'; + +XPATH_EXISTS: 'XPATH_EXISTS'; + +XMLCONCAT: 'XMLCONCAT'; + +XMLELEMENT: 'XMLELEMENT'; + +XMLEXISTS: 'XMLEXISTS'; + +XMLFOREST: 'XMLFOREST'; + +XMLPARSE: 'XMLPARSE'; + +XMLPI: 'XMLPI'; + +XMLROOT: 'XMLROOT'; + +XMLSERIALIZE: 'XMLSERIALIZE'; +//MISSED + +CALL: 'CALL'; + +CURRENT_P: 'CURRENT'; + +ATTACH: 'ATTACH'; + +DETACH: 'DETACH'; + +EXPRESSION: 'EXPRESSION'; + +GENERATED: 'GENERATED'; + +LOGGED: 'LOGGED'; + +STORED: 'STORED'; + +INCLUDE: 'INCLUDE'; + +ROUTINE: 'ROUTINE'; + +TRANSFORM: 'TRANSFORM'; + +IMPORT_P: 'IMPORT'; + +POLICY: 'POLICY'; + +METHOD: 'METHOD'; + +REFERENCING: 'REFERENCING'; + +NEW: 'NEW'; + +OLD: 'OLD'; + +VALUE_P: 'VALUE'; + +SUBSCRIPTION: 'SUBSCRIPTION'; + +PUBLICATION: 'PUBLICATION'; + +OUT_P: 'OUT'; + +END_P: 'END'; + +ROUTINES: 'ROUTINES'; + +SCHEMAS: 'SCHEMAS'; + +PROCEDURES: 'PROCEDURES'; + +INPUT_P: 'INPUT'; + +SUPPORT: 'SUPPORT'; + +PARALLEL: 'PARALLEL'; + +SQL_P: 'SQL'; + +DEPENDS: 'DEPENDS'; + +OVERRIDING: 'OVERRIDING'; + +CONFLICT: 'CONFLICT'; + +SKIP_P: 'SKIP'; + +LOCKED: 'LOCKED'; + +TIES: 'TIES'; + +ROLLUP: 'ROLLUP'; + +CUBE: 'CUBE'; + +GROUPING: 'GROUPING'; + +SETS: 'SETS'; + +TABLESAMPLE: 'TABLESAMPLE'; + +ORDINALITY: 'ORDINALITY'; + +XMLTABLE: 'XMLTABLE'; + +COLUMNS: 'COLUMNS'; + +XMLNAMESPACES: 'XMLNAMESPACES'; + +ROWTYPE: 'ROWTYPE'; + +NORMALIZED: 'NORMALIZED'; + +WITHIN: 'WITHIN'; + +FILTER: 'FILTER'; + +GROUPS: 'GROUPS'; + +OTHERS: 'OTHERS'; + +NFC: 'NFC'; + +NFD: 'NFD'; + +NFKC: 'NFKC'; + +NFKD: 'NFKD'; + +UESCAPE: 'UESCAPE'; + +VIEWS: 'VIEWS'; + +NORMALIZE: 'NORMALIZE'; + +DUMP: 'DUMP'; + +ERROR: 'ERROR'; + +USE_VARIABLE: 'USE_VARIABLE'; + +USE_COLUMN: 'USE_COLUMN'; + +CONSTANT: 'CONSTANT'; + +PERFORM: 'PERFORM'; + +GET: 'GET'; + +DIAGNOSTICS: 'DIAGNOSTICS'; + +STACKED: 'STACKED'; + +ELSIF: 'ELSIF'; + +WHILE: 'WHILE'; + +FOREACH: 'FOREACH'; + +SLICE: 'SLICE'; + +EXIT: 'EXIT'; + +RETURN: 'RETURN'; + +RAISE: 'RAISE'; + +SQLSTATE: 'SQLSTATE'; + +DEBUG: 'DEBUG'; + +INFO: 'INFO'; + +NOTICE: 'NOTICE'; + +WARNING: 'WARNING'; + +EXCEPTION: 'EXCEPTION'; + +ASSERT: 'ASSERT'; + +LOOP: 'LOOP'; + +OPEN: 'OPEN'; + +FORMAT: 'FORMAT'; + + + + + +Identifier: IdentifierStartChar IdentifierChar*; + +fragment IdentifierStartChar options { + caseInsensitive = false; +}: // these are the valid identifier start characters below 0x7F + [a-zA-Z_] + | // these are the valid characters from 0x80 to 0xFF + [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF] + | // these are the letters above 0xFF which only need a single UTF-16 code unit + [\u0100-\uD7FF\uE000-\uFFFF] {this.CharIsLetter()}? + | // letters which require multiple UTF-16 code units + [\uD800-\uDBFF] [\uDC00-\uDFFF] {this.CheckIfUtf32Letter()}? +; + +fragment IdentifierChar: StrictIdentifierChar | '$'; + +fragment StrictIdentifierChar: IdentifierStartChar | [0-9]; +/* Quoted Identifiers + * + * These are divided into four separate tokens, allowing distinction of valid quoted identifiers from invalid quoted + * identifiers without sacrificing the ability of the lexer to reliably recover from lexical errors in the input. + */ + +QuotedIdentifier: UnterminatedQuotedIdentifier '"'; +// This is a quoted identifier which only contains valid characters but is not terminated + +UnterminatedQuotedIdentifier: '"' ('""' | ~ [\u0000"])*; +// This is a quoted identifier which is terminated but contains a \u0000 character + +InvalidQuotedIdentifier: InvalidUnterminatedQuotedIdentifier '"'; +// This is a quoted identifier which is unterminated and contains a \u0000 character + +InvalidUnterminatedQuotedIdentifier: '"' ('""' | ~ '"')*; +/* Unicode Quoted Identifiers + * + * These are divided into four separate tokens, allowing distinction of valid Unicode quoted identifiers from invalid + * Unicode quoted identifiers without sacrificing the ability of the lexer to reliably recover from lexical errors in + * the input. Note that escape sequences are never checked as part of this determination due to the ability of users + * to change the escape character with a UESCAPE clause following the Unicode quoted identifier. + * + * TODO: these rules assume "" is still a valid escape sequence within a Unicode quoted identifier. + */ + +UnicodeQuotedIdentifier: 'U' '&' QuotedIdentifier; +// This is a Unicode quoted identifier which only contains valid characters but is not terminated + +UnterminatedUnicodeQuotedIdentifier: 'U' '&' UnterminatedQuotedIdentifier; +// This is a Unicode quoted identifier which is terminated but contains a \u0000 character + +InvalidUnicodeQuotedIdentifier: 'U' '&' InvalidQuotedIdentifier; +// This is a Unicode quoted identifier which is unterminated and contains a \u0000 character + +InvalidUnterminatedUnicodeQuotedIdentifier: 'U' '&' InvalidUnterminatedQuotedIdentifier; +// + +// CONSTANTS (4.1.2) + +// + +// String Constants (4.1.2.1) + +StringConstant: UnterminatedStringConstant '\''; + +UnterminatedStringConstant: '\'' ('\'\'' | ~ '\'')*; +// String Constants with C-style Escapes (4.1.2.2) + +BeginEscapeStringConstant: 'E' '\'' -> more, pushMode (EscapeStringConstantMode); +// String Constants with Unicode Escapes (4.1.2.3) + +// + +// Note that escape sequences are never checked as part of this token due to the ability of users to change the escape + +// character with a UESCAPE clause following the Unicode string constant. + +// + +// TODO: these rules assume '' is still a valid escape sequence within a Unicode string constant. + +UnicodeEscapeStringConstant: UnterminatedUnicodeEscapeStringConstant '\''; + +UnterminatedUnicodeEscapeStringConstant: 'U' '&' UnterminatedStringConstant; +// Dollar-quoted String Constants (4.1.2.4) + +BeginDollarStringConstant: '$' Tag? '$' {this.PushTag();} -> pushMode (DollarQuotedStringMode); +/* "The tag, if any, of a dollar-quoted string follows the same rules as an + * unquoted identifier, except that it cannot contain a dollar sign." + */ + +fragment Tag: IdentifierStartChar StrictIdentifierChar*; +// Bit-strings Constants (4.1.2.5) + +BinaryStringConstant: UnterminatedBinaryStringConstant '\''; + +UnterminatedBinaryStringConstant: 'B' '\'' [01]*; + +InvalidBinaryStringConstant: InvalidUnterminatedBinaryStringConstant '\''; + +InvalidUnterminatedBinaryStringConstant: 'B' UnterminatedStringConstant; + +HexadecimalStringConstant: UnterminatedHexadecimalStringConstant '\''; + +UnterminatedHexadecimalStringConstant: 'X' '\'' [0-9A-F]*; + +InvalidHexadecimalStringConstant: InvalidUnterminatedHexadecimalStringConstant '\''; + +InvalidUnterminatedHexadecimalStringConstant: 'X' UnterminatedStringConstant; +// Numeric Constants (4.1.2.6) + +Integral: Digits; + +BinaryIntegral: '0b' Digits; + +OctalIntegral: '0o' Digits; + +HexadecimalIntegral: '0x' Digits; + +NumericFail: Digits '..' {this.HandleNumericFail();}; + +Numeric: + Digits '.' Digits? /*? replaced with + to solve problem with DOT_DOT .. but this surely must be rewriten */ ( + 'E' [+-]? Digits + )? + | '.' Digits ('E' [+-]? Digits)? + | Digits 'E' [+-]? Digits +; + +fragment Digits: [0-9]+; + +PLSQLVARIABLENAME: ':' [A-Z_] [A-Z_0-9$]*; + +PLSQLIDENTIFIER: ':"' ('\\' . | '""' | ~ ('"' | '\\'))* '"'; +// + +// WHITESPACE (4.1) + +// + +Whitespace: [ \t]+ -> channel (HIDDEN); + +Newline: ('\r' '\n'? | '\n') -> channel (HIDDEN); +// + +// COMMENTS (4.1.5) + +// + +LineComment: '--' ~ [\r\n]* -> channel (HIDDEN); + +BlockComment: + ('/*' ('/'* BlockComment | ~ [/*] | '/'+ ~ [/*] | '*'+ ~ [/*])* '*'* '*/') -> channel (HIDDEN) +; + +UnterminatedBlockComment: + '/*' ( + '/'* BlockComment + | // these characters are not part of special sequences in a block comment + ~ [/*] + | // handle / or * characters which are not part of /* or */ and do not appear at the end of the file + ('/'+ ~ [/*] | '*'+ ~ [/*]) + )* + // Handle the case of / or * characters at the end of the file, or a nested unterminated block comment + ('/'+ | '*'+ | '/'* UnterminatedBlockComment)? + // Optional assertion to make sure this rule is working as intended + {this.UnterminatedBlockCommentDebugAssert();} +; +// + +// META-COMMANDS + +// + +// http://www.postgresql.org/docs/9.3/static/app-psql.html + +MetaCommand: '\\' -> pushMode(META), more ; + +// + +// ERROR + +// + +// Any character which does not match one of the above rules will appear in the token stream as an ErrorCharacter token. + +// This ensures the lexer itself will never encounter a syntax error, so all error handling may be performed by the + +// parser. + +ErrorCharacter: .; + +mode EscapeStringConstantMode; +EscapeStringConstant: EscapeStringText '\'' -> mode (AfterEscapeStringConstantMode); + +UnterminatedEscapeStringConstant: + EscapeStringText + // Handle a final unmatched \ character appearing at the end of the file + '\\'? EOF +; + +fragment EscapeStringText options { caseInsensitive = false; }: + ( + '\'\'' + | '\\' ( + // two-digit hex escapes are still valid when treated as single-digit escapes + 'x' [0-9a-fA-F] + | 'u' [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] + | 'U' [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] + | // Any character other than the Unicode escapes can follow a backslash. Some have special meaning, + // but that doesn't affect the syntax. + ~ [xuU] + ) + | ~ ['\\] + )* +; + +InvalidEscapeStringConstant: InvalidEscapeStringText '\'' -> mode (AfterEscapeStringConstantMode); + +InvalidUnterminatedEscapeStringConstant: + InvalidEscapeStringText + // Handle a final unmatched \ character appearing at the end of the file + '\\'? EOF +; + +fragment InvalidEscapeStringText: ('\'\'' | '\\' . | ~ ['\\])*; + +mode AfterEscapeStringConstantMode; +AfterEscapeStringConstantMode_Whitespace: Whitespace -> type (Whitespace), channel (HIDDEN); + +AfterEscapeStringConstantMode_Newline: + Newline -> type (Newline), channel (HIDDEN), mode (AfterEscapeStringConstantWithNewlineMode) +; + +AfterEscapeStringConstantMode_NotContinued: + -> skip, popMode +; + +mode AfterEscapeStringConstantWithNewlineMode; +AfterEscapeStringConstantWithNewlineMode_Whitespace: + Whitespace -> type (Whitespace), channel (HIDDEN) +; + +AfterEscapeStringConstantWithNewlineMode_Newline: Newline -> type (Newline), channel (HIDDEN); + +AfterEscapeStringConstantWithNewlineMode_Continued: + '\'' -> more, mode (EscapeStringConstantMode) +; + +AfterEscapeStringConstantWithNewlineMode_NotContinued: + -> skip, popMode +; + +mode DollarQuotedStringMode; +DollarText: + ~ '$'+ + //| '$'([0-9])+ + | // this alternative improves the efficiency of handling $ characters within a dollar-quoted string which are + + // not part of the ending tag. + '$' ~ '$'* +; + +// NB: Next rule on two lines in order to make transformGrammar.py easy. +EndDollarStringConstant: ('$' Tag? '$') {this.IsTag()}? + {this.PopTag();} -> popMode; + +mode META; +MetaSemi : {this.IsSemiColon()}? ';' -> type(SEMI), popMode ; +MetaOther : ~[;\r\n\\"] .*? ('\\\\' | [\r\n]+) -> type(SEMI), popMode ; diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 new file mode 100644 index 0000000..cb1dfe8 --- /dev/null +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -0,0 +1,5472 @@ +/* +PostgreSQL grammar. +The MIT License (MIT). +Copyright (c) 2021-2023, Oleksii Kovalov (Oleksii.Kovalov@outlook.com). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging + +parser grammar PostgreSQLParser; + +options { + tokenVocab = PostgreSQLLexer; + superClass = PostgreSQLParserBase; +} + +// Insert here @header for C++ parser. + +root + : stmtblock EOF + ; + +stmtblock + : stmtmulti + ; + +stmtmulti + : stmt? (SEMI stmt?)* + ; + +stmt + : altereventtrigstmt + | altercollationstmt + | alterdatabasestmt + | alterdatabasesetstmt + | alterdefaultprivilegesstmt + | alterdomainstmt + | alterenumstmt + | alterextensionstmt + | alterextensioncontentsstmt + | alterfdwstmt + | alterforeignserverstmt + | alterfunctionstmt + | altergroupstmt + | alterobjectdependsstmt + | alterobjectschemastmt + | alterownerstmt + | alteroperatorstmt + | altertypestmt + | alterpolicystmt + | alterseqstmt + | altersystemstmt + | altertablestmt + | altertblspcstmt + | altercompositetypestmt + | alterpublicationstmt + | alterrolesetstmt + | alterrolestmt + | altersubscriptionstmt + | alterstatsstmt + | altertsconfigurationstmt + | altertsdictionarystmt + | alterusermappingstmt + | analyzestmt + | callstmt + | checkpointstmt + | closeportalstmt + | clusterstmt + | commentstmt + | constraintssetstmt + | copystmt + | createamstmt + | createasstmt + | createassertionstmt + | createcaststmt + | createconversionstmt + | createdomainstmt + | createextensionstmt + | createfdwstmt + | createforeignserverstmt + | createforeigntablestmt + | createfunctionstmt + | creategroupstmt + | creatematviewstmt + | createopclassstmt + | createopfamilystmt + | createpublicationstmt + | alteropfamilystmt + | createpolicystmt + | createplangstmt + | createschemastmt + | createseqstmt + | createstmt + | createsubscriptionstmt + | createstatsstmt + | createtablespacestmt + | createtransformstmt + | createtrigstmt + | createeventtrigstmt + | createrolestmt + | createuserstmt + | createusermappingstmt + | createdbstmt + | deallocatestmt + | declarecursorstmt + | definestmt + | deletestmt + | discardstmt + | dostmt + | dropcaststmt + | dropopclassstmt + | dropopfamilystmt + | dropownedstmt + | dropstmt + | dropsubscriptionstmt + | droptablespacestmt + | droptransformstmt + | droprolestmt + | dropusermappingstmt + | dropdbstmt + | executestmt + | explainstmt + | fetchstmt + | grantstmt + | grantrolestmt + | importforeignschemastmt + | indexstmt + | insertstmt + | mergestmt + | listenstmt + | refreshmatviewstmt + | loadstmt + | lockstmt + | notifystmt + | preparestmt + | reassignownedstmt + | reindexstmt + | removeaggrstmt + | removefuncstmt + | removeoperstmt + | renamestmt + | revokestmt + | revokerolestmt + | rulestmt + | seclabelstmt + | selectstmt + | transactionstmt + | truncatestmt + | unlistenstmt + | updatestmt + | vacuumstmt + | variableresetstmt + | variablesetstmt + | variableshowstmt + | viewstmt + ; + +callstmt + : CALL func_application + ; + +createrolestmt + : CREATE ROLE roleid with_? optrolelist + ; + +with_ + : WITH + //| WITH_LA + + ; + +optrolelist + : createoptroleelem* + ; + +alteroptrolelist + : alteroptroleelem* + ; + +alteroptroleelem + : PASSWORD (sconst | NULL_P) + | (ENCRYPTED | UNENCRYPTED) PASSWORD sconst + | INHERIT + | CONNECTION LIMIT signediconst + | VALID UNTIL sconst + | USER role_list + | identifier + ; + +createoptroleelem + : alteroptroleelem + | SYSID iconst + | ADMIN role_list + | ROLE role_list + | IN_P (ROLE | GROUP_P) role_list + ; + +createuserstmt + : CREATE USER roleid with_? optrolelist + ; + +alterrolestmt + : ALTER (ROLE | USER) rolespec with_? alteroptrolelist + ; + +in_database_ + : + IN_P DATABASE name + ; + +alterrolesetstmt + : ALTER (ROLE | USER) ALL? rolespec in_database_? setresetclause + ; + +droprolestmt + : DROP (ROLE | USER | GROUP_P) (IF_P EXISTS)? role_list + ; + +creategroupstmt + : CREATE GROUP_P roleid with_? optrolelist + ; + +altergroupstmt + : ALTER GROUP_P rolespec add_drop USER role_list + ; + +add_drop + : ADD_P + | DROP + ; + +createschemastmt + : CREATE SCHEMA (IF_P NOT EXISTS)? (optschemaname? AUTHORIZATION rolespec | colid) optschemaeltlist + ; + +optschemaname + : colid + + ; + +optschemaeltlist + : schema_stmt* + ; + +schema_stmt + : createstmt + | indexstmt + | createseqstmt + | createtrigstmt + | grantstmt + | viewstmt + ; + +variablesetstmt + : SET (LOCAL | SESSION)? set_rest + ; + +set_rest + : TRANSACTION transaction_mode_list + | SESSION CHARACTERISTICS AS TRANSACTION transaction_mode_list + | set_rest_more + ; + +generic_set + : var_name (TO | EQUAL) (var_list | DEFAULT) + ; + +set_rest_more + : generic_set + | var_name FROM CURRENT_P + | TIME ZONE zone_value + | CATALOG sconst + | SCHEMA sconst + | NAMES encoding_? + | ROLE nonreservedword_or_sconst + | SESSION AUTHORIZATION nonreservedword_or_sconst + | XML_P OPTION document_or_content + | TRANSACTION SNAPSHOT sconst + ; + +var_name + : colid (DOT colid)* + ; + +var_list + : var_value (COMMA var_value)* + ; + +var_value + : boolean_or_string_ + | numericonly + ; + +iso_level + : READ (UNCOMMITTED | COMMITTED) + | REPEATABLE READ + | SERIALIZABLE + ; + +boolean_or_string_ + : TRUE_P + | FALSE_P + | ON + | nonreservedword_or_sconst + ; + +zone_value + : sconst + | identifier + | constinterval sconst interval_? + | constinterval OPEN_PAREN iconst CLOSE_PAREN sconst + | numericonly + | DEFAULT + | LOCAL + ; + +encoding_ + : sconst + | DEFAULT + + ; + +nonreservedword_or_sconst + : nonreservedword + | sconst + ; + +variableresetstmt + : RESET reset_rest + ; + +reset_rest + : generic_reset + | TIME ZONE + | TRANSACTION ISOLATION LEVEL + | SESSION AUTHORIZATION + ; + +generic_reset + : var_name + | ALL + ; + +setresetclause + : SET set_rest + | variableresetstmt + ; + +functionsetresetclause + : SET set_rest_more + | variableresetstmt + ; + +variableshowstmt + : SHOW (var_name | TIME ZONE | TRANSACTION ISOLATION LEVEL | SESSION AUTHORIZATION | ALL) + ; + +constraintssetstmt + : SET CONSTRAINTS constraints_set_list constraints_set_mode + ; + +constraints_set_list + : ALL + | qualified_name_list + ; + +constraints_set_mode + : DEFERRED + | IMMEDIATE + ; + +checkpointstmt + : CHECKPOINT + ; + +discardstmt + : DISCARD (ALL | TEMP | TEMPORARY | PLANS | SEQUENCES) + ; + +altertablestmt + : ALTER TABLE (IF_P EXISTS)? relation_expr (alter_table_cmds | partition_cmd) + | ALTER TABLE ALL IN_P TABLESPACE name (OWNED BY role_list)? SET TABLESPACE name nowait_? + | ALTER INDEX (IF_P EXISTS)? qualified_name (alter_table_cmds | index_partition_cmd) + | ALTER INDEX ALL IN_P TABLESPACE name (OWNED BY role_list)? SET TABLESPACE name nowait_? + | ALTER SEQUENCE (IF_P EXISTS)? qualified_name alter_table_cmds + | ALTER VIEW (IF_P EXISTS)? qualified_name alter_table_cmds + | ALTER MATERIALIZED VIEW (IF_P EXISTS)? qualified_name alter_table_cmds + | ALTER MATERIALIZED VIEW ALL IN_P TABLESPACE name (OWNED BY role_list)? SET TABLESPACE name nowait_? + | ALTER FOREIGN TABLE (IF_P EXISTS)? relation_expr alter_table_cmds + ; + +alter_table_cmds + : alter_table_cmd (COMMA alter_table_cmd)* + ; + +partition_cmd + : ATTACH PARTITION qualified_name partitionboundspec + | DETACH PARTITION qualified_name + ; + +index_partition_cmd + : ATTACH PARTITION qualified_name + ; + +alter_table_cmd + : ADD_P columnDef + | ADD_P IF_P NOT EXISTS columnDef + | ADD_P COLUMN columnDef + | ADD_P COLUMN IF_P NOT EXISTS columnDef + | ALTER column_? colid alter_column_default + | ALTER column_? colid DROP NOT NULL_P + | ALTER column_? colid SET NOT NULL_P + | ALTER column_? colid DROP EXPRESSION + | ALTER column_? colid DROP EXPRESSION IF_P EXISTS + | ALTER column_? colid SET STATISTICS signediconst + | ALTER column_? iconst SET STATISTICS signediconst + | ALTER column_? colid SET reloptions + | ALTER column_? colid RESET reloptions + | ALTER column_? colid SET STORAGE colid + | ALTER column_? colid ADD_P GENERATED generated_when AS IDENTITY_P optparenthesizedseqoptlist? + | ALTER column_? colid alter_identity_column_option_list + | ALTER column_? colid DROP IDENTITY_P + | ALTER column_? colid DROP IDENTITY_P IF_P EXISTS + | DROP column_? IF_P EXISTS colid drop_behavior_? + | DROP column_? colid drop_behavior_? + | ALTER column_? colid set_data_? TYPE_P typename collate_clause_? alter_using? + | ALTER column_? colid alter_generic_options + | ADD_P tableconstraint + | ALTER CONSTRAINT name constraintattributespec + | VALIDATE CONSTRAINT name + | DROP CONSTRAINT IF_P EXISTS name drop_behavior_? + | DROP CONSTRAINT name drop_behavior_? + | SET WITHOUT OIDS + | CLUSTER ON name + | SET WITHOUT CLUSTER + | SET LOGGED + | SET UNLOGGED + | ENABLE_P TRIGGER name + | ENABLE_P ALWAYS TRIGGER name + | ENABLE_P REPLICA TRIGGER name + | ENABLE_P TRIGGER ALL + | ENABLE_P TRIGGER USER + | DISABLE_P TRIGGER name + | DISABLE_P TRIGGER ALL + | DISABLE_P TRIGGER USER + | ENABLE_P RULE name + | ENABLE_P ALWAYS RULE name + | ENABLE_P REPLICA RULE name + | DISABLE_P RULE name + | INHERIT qualified_name + | NO INHERIT qualified_name + | OF any_name + | NOT OF + | OWNER TO rolespec + | SET TABLESPACE name + | SET reloptions + | RESET reloptions + | REPLICA IDENTITY_P replica_identity + | ENABLE_P ROW LEVEL SECURITY + | DISABLE_P ROW LEVEL SECURITY + | FORCE ROW LEVEL SECURITY + | NO FORCE ROW LEVEL SECURITY + | alter_generic_options + ; + +alter_column_default + : SET DEFAULT a_expr + | DROP DEFAULT + ; + +drop_behavior_ + : CASCADE + | RESTRICT + + ; + +collate_clause_ + : COLLATE any_name + + ; + +alter_using + : USING a_expr + + ; + +replica_identity + : NOTHING + | FULL + | DEFAULT + | USING INDEX name + ; + +reloptions + : OPEN_PAREN reloption_list CLOSE_PAREN + ; + +reloptions_ + : WITH reloptions + + ; + +reloption_list + : reloption_elem (COMMA reloption_elem)* + ; + +reloption_elem + : colLabel (EQUAL def_arg | DOT colLabel (EQUAL def_arg)?)? + ; + +alter_identity_column_option_list + : alter_identity_column_option+ + ; + +alter_identity_column_option + : RESTART (with_? numericonly)? + | SET (seqoptelem | GENERATED generated_when) + ; + +partitionboundspec + : FOR VALUES WITH OPEN_PAREN hash_partbound CLOSE_PAREN + | FOR VALUES IN_P OPEN_PAREN expr_list CLOSE_PAREN + | FOR VALUES FROM OPEN_PAREN expr_list CLOSE_PAREN TO OPEN_PAREN expr_list CLOSE_PAREN + | DEFAULT + ; + +hash_partbound_elem + : nonreservedword iconst + ; + +hash_partbound + : hash_partbound_elem (COMMA hash_partbound_elem)* + ; + +altercompositetypestmt + : ALTER TYPE_P any_name alter_type_cmds + ; + +alter_type_cmds + : alter_type_cmd (COMMA alter_type_cmd)* + ; + +alter_type_cmd + : ADD_P ATTRIBUTE tablefuncelement drop_behavior_? + | DROP ATTRIBUTE (IF_P EXISTS)? colid drop_behavior_? + | ALTER ATTRIBUTE colid set_data_? TYPE_P typename collate_clause_? drop_behavior_? + ; + +closeportalstmt + : CLOSE (cursor_name | ALL) + ; + +copystmt + : COPY binary_? qualified_name column_list_? copy_from program_? copy_file_name copy_delimiter? with_? copy_options where_clause? + | COPY OPEN_PAREN preparablestmt CLOSE_PAREN TO program_? copy_file_name with_? copy_options + ; + +copy_from + : FROM + | TO + ; + +program_ + : PROGRAM + + ; + +copy_file_name + : sconst + | STDIN + | STDOUT + ; + +copy_options + : copy_opt_list + | OPEN_PAREN copy_generic_opt_list CLOSE_PAREN + ; + +copy_opt_list + : copy_opt_item* + ; + +copy_opt_item + : BINARY + | FREEZE + | DELIMITER as_? sconst + | NULL_P as_? sconst + | CSV + | HEADER_P + | QUOTE as_? sconst + | ESCAPE as_? sconst + | FORCE QUOTE columnlist + | FORCE QUOTE STAR + | FORCE NOT NULL_P columnlist + | FORCE NULL_P columnlist + | ENCODING sconst + ; + +binary_ + : BINARY + + ; + +copy_delimiter + : using_? DELIMITERS sconst + + ; + +using_ + : USING + + ; + +copy_generic_opt_list + : copy_generic_opt_elem (COMMA copy_generic_opt_elem)* + ; + +copy_generic_opt_elem + : colLabel copy_generic_opt_arg? + ; + +copy_generic_opt_arg + : boolean_or_string_ + | numericonly + | STAR + | OPEN_PAREN copy_generic_opt_arg_list CLOSE_PAREN + + ; + +copy_generic_opt_arg_list + : copy_generic_opt_arg_list_item (COMMA copy_generic_opt_arg_list_item)* + ; + +copy_generic_opt_arg_list_item + : boolean_or_string_ + ; + +createstmt + : CREATE opttemp? TABLE (IF_P NOT EXISTS)? qualified_name ( + OPEN_PAREN opttableelementlist? CLOSE_PAREN optinherit? optpartitionspec? table_access_method_clause? optwith? oncommitoption? opttablespace? + | OF any_name opttypedtableelementlist? optpartitionspec? table_access_method_clause? optwith? oncommitoption? opttablespace? + | PARTITION OF qualified_name opttypedtableelementlist? partitionboundspec optpartitionspec? table_access_method_clause? optwith? oncommitoption? + opttablespace? + ) + ; + +opttemp + : TEMPORARY + | TEMP + | LOCAL (TEMPORARY | TEMP) + | GLOBAL (TEMPORARY | TEMP) + | UNLOGGED + + ; + +opttableelementlist + : tableelementlist + + ; + +opttypedtableelementlist + : OPEN_PAREN typedtableelementlist CLOSE_PAREN + + ; + +tableelementlist + : tableelement (COMMA tableelement)* + ; + +typedtableelementlist + : typedtableelement (COMMA typedtableelement)* + ; + +tableelement + : tableconstraint + | tablelikeclause + | columnDef + ; + +typedtableelement + : columnOptions + | tableconstraint + ; + +columnDef + : colid typename create_generic_options? colquallist + ; + +columnOptions + : colid (WITH OPTIONS)? colquallist + ; + +colquallist + : colconstraint* + ; + +colconstraint + : CONSTRAINT name colconstraintelem + | colconstraintelem + | constraintattr + | COLLATE any_name + ; + +colconstraintelem + : NOT NULL_P + | NULL_P + | UNIQUE definition_? optconstablespace? + | PRIMARY KEY definition_? optconstablespace? + | CHECK OPEN_PAREN a_expr CLOSE_PAREN no_inherit_? + | DEFAULT b_expr + | GENERATED generated_when AS ( + IDENTITY_P optparenthesizedseqoptlist? + | OPEN_PAREN a_expr CLOSE_PAREN STORED + ) + | REFERENCES qualified_name column_list_? key_match? key_actions? + ; + +generated_when + : ALWAYS + | BY DEFAULT + ; + +constraintattr + : DEFERRABLE + | NOT DEFERRABLE + | INITIALLY (DEFERRED | IMMEDIATE) + ; + +tablelikeclause + : LIKE qualified_name tablelikeoptionlist + ; + +tablelikeoptionlist + : ((INCLUDING | EXCLUDING) tablelikeoption)* + ; + +tablelikeoption + : COMMENTS + | CONSTRAINTS + | DEFAULTS + | IDENTITY_P + | GENERATED + | INDEXES + | STATISTICS + | STORAGE + | ALL + ; + +tableconstraint + : CONSTRAINT name constraintelem + | constraintelem + ; + +constraintelem + : CHECK OPEN_PAREN a_expr CLOSE_PAREN constraintattributespec + | UNIQUE ( + OPEN_PAREN columnlist CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec + | existingindex constraintattributespec + ) + | PRIMARY KEY ( + OPEN_PAREN columnlist CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec + | existingindex constraintattributespec + ) + | EXCLUDE access_method_clause? OPEN_PAREN exclusionconstraintlist CLOSE_PAREN c_include_? definition_? optconstablespace? exclusionwhereclause? + constraintattributespec + | FOREIGN KEY OPEN_PAREN columnlist CLOSE_PAREN REFERENCES qualified_name column_list_? key_match? key_actions? constraintattributespec + ; + +no_inherit_ + : NO INHERIT + + ; + +column_list_ + : OPEN_PAREN columnlist CLOSE_PAREN + + ; + +columnlist + : columnElem (COMMA columnElem)* + ; + +columnElem + : colid + ; + +c_include_ + : INCLUDE OPEN_PAREN columnlist CLOSE_PAREN + + ; + +key_match + : MATCH (FULL | PARTIAL | SIMPLE) + + ; + +exclusionconstraintlist + : exclusionconstraintelem (COMMA exclusionconstraintelem)* + ; + +exclusionconstraintelem + : index_elem WITH (any_operator | OPERATOR OPEN_PAREN any_operator CLOSE_PAREN) + ; + +exclusionwhereclause + : WHERE OPEN_PAREN a_expr CLOSE_PAREN + + ; + +key_actions + : key_update + | key_delete + | key_update key_delete + | key_delete key_update + + ; + +key_update + : ON UPDATE key_action + ; + +key_delete + : ON DELETE_P key_action + ; + +key_action + : NO ACTION + | RESTRICT + | CASCADE + | SET (NULL_P | DEFAULT) + ; + +optinherit + : INHERITS OPEN_PAREN qualified_name_list CLOSE_PAREN + + ; + +optpartitionspec + : partitionspec + + ; + +partitionspec + : PARTITION BY colid OPEN_PAREN part_params CLOSE_PAREN + ; + +part_params + : part_elem (COMMA part_elem)* + ; + +part_elem + : colid collate_? class_? + | func_expr_windowless collate_? class_? + | OPEN_PAREN a_expr CLOSE_PAREN collate_? class_? + ; + +table_access_method_clause + : USING name + + ; + +optwith + : WITH reloptions + | WITHOUT OIDS + + ; + +oncommitoption + : ON COMMIT (DROP | DELETE_P ROWS | PRESERVE ROWS) + + ; + +opttablespace + : TABLESPACE name + + ; + +optconstablespace + : USING INDEX TABLESPACE name + + ; + +existingindex + : USING INDEX name + ; + +createstatsstmt + : CREATE STATISTICS (IF_P NOT EXISTS)? any_name name_list_? ON expr_list FROM from_list + ; + +alterstatsstmt + : ALTER STATISTICS (IF_P EXISTS)? any_name SET STATISTICS signediconst + ; + +createasstmt + : CREATE opttemp? TABLE (IF_P NOT EXISTS)? create_as_target AS selectstmt with_data_? + ; + +create_as_target + : qualified_name column_list_? table_access_method_clause? optwith? oncommitoption? opttablespace? + ; + +with_data_ + : WITH (DATA_P | NO DATA_P) + + ; + +creatematviewstmt + : CREATE optnolog? MATERIALIZED VIEW (IF_P NOT EXISTS)? create_mv_target AS selectstmt with_data_? + ; + +create_mv_target + : qualified_name column_list_? table_access_method_clause? reloptions_? opttablespace? + ; + +optnolog + : UNLOGGED + + ; + +refreshmatviewstmt + : REFRESH MATERIALIZED VIEW concurrently_? qualified_name with_data_? + ; + +createseqstmt + : CREATE opttemp? SEQUENCE (IF_P NOT EXISTS)? qualified_name optseqoptlist? + ; + +alterseqstmt + : ALTER SEQUENCE (IF_P EXISTS)? qualified_name seqoptlist + ; + +optseqoptlist + : seqoptlist + + ; + +optparenthesizedseqoptlist + : OPEN_PAREN seqoptlist CLOSE_PAREN + + ; + +seqoptlist + : seqoptelem+ + ; + +seqoptelem + : AS simpletypename + | CACHE numericonly + | CYCLE + | INCREMENT by_? numericonly + | MAXVALUE numericonly + | MINVALUE numericonly + | NO (MAXVALUE | MINVALUE | CYCLE) + | OWNED BY any_name + | SEQUENCE NAME_P any_name + | START with_? numericonly + | RESTART with_? numericonly? + ; + +by_ + : BY + + ; + +numericonly + : fconst + | PLUS fconst + | MINUS fconst + | signediconst + ; + +numericonly_list + : numericonly (COMMA numericonly)* + ; + +createplangstmt + : CREATE or_replace_? trusted_? procedural_? LANGUAGE name ( + HANDLER handler_name inline_handler_? validator_? + )? + ; + +trusted_ + : TRUSTED + + ; + +handler_name + : name attrs? + ; + +inline_handler_ + : INLINE_P handler_name + + ; + +validator_clause + : VALIDATOR handler_name + | NO VALIDATOR + ; + +validator_ + : validator_clause + + ; + +procedural_ + : PROCEDURAL + + ; + +createtablespacestmt + : CREATE TABLESPACE name opttablespaceowner? LOCATION sconst reloptions_? + ; + +opttablespaceowner + : OWNER rolespec + + ; + +droptablespacestmt + : DROP TABLESPACE (IF_P EXISTS)? name + ; + +createextensionstmt + : CREATE EXTENSION (IF_P NOT EXISTS)? name with_? create_extension_opt_list + ; + +create_extension_opt_list + : create_extension_opt_item* + ; + +create_extension_opt_item + : SCHEMA name + | VERSION_P nonreservedword_or_sconst + | FROM nonreservedword_or_sconst + | CASCADE + ; + +alterextensionstmt + : ALTER EXTENSION name UPDATE alter_extension_opt_list + ; + +alter_extension_opt_list + : alter_extension_opt_item* + ; + +alter_extension_opt_item + : TO nonreservedword_or_sconst + ; + +alterextensioncontentsstmt + : ALTER EXTENSION name add_drop object_type_name name + | ALTER EXTENSION name add_drop object_type_any_name any_name + | ALTER EXTENSION name add_drop AGGREGATE aggregate_with_argtypes + | ALTER EXTENSION name add_drop CAST OPEN_PAREN typename AS typename CLOSE_PAREN + | ALTER EXTENSION name add_drop DOMAIN_P typename + | ALTER EXTENSION name add_drop FUNCTION function_with_argtypes + | ALTER EXTENSION name add_drop OPERATOR operator_with_argtypes + | ALTER EXTENSION name add_drop OPERATOR CLASS any_name USING name + | ALTER EXTENSION name add_drop OPERATOR FAMILY any_name USING name + | ALTER EXTENSION name add_drop PROCEDURE function_with_argtypes + | ALTER EXTENSION name add_drop ROUTINE function_with_argtypes + | ALTER EXTENSION name add_drop TRANSFORM FOR typename LANGUAGE name + | ALTER EXTENSION name add_drop TYPE_P typename + ; + +createfdwstmt + : CREATE FOREIGN DATA_P WRAPPER name fdw_options_? create_generic_options? + ; + +fdw_option + : HANDLER handler_name + | NO HANDLER + | VALIDATOR handler_name + | NO VALIDATOR + ; + +fdw_options + : fdw_option+ + ; + +fdw_options_ + : fdw_options + + ; + +alterfdwstmt + : ALTER FOREIGN DATA_P WRAPPER name fdw_options_? alter_generic_options + | ALTER FOREIGN DATA_P WRAPPER name fdw_options + ; + +create_generic_options + : OPTIONS OPEN_PAREN generic_option_list CLOSE_PAREN + + ; + +generic_option_list + : generic_option_elem (COMMA generic_option_elem)* + ; + +alter_generic_options + : OPTIONS OPEN_PAREN alter_generic_option_list CLOSE_PAREN + ; + +alter_generic_option_list + : alter_generic_option_elem (COMMA alter_generic_option_elem)* + ; + +alter_generic_option_elem + : generic_option_elem + | SET generic_option_elem + | ADD_P generic_option_elem + | DROP generic_option_name + ; + +generic_option_elem + : generic_option_name generic_option_arg + ; + +generic_option_name + : colLabel + ; + +generic_option_arg + : sconst + ; + +createforeignserverstmt + : CREATE SERVER name type_? foreign_server_version_? FOREIGN DATA_P WRAPPER name create_generic_options? + | CREATE SERVER IF_P NOT EXISTS name type_? foreign_server_version_? FOREIGN DATA_P WRAPPER name create_generic_options? + ; + +type_ + : TYPE_P sconst + + ; + +foreign_server_version + : VERSION_P (sconst | NULL_P) + ; + +foreign_server_version_ + : foreign_server_version + + ; + +alterforeignserverstmt + : ALTER SERVER name (alter_generic_options | foreign_server_version alter_generic_options?) + ; + +createforeigntablestmt + : CREATE FOREIGN TABLE qualified_name OPEN_PAREN opttableelementlist? CLOSE_PAREN optinherit? SERVER name create_generic_options? + | CREATE FOREIGN TABLE IF_P NOT EXISTS qualified_name OPEN_PAREN opttableelementlist? CLOSE_PAREN optinherit? SERVER name create_generic_options? + | CREATE FOREIGN TABLE qualified_name PARTITION OF qualified_name opttypedtableelementlist? partitionboundspec SERVER name create_generic_options? + | CREATE FOREIGN TABLE IF_P NOT EXISTS qualified_name PARTITION OF qualified_name opttypedtableelementlist? partitionboundspec SERVER name + create_generic_options? + ; + +importforeignschemastmt + : IMPORT_P FOREIGN SCHEMA name import_qualification? FROM SERVER name INTO name create_generic_options? + ; + +import_qualification_type + : LIMIT TO + | EXCEPT + ; + +import_qualification + : import_qualification_type OPEN_PAREN relation_expr_list CLOSE_PAREN + + ; + +createusermappingstmt + : CREATE USER MAPPING FOR auth_ident SERVER name create_generic_options? + | CREATE USER MAPPING IF_P NOT EXISTS FOR auth_ident SERVER name create_generic_options? + ; + +auth_ident + : rolespec + | USER + ; + +dropusermappingstmt + : DROP USER MAPPING FOR auth_ident SERVER name + | DROP USER MAPPING IF_P EXISTS FOR auth_ident SERVER name + ; + +alterusermappingstmt + : ALTER USER MAPPING FOR auth_ident SERVER name alter_generic_options + ; + +createpolicystmt + : CREATE POLICY name ON qualified_name rowsecuritydefaultpermissive? rowsecuritydefaultforcmd? rowsecuritydefaulttorole? rowsecurityoptionalexpr? + rowsecurityoptionalwithcheck? + ; + +alterpolicystmt + : ALTER POLICY name ON qualified_name rowsecurityoptionaltorole? rowsecurityoptionalexpr? rowsecurityoptionalwithcheck? + ; + +rowsecurityoptionalexpr + : USING OPEN_PAREN a_expr CLOSE_PAREN + + ; + +rowsecurityoptionalwithcheck + : WITH CHECK OPEN_PAREN a_expr CLOSE_PAREN + + ; + +rowsecuritydefaulttorole + : TO role_list + + ; + +rowsecurityoptionaltorole + : TO role_list + + ; + +rowsecuritydefaultpermissive + : AS identifier + + ; + +rowsecuritydefaultforcmd + : FOR row_security_cmd + + ; + +row_security_cmd + : ALL + | SELECT + | INSERT + | UPDATE + | DELETE_P + ; + +createamstmt + : CREATE ACCESS METHOD name TYPE_P am_type HANDLER handler_name + ; + +am_type + : INDEX + | TABLE + ; + +createtrigstmt + : CREATE TRIGGER name triggeractiontime triggerevents ON qualified_name triggerreferencing? triggerforspec? triggerwhen? EXECUTE + function_or_procedure func_name OPEN_PAREN triggerfuncargs CLOSE_PAREN + | CREATE CONSTRAINT TRIGGER name AFTER triggerevents ON qualified_name optconstrfromtable? constraintattributespec FOR EACH ROW triggerwhen? EXECUTE + function_or_procedure func_name OPEN_PAREN triggerfuncargs CLOSE_PAREN + ; + +triggeractiontime + : BEFORE + | AFTER + | INSTEAD OF + ; + +triggerevents + : triggeroneevent (OR triggeroneevent)* + ; + +triggeroneevent + : INSERT + | DELETE_P + | UPDATE + | UPDATE OF columnlist + | TRUNCATE + ; + +triggerreferencing + : REFERENCING triggertransitions + + ; + +triggertransitions + : triggertransition+ + ; + +triggertransition + : transitionoldornew transitionrowortable as_? transitionrelname + ; + +transitionoldornew + : NEW + | OLD + ; + +transitionrowortable + : TABLE + | ROW + ; + +transitionrelname + : colid + ; + +triggerforspec + : FOR triggerforopteach? triggerfortype + + ; + +triggerforopteach + : EACH + + ; + +triggerfortype + : ROW + | STATEMENT + ; + +triggerwhen + : WHEN OPEN_PAREN a_expr CLOSE_PAREN + + ; + +function_or_procedure + : FUNCTION + | PROCEDURE + ; + +triggerfuncargs + : (triggerfuncarg |) (COMMA triggerfuncarg)* + ; + +triggerfuncarg + : iconst + | fconst + | sconst + | colLabel + ; + +optconstrfromtable + : FROM qualified_name + + ; + +constraintattributespec + : constraintattributeElem* + ; + +constraintattributeElem + : NOT DEFERRABLE + | DEFERRABLE + | INITIALLY IMMEDIATE + | INITIALLY DEFERRED + | NOT VALID + | NO INHERIT + ; + +createeventtrigstmt + : CREATE EVENT TRIGGER name ON colLabel EXECUTE function_or_procedure func_name OPEN_PAREN CLOSE_PAREN + | CREATE EVENT TRIGGER name ON colLabel WHEN event_trigger_when_list EXECUTE function_or_procedure func_name OPEN_PAREN CLOSE_PAREN + ; + +event_trigger_when_list + : event_trigger_when_item (AND event_trigger_when_item)* + ; + +event_trigger_when_item + : colid IN_P OPEN_PAREN event_trigger_value_list CLOSE_PAREN + ; + +event_trigger_value_list + : sconst (COMMA sconst)* + ; + +altereventtrigstmt + : ALTER EVENT TRIGGER name enable_trigger + ; + +enable_trigger + : ENABLE_P + | ENABLE_P REPLICA + | ENABLE_P ALWAYS + | DISABLE_P + ; + +createassertionstmt + : CREATE ASSERTION any_name CHECK OPEN_PAREN a_expr CLOSE_PAREN constraintattributespec + ; + +definestmt + : CREATE or_replace_? AGGREGATE func_name aggr_args definition + | CREATE or_replace_? AGGREGATE func_name old_aggr_definition + | CREATE OPERATOR any_operator definition + | CREATE TYPE_P any_name definition + | CREATE TYPE_P any_name + | CREATE TYPE_P any_name AS OPEN_PAREN opttablefuncelementlist? CLOSE_PAREN + | CREATE TYPE_P any_name AS ENUM_P OPEN_PAREN enum_val_list_? CLOSE_PAREN + | CREATE TYPE_P any_name AS RANGE definition + | CREATE TEXT_P SEARCH PARSER any_name definition + | CREATE TEXT_P SEARCH DICTIONARY any_name definition + | CREATE TEXT_P SEARCH TEMPLATE any_name definition + | CREATE TEXT_P SEARCH CONFIGURATION any_name definition + | CREATE COLLATION any_name definition + | CREATE COLLATION IF_P NOT EXISTS any_name definition + | CREATE COLLATION any_name FROM any_name + | CREATE COLLATION IF_P NOT EXISTS any_name FROM any_name + ; + +definition + : OPEN_PAREN def_list CLOSE_PAREN + ; + +def_list + : def_elem (COMMA def_elem)* + ; + +def_elem + : colLabel (EQUAL def_arg)? + ; + +def_arg + : func_type + | reserved_keyword + | qual_all_op + | numericonly + | sconst + | NONE + ; + +old_aggr_definition + : OPEN_PAREN old_aggr_list CLOSE_PAREN + ; + +old_aggr_list + : old_aggr_elem (COMMA old_aggr_elem)* + ; + +old_aggr_elem + : identifier EQUAL def_arg + ; + +enum_val_list_ + : enum_val_list + + ; + +enum_val_list + : sconst (COMMA sconst)* + ; + +alterenumstmt + : ALTER TYPE_P any_name ADD_P VALUE_P if_not_exists_? sconst + | ALTER TYPE_P any_name ADD_P VALUE_P if_not_exists_? sconst BEFORE sconst + | ALTER TYPE_P any_name ADD_P VALUE_P if_not_exists_? sconst AFTER sconst + | ALTER TYPE_P any_name RENAME VALUE_P sconst TO sconst + ; + +if_not_exists_ + : IF_P NOT EXISTS + + ; + +createopclassstmt + : CREATE OPERATOR CLASS any_name default_? FOR TYPE_P typename USING name opfamily_? AS opclass_item_list + ; + +opclass_item_list + : opclass_item (COMMA opclass_item)* + ; + +opclass_item + : OPERATOR iconst any_operator opclass_purpose? recheck_? + | OPERATOR iconst operator_with_argtypes opclass_purpose? recheck_? + | FUNCTION iconst function_with_argtypes + | FUNCTION iconst OPEN_PAREN type_list CLOSE_PAREN function_with_argtypes + | STORAGE typename + ; + +default_ + : DEFAULT + + ; + +opfamily_ + : FAMILY any_name + + ; + +opclass_purpose + : FOR SEARCH + | FOR ORDER BY any_name + + ; + +recheck_ + : RECHECK + + ; + +createopfamilystmt + : CREATE OPERATOR FAMILY any_name USING name + ; + +alteropfamilystmt + : ALTER OPERATOR FAMILY any_name USING name ADD_P opclass_item_list + | ALTER OPERATOR FAMILY any_name USING name DROP opclass_drop_list + ; + +opclass_drop_list + : opclass_drop (COMMA opclass_drop)* + ; + +opclass_drop + : OPERATOR iconst OPEN_PAREN type_list CLOSE_PAREN + | FUNCTION iconst OPEN_PAREN type_list CLOSE_PAREN + ; + +dropopclassstmt + : DROP OPERATOR CLASS any_name USING name drop_behavior_? + | DROP OPERATOR CLASS IF_P EXISTS any_name USING name drop_behavior_? + ; + +dropopfamilystmt + : DROP OPERATOR FAMILY any_name USING name drop_behavior_? + | DROP OPERATOR FAMILY IF_P EXISTS any_name USING name drop_behavior_? + ; + +dropownedstmt + : DROP OWNED BY role_list drop_behavior_? + ; + +reassignownedstmt + : REASSIGN OWNED BY role_list TO rolespec + ; + +dropstmt + : DROP object_type_any_name IF_P EXISTS any_name_list_ drop_behavior_? + | DROP object_type_any_name any_name_list_ drop_behavior_? + | DROP drop_type_name IF_P EXISTS name_list drop_behavior_? + | DROP drop_type_name name_list drop_behavior_? + | DROP object_type_name_on_any_name name ON any_name drop_behavior_? + | DROP object_type_name_on_any_name IF_P EXISTS name ON any_name drop_behavior_? + | DROP TYPE_P type_name_list drop_behavior_? + | DROP TYPE_P IF_P EXISTS type_name_list drop_behavior_? + | DROP DOMAIN_P type_name_list drop_behavior_? + | DROP DOMAIN_P IF_P EXISTS type_name_list drop_behavior_? + | DROP INDEX CONCURRENTLY any_name_list_ drop_behavior_? + | DROP INDEX CONCURRENTLY IF_P EXISTS any_name_list_ drop_behavior_? + ; + +object_type_any_name + : TABLE + | SEQUENCE + | VIEW + | MATERIALIZED VIEW + | INDEX + | FOREIGN TABLE + | COLLATION + | CONVERSION_P + | STATISTICS + | TEXT_P SEARCH PARSER + | TEXT_P SEARCH DICTIONARY + | TEXT_P SEARCH TEMPLATE + | TEXT_P SEARCH CONFIGURATION + ; + +object_type_name + : drop_type_name + | DATABASE + | ROLE + | SUBSCRIPTION + | TABLESPACE + ; + +drop_type_name + : ACCESS METHOD + | EVENT TRIGGER + | EXTENSION + | FOREIGN DATA_P WRAPPER + | procedural_? LANGUAGE + | PUBLICATION + | SCHEMA + | SERVER + ; + +object_type_name_on_any_name + : POLICY + | RULE + | TRIGGER + ; + +any_name_list_ + : any_name (COMMA any_name)* + ; + +any_name + : colid attrs? + ; + +attrs + : (DOT attr_name)+ + ; + +type_name_list + : typename (COMMA typename)* + ; + +truncatestmt + : TRUNCATE table_? relation_expr_list restart_seqs_? drop_behavior_? + ; + +restart_seqs_ + : CONTINUE_P IDENTITY_P + | RESTART IDENTITY_P + + ; + +commentstmt + : COMMENT ON object_type_any_name any_name IS comment_text + | COMMENT ON COLUMN any_name IS comment_text + | COMMENT ON object_type_name name IS comment_text + | COMMENT ON TYPE_P typename IS comment_text + | COMMENT ON DOMAIN_P typename IS comment_text + | COMMENT ON AGGREGATE aggregate_with_argtypes IS comment_text + | COMMENT ON FUNCTION function_with_argtypes IS comment_text + | COMMENT ON OPERATOR operator_with_argtypes IS comment_text + | COMMENT ON CONSTRAINT name ON any_name IS comment_text + | COMMENT ON CONSTRAINT name ON DOMAIN_P any_name IS comment_text + | COMMENT ON object_type_name_on_any_name name ON any_name IS comment_text + | COMMENT ON PROCEDURE function_with_argtypes IS comment_text + | COMMENT ON ROUTINE function_with_argtypes IS comment_text + | COMMENT ON TRANSFORM FOR typename LANGUAGE name IS comment_text + | COMMENT ON OPERATOR CLASS any_name USING name IS comment_text + | COMMENT ON OPERATOR FAMILY any_name USING name IS comment_text + | COMMENT ON LARGE_P OBJECT_P numericonly IS comment_text + | COMMENT ON CAST OPEN_PAREN typename AS typename CLOSE_PAREN IS comment_text + ; + +comment_text + : sconst + | NULL_P + ; + +seclabelstmt + : SECURITY LABEL provider_? ON object_type_any_name any_name IS security_label + | SECURITY LABEL provider_? ON COLUMN any_name IS security_label + | SECURITY LABEL provider_? ON object_type_name name IS security_label + | SECURITY LABEL provider_? ON TYPE_P typename IS security_label + | SECURITY LABEL provider_? ON DOMAIN_P typename IS security_label + | SECURITY LABEL provider_? ON AGGREGATE aggregate_with_argtypes IS security_label + | SECURITY LABEL provider_? ON FUNCTION function_with_argtypes IS security_label + | SECURITY LABEL provider_? ON LARGE_P OBJECT_P numericonly IS security_label + | SECURITY LABEL provider_? ON PROCEDURE function_with_argtypes IS security_label + | SECURITY LABEL provider_? ON ROUTINE function_with_argtypes IS security_label + ; + +provider_ + : FOR nonreservedword_or_sconst + + ; + +security_label + : sconst + | NULL_P + ; + +fetchstmt + : FETCH fetch_args + | MOVE fetch_args + ; + +fetch_args + : cursor_name + | from_in cursor_name + | NEXT from_in_? cursor_name + | PRIOR from_in_? cursor_name + | FIRST_P from_in_? cursor_name + | LAST_P from_in_? cursor_name + | ABSOLUTE_P signediconst from_in_? cursor_name + | RELATIVE_P signediconst from_in_? cursor_name + | signediconst from_in_? cursor_name + | ALL from_in_? cursor_name + | FORWARD from_in_? cursor_name + | FORWARD signediconst from_in_? cursor_name + | FORWARD ALL from_in_? cursor_name + | BACKWARD from_in_? cursor_name + | BACKWARD signediconst from_in_? cursor_name + | BACKWARD ALL from_in_? cursor_name + ; + +from_in + : FROM + | IN_P + ; + +from_in_ + : from_in + + ; + +grantstmt + : GRANT privileges ON privilege_target TO grantee_list grant_grant_option_? + ; + +revokestmt + : REVOKE privileges ON privilege_target FROM grantee_list drop_behavior_? + | REVOKE GRANT OPTION FOR privileges ON privilege_target FROM grantee_list drop_behavior_? + ; + +privileges + : privilege_list + | ALL + | ALL PRIVILEGES + | ALL OPEN_PAREN columnlist CLOSE_PAREN + | ALL PRIVILEGES OPEN_PAREN columnlist CLOSE_PAREN + ; + +privilege_list + : privilege (COMMA privilege)* + ; + +privilege + : SELECT column_list_? + | REFERENCES column_list_? + | CREATE column_list_? + | colid column_list_? + ; + +privilege_target + : qualified_name_list + | TABLE qualified_name_list + | SEQUENCE qualified_name_list + | FOREIGN DATA_P WRAPPER name_list + | FOREIGN SERVER name_list + | FUNCTION function_with_argtypes_list + | PROCEDURE function_with_argtypes_list + | ROUTINE function_with_argtypes_list + | DATABASE name_list + | DOMAIN_P any_name_list_ + | LANGUAGE name_list + | LARGE_P OBJECT_P numericonly_list + | SCHEMA name_list + | TABLESPACE name_list + | TYPE_P any_name_list_ + | ALL TABLES IN_P SCHEMA name_list + | ALL SEQUENCES IN_P SCHEMA name_list + | ALL FUNCTIONS IN_P SCHEMA name_list + | ALL PROCEDURES IN_P SCHEMA name_list + | ALL ROUTINES IN_P SCHEMA name_list + ; + +grantee_list + : grantee (COMMA grantee)* + ; + +grantee + : rolespec + | GROUP_P rolespec + ; + +grant_grant_option_ + : WITH GRANT OPTION + + ; + +grantrolestmt + : GRANT privilege_list TO role_list grant_admin_option_? granted_by_? + ; + +revokerolestmt + : REVOKE privilege_list FROM role_list granted_by_? drop_behavior_? + | REVOKE ADMIN OPTION FOR privilege_list FROM role_list granted_by_? drop_behavior_? + ; + +grant_admin_option_ + : WITH ADMIN OPTION + + ; + +granted_by_ + : GRANTED BY rolespec + + ; + +alterdefaultprivilegesstmt + : ALTER DEFAULT PRIVILEGES defacloptionlist defaclaction + ; + +defacloptionlist + : defacloption* + ; + +defacloption + : IN_P SCHEMA name_list + | FOR ROLE role_list + | FOR USER role_list + ; + +defaclaction + : GRANT privileges ON defacl_privilege_target TO grantee_list grant_grant_option_? + | REVOKE privileges ON defacl_privilege_target FROM grantee_list drop_behavior_? + | REVOKE GRANT OPTION FOR privileges ON defacl_privilege_target FROM grantee_list drop_behavior_? + ; + +defacl_privilege_target + : TABLES + | FUNCTIONS + | ROUTINES + | SEQUENCES + | TYPES_P + | SCHEMAS + ; + +//create index + +indexstmt + : CREATE unique_? INDEX concurrently_? (if_not_exists_? index_name_)? + ON relation_expr access_method_clause? OPEN_PAREN index_params CLOSE_PAREN include_? nulls_distinct? + reloptions_? opttablespace? where_clause? + | CREATE unique_? INDEX concurrently_? if_not_exists_? name ON relation_expr access_method_clause? OPEN_PAREN index_params CLOSE_PAREN + include_? nulls_distinct? reloptions_? opttablespace? where_clause? + ; + +unique_ + : UNIQUE + ; + +nulls_distinct + : NULLS_P NOT? DISTINCT + ; + +single_name_ + : colid + ; + +concurrently_ + : CONCURRENTLY + + ; + +index_name_ + : name + + ; + +access_method_clause + : USING name + + ; + +index_params + : index_elem (COMMA index_elem)* + ; + +index_elem_options + : collate_? class_? asc_desc_? nulls_order_? + | collate_? any_name reloptions asc_desc_? nulls_order_? + ; + +index_elem + : colid index_elem_options + | func_expr_windowless index_elem_options + | OPEN_PAREN a_expr CLOSE_PAREN index_elem_options + ; + +include_ + : INCLUDE OPEN_PAREN index_including_params CLOSE_PAREN + + ; + +index_including_params + : index_elem (COMMA index_elem)* + ; + +collate_ + : COLLATE any_name + + ; + +class_ + : any_name + + ; + +asc_desc_ + : ASC + | DESC + + ; + +//TOD NULLS_LA was used + +nulls_order_ + : NULLS_P FIRST_P + | NULLS_P LAST_P + + ; + +createfunctionstmt + : CREATE or_replace_? (FUNCTION | PROCEDURE) func_name func_args_with_defaults ( + RETURNS (func_return | TABLE OPEN_PAREN table_func_column_list CLOSE_PAREN) + )? createfunc_opt_list + ; + +or_replace_ + : OR REPLACE + + ; + +func_args + : OPEN_PAREN func_args_list? CLOSE_PAREN + ; + +func_args_list + : func_arg (COMMA func_arg)* + ; + +function_with_argtypes_list + : function_with_argtypes (COMMA function_with_argtypes)* + ; + +function_with_argtypes + : func_name func_args + | type_func_name_keyword + | colid indirection? + ; + +func_args_with_defaults + : OPEN_PAREN func_args_with_defaults_list? CLOSE_PAREN + ; + +func_args_with_defaults_list + : func_arg_with_default (COMMA func_arg_with_default)* + ; + +func_arg + : arg_class param_name? func_type + | param_name arg_class? func_type + | func_type + ; + +arg_class + : IN_P OUT_P? + | OUT_P + | INOUT + | VARIADIC + ; + +param_name + : type_function_name + ; + +func_return + : func_type + ; + +func_type + : typename + | SETOF? type_function_name attrs PERCENT TYPE_P + ; + +func_arg_with_default + : func_arg ((DEFAULT | EQUAL) a_expr)? + ; + +aggr_arg + : func_arg + ; + +aggr_args + : OPEN_PAREN ( + STAR + | aggr_args_list + | ORDER BY aggr_args_list + | aggr_args_list ORDER BY aggr_args_list + ) CLOSE_PAREN + ; + +aggr_args_list + : aggr_arg (COMMA aggr_arg)* + ; + +aggregate_with_argtypes + : func_name aggr_args + ; + +aggregate_with_argtypes_list + : aggregate_with_argtypes (COMMA aggregate_with_argtypes)* + ; + +createfunc_opt_list + : createfunc_opt_item+ {this.ParseRoutineBody();} + // | createfunc_opt_list createfunc_opt_item + ; + +common_func_opt_item + : CALLED ON NULL_P INPUT_P + | RETURNS NULL_P ON NULL_P INPUT_P + | STRICT_P + | IMMUTABLE + | STABLE + | VOLATILE + | EXTERNAL SECURITY DEFINER + | EXTERNAL SECURITY INVOKER + | SECURITY DEFINER + | SECURITY INVOKER + | LEAKPROOF + | NOT LEAKPROOF + | COST numericonly + | ROWS numericonly + | SUPPORT any_name + | functionsetresetclause + | PARALLEL colid + ; + +createfunc_opt_item + : AS func_as + | LANGUAGE nonreservedword_or_sconst + | TRANSFORM transform_type_list + | WINDOW + | common_func_opt_item + ; + +//https://www.postgresql.org/docs/9.1/sql-createfunction.html + +// | AS 'definition' + +// | AS 'obj_file', 'link_symbol' + +func_as + : + /* |AS 'definition'*/ def = sconst + /*| AS 'obj_file', 'link_symbol'*/ + | sconst COMMA sconst + ; + +transform_type_list + : FOR TYPE_P typename (COMMA FOR TYPE_P typename)* + ; + +definition_ + : WITH definition + + ; + +table_func_column + : param_name func_type + ; + +table_func_column_list + : table_func_column (COMMA table_func_column)* + ; + +alterfunctionstmt + : ALTER (FUNCTION | PROCEDURE | ROUTINE) function_with_argtypes alterfunc_opt_list restrict_? + ; + +alterfunc_opt_list + : common_func_opt_item+ + ; + +restrict_ + : RESTRICT + + ; + +removefuncstmt + : DROP FUNCTION function_with_argtypes_list drop_behavior_? + | DROP FUNCTION IF_P EXISTS function_with_argtypes_list drop_behavior_? + | DROP PROCEDURE function_with_argtypes_list drop_behavior_? + | DROP PROCEDURE IF_P EXISTS function_with_argtypes_list drop_behavior_? + | DROP ROUTINE function_with_argtypes_list drop_behavior_? + | DROP ROUTINE IF_P EXISTS function_with_argtypes_list drop_behavior_? + ; + +removeaggrstmt + : DROP AGGREGATE aggregate_with_argtypes_list drop_behavior_? + | DROP AGGREGATE IF_P EXISTS aggregate_with_argtypes_list drop_behavior_? + ; + +removeoperstmt + : DROP OPERATOR operator_with_argtypes_list drop_behavior_? + | DROP OPERATOR IF_P EXISTS operator_with_argtypes_list drop_behavior_? + ; + +oper_argtypes + : OPEN_PAREN typename CLOSE_PAREN + | OPEN_PAREN typename COMMA typename CLOSE_PAREN + | OPEN_PAREN NONE COMMA typename CLOSE_PAREN + | OPEN_PAREN typename COMMA NONE CLOSE_PAREN + ; + +any_operator + : (colid DOT)* all_op + ; + +operator_with_argtypes_list + : operator_with_argtypes (COMMA operator_with_argtypes)* + ; + +operator_with_argtypes + : any_operator oper_argtypes + ; + +dostmt + : DO dostmt_opt_list + ; + +dostmt_opt_list + : dostmt_opt_item+ + ; + +dostmt_opt_item + : sconst + | LANGUAGE nonreservedword_or_sconst + ; + +createcaststmt + : CREATE CAST OPEN_PAREN typename AS typename CLOSE_PAREN WITH FUNCTION function_with_argtypes cast_context? + | CREATE CAST OPEN_PAREN typename AS typename CLOSE_PAREN WITHOUT FUNCTION cast_context? + | CREATE CAST OPEN_PAREN typename AS typename CLOSE_PAREN WITH INOUT cast_context? + ; + +cast_context + : AS IMPLICIT_P + | AS ASSIGNMENT + + ; + +dropcaststmt + : DROP CAST if_exists_? OPEN_PAREN typename AS typename CLOSE_PAREN drop_behavior_? + ; + +if_exists_ + : IF_P EXISTS + + ; + +createtransformstmt + : CREATE or_replace_? TRANSFORM FOR typename LANGUAGE name OPEN_PAREN transform_element_list CLOSE_PAREN + ; + +transform_element_list + : FROM SQL_P WITH FUNCTION function_with_argtypes COMMA TO SQL_P WITH FUNCTION function_with_argtypes + | TO SQL_P WITH FUNCTION function_with_argtypes COMMA FROM SQL_P WITH FUNCTION function_with_argtypes + | FROM SQL_P WITH FUNCTION function_with_argtypes + | TO SQL_P WITH FUNCTION function_with_argtypes + ; + +droptransformstmt + : DROP TRANSFORM if_exists_? FOR typename LANGUAGE name drop_behavior_? + ; + +reindexstmt + : REINDEX reindex_option_list? reindex_target_relation concurrently_? qualified_name + | REINDEX reindex_option_list? SCHEMA concurrently_? name + | REINDEX reindex_option_list? reindex_target_all concurrently_? single_name_? + ; + +reindex_target_relation + : INDEX + | TABLE + ; + +reindex_target_all + : SYSTEM_P + | DATABASE + ; + +reindex_option_list + : OPEN_PAREN utility_option_list CLOSE_PAREN + ; + +altertblspcstmt + : ALTER TABLESPACE name SET reloptions + | ALTER TABLESPACE name RESET reloptions + ; + +renamestmt + : ALTER AGGREGATE aggregate_with_argtypes RENAME TO name + | ALTER COLLATION any_name RENAME TO name + | ALTER CONVERSION_P any_name RENAME TO name + | ALTER DATABASE name RENAME TO name + | ALTER DOMAIN_P any_name RENAME TO name + | ALTER DOMAIN_P any_name RENAME CONSTRAINT name TO name + | ALTER FOREIGN DATA_P WRAPPER name RENAME TO name + | ALTER FUNCTION function_with_argtypes RENAME TO name + | ALTER GROUP_P roleid RENAME TO roleid + | ALTER procedural_? LANGUAGE name RENAME TO name + | ALTER OPERATOR CLASS any_name USING name RENAME TO name + | ALTER OPERATOR FAMILY any_name USING name RENAME TO name + | ALTER POLICY name ON qualified_name RENAME TO name + | ALTER POLICY IF_P EXISTS name ON qualified_name RENAME TO name + | ALTER PROCEDURE function_with_argtypes RENAME TO name + | ALTER PUBLICATION name RENAME TO name + | ALTER ROUTINE function_with_argtypes RENAME TO name + | ALTER SCHEMA name RENAME TO name + | ALTER SERVER name RENAME TO name + | ALTER SUBSCRIPTION name RENAME TO name + | ALTER TABLE relation_expr RENAME TO name + | ALTER TABLE IF_P EXISTS relation_expr RENAME TO name + | ALTER SEQUENCE qualified_name RENAME TO name + | ALTER SEQUENCE IF_P EXISTS qualified_name RENAME TO name + | ALTER VIEW qualified_name RENAME TO name + | ALTER VIEW IF_P EXISTS qualified_name RENAME TO name + | ALTER MATERIALIZED VIEW qualified_name RENAME TO name + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name RENAME TO name + | ALTER INDEX qualified_name RENAME TO name + | ALTER INDEX IF_P EXISTS qualified_name RENAME TO name + | ALTER FOREIGN TABLE relation_expr RENAME TO name + | ALTER FOREIGN TABLE IF_P EXISTS relation_expr RENAME TO name + | ALTER TABLE relation_expr RENAME column_? name TO name + | ALTER TABLE IF_P EXISTS relation_expr RENAME column_? name TO name + | ALTER VIEW qualified_name RENAME column_? name TO name + | ALTER VIEW IF_P EXISTS qualified_name RENAME column_? name TO name + | ALTER MATERIALIZED VIEW qualified_name RENAME column_? name TO name + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name RENAME column_? name TO name + | ALTER TABLE relation_expr RENAME CONSTRAINT name TO name + | ALTER TABLE IF_P EXISTS relation_expr RENAME CONSTRAINT name TO name + | ALTER FOREIGN TABLE relation_expr RENAME column_? name TO name + | ALTER FOREIGN TABLE IF_P EXISTS relation_expr RENAME column_? name TO name + | ALTER RULE name ON qualified_name RENAME TO name + | ALTER TRIGGER name ON qualified_name RENAME TO name + | ALTER EVENT TRIGGER name RENAME TO name + | ALTER ROLE roleid RENAME TO roleid + | ALTER USER roleid RENAME TO roleid + | ALTER TABLESPACE name RENAME TO name + | ALTER STATISTICS any_name RENAME TO name + | ALTER TEXT_P SEARCH PARSER any_name RENAME TO name + | ALTER TEXT_P SEARCH DICTIONARY any_name RENAME TO name + | ALTER TEXT_P SEARCH TEMPLATE any_name RENAME TO name + | ALTER TEXT_P SEARCH CONFIGURATION any_name RENAME TO name + | ALTER TYPE_P any_name RENAME TO name + | ALTER TYPE_P any_name RENAME ATTRIBUTE name TO name drop_behavior_? + ; + +column_ + : COLUMN + + ; + +set_data_ + : SET DATA_P + + ; + +alterobjectdependsstmt + : ALTER FUNCTION function_with_argtypes no_? DEPENDS ON EXTENSION name + | ALTER PROCEDURE function_with_argtypes no_? DEPENDS ON EXTENSION name + | ALTER ROUTINE function_with_argtypes no_? DEPENDS ON EXTENSION name + | ALTER TRIGGER name ON qualified_name no_? DEPENDS ON EXTENSION name + | ALTER MATERIALIZED VIEW qualified_name no_? DEPENDS ON EXTENSION name + | ALTER INDEX qualified_name no_? DEPENDS ON EXTENSION name + ; + +no_ + : NO + + ; + +alterobjectschemastmt + : ALTER AGGREGATE aggregate_with_argtypes SET SCHEMA name + | ALTER COLLATION any_name SET SCHEMA name + | ALTER CONVERSION_P any_name SET SCHEMA name + | ALTER DOMAIN_P any_name SET SCHEMA name + | ALTER EXTENSION name SET SCHEMA name + | ALTER FUNCTION function_with_argtypes SET SCHEMA name + | ALTER OPERATOR operator_with_argtypes SET SCHEMA name + | ALTER OPERATOR CLASS any_name USING name SET SCHEMA name + | ALTER OPERATOR FAMILY any_name USING name SET SCHEMA name + | ALTER PROCEDURE function_with_argtypes SET SCHEMA name + | ALTER ROUTINE function_with_argtypes SET SCHEMA name + | ALTER TABLE relation_expr SET SCHEMA name + | ALTER TABLE IF_P EXISTS relation_expr SET SCHEMA name + | ALTER STATISTICS any_name SET SCHEMA name + | ALTER TEXT_P SEARCH PARSER any_name SET SCHEMA name + | ALTER TEXT_P SEARCH DICTIONARY any_name SET SCHEMA name + | ALTER TEXT_P SEARCH TEMPLATE any_name SET SCHEMA name + | ALTER TEXT_P SEARCH CONFIGURATION any_name SET SCHEMA name + | ALTER SEQUENCE qualified_name SET SCHEMA name + | ALTER SEQUENCE IF_P EXISTS qualified_name SET SCHEMA name + | ALTER VIEW qualified_name SET SCHEMA name + | ALTER VIEW IF_P EXISTS qualified_name SET SCHEMA name + | ALTER MATERIALIZED VIEW qualified_name SET SCHEMA name + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name SET SCHEMA name + | ALTER FOREIGN TABLE relation_expr SET SCHEMA name + | ALTER FOREIGN TABLE IF_P EXISTS relation_expr SET SCHEMA name + | ALTER TYPE_P any_name SET SCHEMA name + ; + +alteroperatorstmt + : ALTER OPERATOR operator_with_argtypes SET OPEN_PAREN operator_def_list CLOSE_PAREN + ; + +operator_def_list + : operator_def_elem (COMMA operator_def_elem)* + ; + +operator_def_elem + : colLabel EQUAL NONE + | colLabel EQUAL operator_def_arg + ; + +operator_def_arg + : func_type + | reserved_keyword + | qual_all_op + | numericonly + | sconst + ; + +altertypestmt + : ALTER TYPE_P any_name SET OPEN_PAREN operator_def_list CLOSE_PAREN + ; + +alterownerstmt + : ALTER AGGREGATE aggregate_with_argtypes OWNER TO rolespec + | ALTER COLLATION any_name OWNER TO rolespec + | ALTER CONVERSION_P any_name OWNER TO rolespec + | ALTER DATABASE name OWNER TO rolespec + | ALTER DOMAIN_P any_name OWNER TO rolespec + | ALTER FUNCTION function_with_argtypes OWNER TO rolespec + | ALTER procedural_? LANGUAGE name OWNER TO rolespec + | ALTER LARGE_P OBJECT_P numericonly OWNER TO rolespec + | ALTER OPERATOR operator_with_argtypes OWNER TO rolespec + | ALTER OPERATOR CLASS any_name USING name OWNER TO rolespec + | ALTER OPERATOR FAMILY any_name USING name OWNER TO rolespec + | ALTER PROCEDURE function_with_argtypes OWNER TO rolespec + | ALTER ROUTINE function_with_argtypes OWNER TO rolespec + | ALTER SCHEMA name OWNER TO rolespec + | ALTER TYPE_P any_name OWNER TO rolespec + | ALTER TABLESPACE name OWNER TO rolespec + | ALTER STATISTICS any_name OWNER TO rolespec + | ALTER TEXT_P SEARCH DICTIONARY any_name OWNER TO rolespec + | ALTER TEXT_P SEARCH CONFIGURATION any_name OWNER TO rolespec + | ALTER FOREIGN DATA_P WRAPPER name OWNER TO rolespec + | ALTER SERVER name OWNER TO rolespec + | ALTER EVENT TRIGGER name OWNER TO rolespec + | ALTER PUBLICATION name OWNER TO rolespec + | ALTER SUBSCRIPTION name OWNER TO rolespec + ; + +createpublicationstmt + : CREATE PUBLICATION name publication_for_tables_? definition_? + ; + +publication_for_tables_ + : publication_for_tables + + ; + +publication_for_tables + : FOR TABLE relation_expr_list + | FOR ALL TABLES + ; + +alterpublicationstmt + : ALTER PUBLICATION name SET definition + | ALTER PUBLICATION name ADD_P TABLE relation_expr_list + | ALTER PUBLICATION name SET TABLE relation_expr_list + | ALTER PUBLICATION name DROP TABLE relation_expr_list + ; + +createsubscriptionstmt + : CREATE SUBSCRIPTION name CONNECTION sconst PUBLICATION publication_name_list definition_? + ; + +publication_name_list + : publication_name_item (COMMA publication_name_item)* + ; + +publication_name_item + : colLabel + ; + +altersubscriptionstmt + : ALTER SUBSCRIPTION name SET definition + | ALTER SUBSCRIPTION name CONNECTION sconst + | ALTER SUBSCRIPTION name REFRESH PUBLICATION definition_? + | ALTER SUBSCRIPTION name SET PUBLICATION publication_name_list definition_? + | ALTER SUBSCRIPTION name ENABLE_P + | ALTER SUBSCRIPTION name DISABLE_P + ; + +dropsubscriptionstmt + : DROP SUBSCRIPTION name drop_behavior_? + | DROP SUBSCRIPTION IF_P EXISTS name drop_behavior_? + ; + +rulestmt + : CREATE or_replace_? RULE name AS ON event TO qualified_name where_clause? DO instead_? ruleactionlist + ; + +ruleactionlist + : NOTHING + | ruleactionstmt + | OPEN_PAREN ruleactionmulti CLOSE_PAREN + ; + +ruleactionmulti + : ruleactionstmtOrEmpty? (SEMI ruleactionstmtOrEmpty?)* + ; + +ruleactionstmt + : selectstmt + | insertstmt + | updatestmt + | deletestmt + | notifystmt + ; + +ruleactionstmtOrEmpty + : ruleactionstmt + + ; + +event + : SELECT + | UPDATE + | DELETE_P + | INSERT + ; + +instead_ + : INSTEAD + | ALSO + + ; + +notifystmt + : NOTIFY colid notify_payload? + ; + +notify_payload + : COMMA sconst + + ; + +listenstmt + : LISTEN colid + ; + +unlistenstmt + : UNLISTEN colid + | UNLISTEN STAR + ; + +transactionstmt + : ABORT_P transaction_? transaction_chain_? + | BEGIN_P transaction_? transaction_mode_list_or_empty? + | START TRANSACTION transaction_mode_list_or_empty? + | COMMIT transaction_? transaction_chain_? + | END_P transaction_? transaction_chain_? + | ROLLBACK transaction_? transaction_chain_? + | SAVEPOINT colid + | RELEASE SAVEPOINT colid + | RELEASE colid + | ROLLBACK transaction_? TO SAVEPOINT colid + | ROLLBACK transaction_? TO colid + | PREPARE TRANSACTION sconst + | COMMIT PREPARED sconst + | ROLLBACK PREPARED sconst + ; + +transaction_ + : WORK + | TRANSACTION + + ; + +transaction_mode_item + : ISOLATION LEVEL iso_level + | READ ONLY + | READ WRITE + | DEFERRABLE + | NOT DEFERRABLE + ; + +transaction_mode_list + : transaction_mode_item (COMMA? transaction_mode_item)* + ; + +transaction_mode_list_or_empty + : transaction_mode_list + + ; + +transaction_chain_ + : AND NO? CHAIN + + ; + +viewstmt + : CREATE (OR REPLACE)? opttemp? ( + VIEW qualified_name column_list_? reloptions_? + | RECURSIVE VIEW qualified_name OPEN_PAREN columnlist CLOSE_PAREN reloptions_? + ) AS selectstmt check_option_? + ; + +check_option_ + : WITH (CASCADED | LOCAL)? CHECK OPTION + + ; + +loadstmt + : LOAD file_name + ; + +createdbstmt + : CREATE DATABASE name with_? createdb_opt_list? + ; + +createdb_opt_list + : createdb_opt_items + + ; + +createdb_opt_items + : createdb_opt_item+ + ; + +createdb_opt_item + : createdb_opt_name equal_? (signediconst | boolean_or_string_ | DEFAULT) + ; + +createdb_opt_name + : identifier + | CONNECTION LIMIT + | ENCODING + | LOCATION + | OWNER + | TABLESPACE + | TEMPLATE + ; + +equal_ + : EQUAL + + ; + +alterdatabasestmt + : ALTER DATABASE name (WITH createdb_opt_list? | createdb_opt_list? | SET TABLESPACE name) + ; + +alterdatabasesetstmt + : ALTER DATABASE name setresetclause + ; + +dropdbstmt + : DROP DATABASE (IF_P EXISTS)? name (with_? OPEN_PAREN drop_option_list CLOSE_PAREN)? + ; + +drop_option_list + : drop_option (COMMA drop_option)* + ; + +drop_option + : FORCE + ; + +altercollationstmt + : ALTER COLLATION any_name REFRESH VERSION_P + ; + +altersystemstmt + : ALTER SYSTEM_P (SET | RESET) generic_set + ; + +createdomainstmt + : CREATE DOMAIN_P any_name as_? typename colquallist + ; + +alterdomainstmt + : ALTER DOMAIN_P any_name ( + alter_column_default + | DROP NOT NULL_P + | SET NOT NULL_P + | ADD_P tableconstraint + | DROP CONSTRAINT (IF_P EXISTS)? name drop_behavior_? + | VALIDATE CONSTRAINT name + ) + ; + +as_ + : AS + + ; + +altertsdictionarystmt + : ALTER TEXT_P SEARCH DICTIONARY any_name definition + ; + +altertsconfigurationstmt + : ALTER TEXT_P SEARCH CONFIGURATION any_name ADD_P MAPPING FOR name_list any_with any_name_list_ + | ALTER TEXT_P SEARCH CONFIGURATION any_name ALTER MAPPING FOR name_list any_with any_name_list_ + | ALTER TEXT_P SEARCH CONFIGURATION any_name ALTER MAPPING REPLACE any_name any_with any_name + | ALTER TEXT_P SEARCH CONFIGURATION any_name ALTER MAPPING FOR name_list REPLACE any_name any_with any_name + | ALTER TEXT_P SEARCH CONFIGURATION any_name DROP MAPPING FOR name_list + | ALTER TEXT_P SEARCH CONFIGURATION any_name DROP MAPPING IF_P EXISTS FOR name_list + ; + +any_with + : WITH + //TODO + + // | WITH_LA + ; + +createconversionstmt + : CREATE default_? CONVERSION_P any_name FOR sconst TO sconst FROM any_name + ; + +clusterstmt + : CLUSTER verbose_? qualified_name cluster_index_specification? + | CLUSTER verbose_? + | CLUSTER verbose_? name ON qualified_name + ; + +cluster_index_specification + : USING name + + ; + +vacuumstmt + : VACUUM full_? freeze_? verbose_? analyze_? vacuum_relation_list_? + | VACUUM OPEN_PAREN vac_analyze_option_list CLOSE_PAREN vacuum_relation_list_? + ; + +analyzestmt + : analyze_keyword verbose_? vacuum_relation_list_? + | analyze_keyword OPEN_PAREN vac_analyze_option_list CLOSE_PAREN vacuum_relation_list_? + ; + +utility_option_list + : utility_option_elem ( ',' utility_option_elem)* + ; + +vac_analyze_option_list + : vac_analyze_option_elem (COMMA vac_analyze_option_elem)* + ; + +analyze_keyword + : ANALYZE + | ANALYSE + ; + +utility_option_elem + : utility_option_name utility_option_arg? + ; + +utility_option_name + : nonreservedword + | analyze_keyword + | FORMAT_LA + ; + +utility_option_arg + : boolean_or_string_ + | numericonly + ; + +vac_analyze_option_elem + : vac_analyze_option_name vac_analyze_option_arg? + ; + +vac_analyze_option_name + : nonreservedword + | analyze_keyword + ; + +vac_analyze_option_arg + : boolean_or_string_ + | numericonly + + ; + +analyze_ + : analyze_keyword + + ; + +verbose_ + : VERBOSE + + ; + +full_ + : FULL + + ; + +freeze_ + : FREEZE + + ; + +name_list_ + : OPEN_PAREN name_list CLOSE_PAREN + + ; + +vacuum_relation + : qualified_name name_list_? + ; + +vacuum_relation_list + : vacuum_relation (COMMA vacuum_relation)* + ; + +vacuum_relation_list_ + : vacuum_relation_list + + ; + +explainstmt + : EXPLAIN explainablestmt + | EXPLAIN analyze_keyword verbose_? explainablestmt + | EXPLAIN VERBOSE explainablestmt + | EXPLAIN OPEN_PAREN explain_option_list CLOSE_PAREN explainablestmt + ; + +explainablestmt + : selectstmt + | insertstmt + | updatestmt + | deletestmt + | declarecursorstmt + | createasstmt + | creatematviewstmt + | refreshmatviewstmt + | executestmt + ; + +explain_option_list + : explain_option_elem (COMMA explain_option_elem)* + ; + +explain_option_elem + : explain_option_name explain_option_arg? + ; + +explain_option_name + : nonreservedword + | analyze_keyword + ; + +explain_option_arg + : boolean_or_string_ + | numericonly + + ; + +preparestmt + : PREPARE name prep_type_clause? AS preparablestmt + ; + +prep_type_clause + : OPEN_PAREN type_list CLOSE_PAREN + + ; + +preparablestmt + : selectstmt + | insertstmt + | updatestmt + | deletestmt + ; + +executestmt + : EXECUTE name execute_param_clause? + | CREATE opttemp? TABLE create_as_target AS EXECUTE name execute_param_clause? with_data_? + | CREATE opttemp? TABLE IF_P NOT EXISTS create_as_target AS EXECUTE name execute_param_clause? with_data_? + ; + +execute_param_clause + : OPEN_PAREN expr_list CLOSE_PAREN + + ; + +deallocatestmt + : DEALLOCATE name + | DEALLOCATE PREPARE name + | DEALLOCATE ALL + | DEALLOCATE PREPARE ALL + ; + +insertstmt + : with_clause_? INSERT INTO insert_target insert_rest on_conflict_? returning_clause? + ; + +insert_target + : qualified_name (AS colid)? + ; + +insert_rest + : selectstmt + | OVERRIDING override_kind VALUE_P selectstmt + | OPEN_PAREN insert_column_list CLOSE_PAREN (OVERRIDING override_kind VALUE_P)? selectstmt + | DEFAULT VALUES + ; + +override_kind + : USER + | SYSTEM_P + ; + +insert_column_list + : insert_column_item (COMMA insert_column_item)* + ; + +insert_column_item + : colid opt_indirection + ; + +on_conflict_ + : ON CONFLICT conf_expr_? DO (UPDATE SET set_clause_list where_clause? | NOTHING) + + ; + +conf_expr_ + : OPEN_PAREN index_params CLOSE_PAREN where_clause? + | ON CONSTRAINT name + + ; + +returning_clause + : RETURNING target_list + + ; + +// https://www.postgresql.org/docs/current/sql-merge.html +mergestmt + : MERGE INTO? qualified_name alias_clause? USING (select_with_parens | qualified_name) alias_clause? ON a_expr ( + merge_insert_clause merge_update_clause? + | merge_update_clause merge_insert_clause? + ) merge_delete_clause? + ; + +merge_insert_clause + : WHEN NOT MATCHED (AND a_expr)? THEN? INSERT (OPEN_PAREN insert_column_list CLOSE_PAREN)? values_clause + ; + +merge_update_clause + : WHEN MATCHED (AND a_expr)? THEN? UPDATE SET set_clause_list + ; + +merge_delete_clause + : WHEN MATCHED THEN? DELETE_P + ; + +deletestmt + : with_clause_? DELETE_P FROM relation_expr_opt_alias using_clause? where_or_current_clause? returning_clause? + ; + +using_clause + : USING from_list + + ; + +lockstmt + : LOCK_P table_? relation_expr_list lock_? nowait_? + ; + +lock_ + : IN_P lock_type MODE + + ; + +lock_type + : ACCESS (SHARE | EXCLUSIVE) + | ROW (SHARE | EXCLUSIVE) + | SHARE (UPDATE EXCLUSIVE | ROW EXCLUSIVE)? + | EXCLUSIVE + ; + +nowait_ + : NOWAIT + + ; + +nowait_or_skip_ + : NOWAIT + | SKIP_P LOCKED + + ; + +updatestmt + : with_clause_? UPDATE relation_expr_opt_alias SET set_clause_list from_clause? where_or_current_clause? returning_clause? + ; + +set_clause_list + : set_clause (COMMA set_clause)* + ; + +set_clause + : set_target EQUAL a_expr + | OPEN_PAREN set_target_list CLOSE_PAREN EQUAL a_expr + ; + +set_target + : colid opt_indirection + ; + +set_target_list + : set_target (COMMA set_target)* + ; + +declarecursorstmt + : DECLARE cursor_name cursor_options CURSOR hold_? FOR selectstmt + ; + +cursor_name + : name + ; + +cursor_options + : (NO SCROLL | SCROLL | BINARY | INSENSITIVE)* + ; + +hold_ + : + WITH HOLD + | WITHOUT HOLD + ; + +/* +TODO: why select_with_parens alternative is needed at all? +i guess it because original byson grammar can choose selectstmt(2)->select_with_parens on only OPEN_PARENT/SELECT kewords at the begining of statement; +(select * from tab); +parse can go through selectstmt( )->select_no_parens(1)->select_clause(2)->select_with_parens(1)->select_no_parens(1)->select_clause(1)->simple_select +instead of selectstmt(1)->select_no_parens(1)->select_clause(2)->select_with_parens(1)->select_no_parens(1)->select_clause(1)->simple_select +all standard tests passed on both variants +*/ + +selectstmt + : select_no_parens + | select_with_parens + ; + +select_with_parens + : OPEN_PAREN select_no_parens CLOSE_PAREN + | OPEN_PAREN select_with_parens CLOSE_PAREN + ; + +select_no_parens + : select_clause sort_clause_? ( + for_locking_clause select_limit_? + | select_limit for_locking_clause_? + )? + | with_clause select_clause sort_clause_? ( + for_locking_clause select_limit_? + | select_limit for_locking_clause_? + )? + ; + +select_clause + : simple_select_intersect ((UNION | EXCEPT) all_or_distinct? simple_select_intersect)* + ; + +simple_select_intersect + : simple_select_pramary (INTERSECT all_or_distinct? simple_select_pramary)* + ; + +simple_select_pramary + : ( + SELECT + ( all_clause_? target_list_? + into_clause? from_clause? where_clause? + group_clause? having_clause? window_clause? + | distinct_clause target_list + into_clause? from_clause? where_clause? + group_clause? having_clause? window_clause? + ) + ) + | values_clause + | TABLE relation_expr + | select_with_parens + ; + +with_clause + : WITH RECURSIVE? cte_list + ; + +cte_list + : common_table_expr (COMMA common_table_expr)* + ; + +common_table_expr + : name name_list_? AS materialized_? OPEN_PAREN preparablestmt CLOSE_PAREN + ; + +materialized_ + : MATERIALIZED + | NOT MATERIALIZED + + ; + +with_clause_ + : with_clause + + ; + +into_clause + : INTO opttempTableName + ; + +strict_ + : + STRICT_P + ; + +opttempTableName + : (LOCAL | GLOBAL)? (TEMPORARY | TEMP) table_? qualified_name + | UNLOGGED table_? qualified_name + | TABLE qualified_name + | qualified_name + ; + +table_ + : TABLE + + ; + +all_or_distinct + : ALL + | DISTINCT + + ; + +distinct_clause + : DISTINCT (ON OPEN_PAREN expr_list CLOSE_PAREN)? + ; + +all_clause_ + : ALL + + ; + +sort_clause_ + : sort_clause + + ; + +sort_clause + : ORDER BY sortby_list + ; + +sortby_list + : sortby (COMMA sortby)* + ; + +sortby + : a_expr (USING qual_all_op | asc_desc_?) nulls_order_? + ; + +select_limit + : limit_clause offset_clause? + | offset_clause limit_clause? + ; + +select_limit_ + : select_limit + + ; + +limit_clause + : LIMIT select_limit_value (COMMA select_offset_value)? + | FETCH first_or_next ( + select_fetch_first_value row_or_rows (ONLY | WITH TIES) + | row_or_rows (ONLY | WITH TIES) + ) + ; + +offset_clause + : OFFSET (select_offset_value | select_fetch_first_value row_or_rows) + ; + +select_limit_value + : a_expr + | ALL + ; + +select_offset_value + : a_expr + ; + +select_fetch_first_value + : c_expr + | PLUS i_or_f_const + | MINUS i_or_f_const + ; + +i_or_f_const + : iconst + | fconst + ; + +row_or_rows + : ROW + | ROWS + ; + +first_or_next + : FIRST_P + | NEXT + ; + +group_clause + : GROUP_P BY group_by_list + + ; + +group_by_list + : group_by_item (COMMA group_by_item)* + ; + +group_by_item + : empty_grouping_set + | cube_clause + | rollup_clause + | grouping_sets_clause + | a_expr + ; + +empty_grouping_set + : OPEN_PAREN CLOSE_PAREN + ; + +rollup_clause + : ROLLUP OPEN_PAREN expr_list CLOSE_PAREN + ; + +cube_clause + : CUBE OPEN_PAREN expr_list CLOSE_PAREN + ; + +grouping_sets_clause + : GROUPING SETS OPEN_PAREN group_by_list CLOSE_PAREN + ; + +having_clause + : HAVING a_expr + + ; + +for_locking_clause + : for_locking_items + | FOR READ ONLY + ; + +for_locking_clause_ + : for_locking_clause + + ; + +for_locking_items + : for_locking_item+ + ; + +for_locking_item + : for_locking_strength locked_rels_list? nowait_or_skip_? + ; + +for_locking_strength + : FOR ((NO KEY)? UPDATE | KEY? SHARE) + ; + +locked_rels_list + : OF qualified_name_list + + ; + +values_clause + : VALUES OPEN_PAREN expr_list CLOSE_PAREN (COMMA OPEN_PAREN expr_list CLOSE_PAREN)* + ; + +from_clause + : FROM from_list + + ; + +from_list + : table_ref (COMMA table_ref)* + ; + +table_ref + : ( + relation_expr alias_clause? tablesample_clause? + | func_table func_alias_clause? + | xmltable alias_clause? + | select_with_parens alias_clause? + | LATERAL_P ( + xmltable alias_clause? + | func_table func_alias_clause? + | select_with_parens alias_clause? + ) + | OPEN_PAREN table_ref ( + CROSS JOIN table_ref + | NATURAL join_type? JOIN table_ref + | join_type? JOIN table_ref join_qual + )? CLOSE_PAREN alias_clause? + ) ( + CROSS JOIN table_ref + | NATURAL join_type? JOIN table_ref + | join_type? JOIN table_ref join_qual + )* + ; + +alias_clause + : AS? colid (OPEN_PAREN name_list CLOSE_PAREN)? + ; + +func_alias_clause + : alias_clause + | (AS colid? | colid) OPEN_PAREN tablefuncelementlist CLOSE_PAREN + + ; + +join_type + : (FULL | LEFT | RIGHT | INNER_P) OUTER_P? + ; + +join_qual + : USING OPEN_PAREN name_list CLOSE_PAREN + | ON a_expr + ; + +relation_expr + : qualified_name STAR? + | ONLY (qualified_name | OPEN_PAREN qualified_name CLOSE_PAREN) + ; + +relation_expr_list + : relation_expr (COMMA relation_expr)* + ; + +relation_expr_opt_alias + : relation_expr (AS? colid)? + ; + +tablesample_clause + : TABLESAMPLE func_name OPEN_PAREN expr_list CLOSE_PAREN repeatable_clause_? + ; + +repeatable_clause_ + : REPEATABLE OPEN_PAREN a_expr CLOSE_PAREN + + ; + +func_table + : func_expr_windowless ordinality_? + | ROWS FROM OPEN_PAREN rowsfrom_list CLOSE_PAREN ordinality_? + ; + +rowsfrom_item + : func_expr_windowless col_def_list_? + ; + +rowsfrom_list + : rowsfrom_item (COMMA rowsfrom_item)* + ; + +col_def_list_ + : AS OPEN_PAREN tablefuncelementlist CLOSE_PAREN + + ; + +//TODO WITH_LA was used + +ordinality_ + : WITH ORDINALITY + + ; + +where_clause + : WHERE a_expr + + ; + +where_or_current_clause + : WHERE (CURRENT_P OF cursor_name | a_expr) + + ; + +opttablefuncelementlist + : tablefuncelementlist + + ; + +tablefuncelementlist + : tablefuncelement (COMMA tablefuncelement)* + ; + +tablefuncelement + : colid typename collate_clause_? + ; + +xmltable + : XMLTABLE OPEN_PAREN ( + c_expr xmlexists_argument COLUMNS xmltable_column_list + | XMLNAMESPACES OPEN_PAREN xml_namespace_list CLOSE_PAREN COMMA c_expr xmlexists_argument COLUMNS xmltable_column_list + ) CLOSE_PAREN + ; + +xmltable_column_list + : xmltable_column_el (COMMA xmltable_column_el)* + ; + +xmltable_column_el + : colid (typename xmltable_column_option_list? | FOR ORDINALITY) + ; + +xmltable_column_option_list + : xmltable_column_option_el+ + ; + +xmltable_column_option_el + : DEFAULT a_expr + | identifier a_expr + | NOT NULL_P + | NULL_P + ; + +xml_namespace_list + : xml_namespace_el (COMMA xml_namespace_el)* + ; + +xml_namespace_el + : b_expr AS colLabel + | DEFAULT b_expr + ; + +typename + : SETOF? simpletypename + ( opt_array_bounds + | ARRAY (OPEN_BRACKET iconst CLOSE_BRACKET)? + ) + ; + +opt_array_bounds + : (OPEN_BRACKET iconst? CLOSE_BRACKET)* + ; + +simpletypename + : generictype + | numeric + | bit + | character + | constdatetime + | constinterval (interval_? | OPEN_PAREN iconst CLOSE_PAREN) + | jsonType + ; + +consttypename + : numeric + | constbit + | constcharacter + | constdatetime + | jsonType + ; + +generictype + : type_function_name attrs? type_modifiers_? + ; + +type_modifiers_ + : OPEN_PAREN expr_list CLOSE_PAREN + + ; + +numeric + : INT_P + | INTEGER + | SMALLINT + | BIGINT + | REAL + | FLOAT_P float_? + | DOUBLE_P PRECISION + | DECIMAL_P type_modifiers_? + | DEC type_modifiers_? + | NUMERIC type_modifiers_? + | BOOLEAN_P + ; + +float_ + : OPEN_PAREN iconst CLOSE_PAREN + + ; + +//todo: merge alts + +bit + : bitwithlength + | bitwithoutlength + ; + +constbit + : bitwithlength + | bitwithoutlength + ; + +bitwithlength + : BIT varying_? OPEN_PAREN expr_list CLOSE_PAREN + ; + +bitwithoutlength + : BIT varying_? + ; + +character + : character_c (OPEN_PAREN iconst CLOSE_PAREN)? + ; + +constcharacter + : character_c (OPEN_PAREN iconst CLOSE_PAREN)? + ; + +character_c + : (CHARACTER | CHAR_P | NCHAR) varying_? + | VARCHAR + | NATIONAL (CHARACTER | CHAR_P) varying_? + ; + +varying_ + : VARYING + + ; + +constdatetime + : (TIMESTAMP | TIME) (OPEN_PAREN iconst CLOSE_PAREN)? timezone_? + ; + +constinterval + : INTERVAL + ; + +//TODO with_la was used + +timezone_ + : WITH TIME ZONE + | WITHOUT TIME ZONE + + ; + +interval_ + : YEAR_P + | MONTH_P + | DAY_P + | HOUR_P + | MINUTE_P + | interval_second + | YEAR_P TO MONTH_P + | DAY_P TO (HOUR_P | MINUTE_P | interval_second) + | HOUR_P TO (MINUTE_P | interval_second) + | MINUTE_P TO interval_second + + ; + +interval_second + : SECOND_P (OPEN_PAREN iconst CLOSE_PAREN)? + ; + +jsonType + : JSON + ; + +escape_ + : ESCAPE a_expr + + ; + +//precendence accroding to Table 4.2. Operator Precedence (highest to lowest) + +//https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-PRECEDENCE + +/* +original version of a_expr, for info + a_expr: c_expr + //:: left PostgreSQL-style typecast + | a_expr TYPECAST typename -- 1 + | a_expr COLLATE any_name -- 2 + | a_expr AT TIME ZONE a_expr-- 3 + //right unary plus, unary minus + | (PLUS| MINUS) a_expr -- 4 + //left exponentiation + | a_expr CARET a_expr -- 5 + //left multiplication, division, modulo + | a_expr (STAR | SLASH | PERCENT) a_expr -- 6 + //left addition, subtraction + | a_expr (PLUS | MINUS) a_expr -- 7 + //left all other native and user-defined operators + | a_expr qual_op a_expr -- 8 + | qual_op a_expr -- 9 + //range containment, set membership, string matching BETWEEN IN LIKE ILIKE SIMILAR + | a_expr NOT? (LIKE|ILIKE|SIMILAR TO|(BETWEEN SYMMETRIC?)) a_expr opt_escape -- 10 + //< > = <= >= <> comparison operators + | a_expr (LT | GT | EQUAL | LESS_EQUALS | GREATER_EQUALS | NOT_EQUALS) a_expr -- 11 + //IS ISNULL NOTNULL IS TRUE, IS FALSE, IS NULL, IS DISTINCT FROM, etc + | a_expr IS NOT? + ( + NULL_P + |TRUE_P + |FALSE_P + |UNKNOWN + |DISTINCT FROM a_expr + |OF OPEN_PAREN type_list CLOSE_PAREN + |DOCUMENT_P + |unicode_normal_form? NORMALIZED + ) -- 12 + | a_expr (ISNULL|NOTNULL) -- 13 + | row OVERLAPS row -- 14 + //NOT right logical negation + | NOT a_expr -- 15 + //AND left logical conjunction + | a_expr AND a_expr -- 16 + //OR left logical disjunction + | a_expr OR a_expr -- 17 + | a_expr (LESS_LESS|GREATER_GREATER) a_expr -- 18 + | a_expr qual_op -- 19 + | a_expr NOT? IN_P in_expr -- 20 + | a_expr subquery_Op sub_type (select_with_parens|OPEN_PAREN a_expr CLOSE_PAREN) -- 21 + | UNIQUE select_with_parens -- 22 + | DEFAULT -- 23 +; +*/ + +a_expr + : a_expr_qual + ; + +/*23*/ + +/*moved to c_expr*/ + +/*22*/ + +/*moved to c_expr*/ + +/*19*/ + +a_expr_qual + : a_expr_lessless ({this.OnlyAcceptableOps()}? qual_op | ) + ; + +/*18*/ + +a_expr_lessless + : a_expr_or ((LESS_LESS | GREATER_GREATER) a_expr_or)* + ; + +/*17*/ + +a_expr_or + : a_expr_and (OR a_expr_and)* + ; + +/*16*/ + +a_expr_and + : a_expr_between (AND a_expr_between)* + ; + +/*21*/ + +a_expr_between + : a_expr_in (NOT? BETWEEN SYMMETRIC? a_expr_in AND a_expr_in)? + ; + +/*20*/ + +a_expr_in + : a_expr_unary_not (NOT? IN_P in_expr)? + ; + +/*15*/ + +a_expr_unary_not + : NOT? a_expr_isnull + ; + +/*14*/ + +/*moved to c_expr*/ + +/*13*/ + +a_expr_isnull + : a_expr_is_not (ISNULL | NOTNULL)? + ; + +/*12*/ + +a_expr_is_not + : a_expr_compare ( + IS NOT? ( + NULL_P + | TRUE_P + | FALSE_P + | UNKNOWN + | DISTINCT FROM a_expr + | OF OPEN_PAREN type_list CLOSE_PAREN + | DOCUMENT_P + | unicode_normal_form? NORMALIZED + ) + )? + ; + +/*11*/ + +a_expr_compare + : a_expr_like ( + (LT | GT | EQUAL | LESS_EQUALS | GREATER_EQUALS | NOT_EQUALS) a_expr_like + | subquery_Op sub_type (select_with_parens | OPEN_PAREN a_expr CLOSE_PAREN) /*21*/ + )? + ; + +/*10*/ + +a_expr_like + : a_expr_qual_op (NOT? (LIKE | ILIKE | SIMILAR TO) a_expr_qual_op escape_?)? + ; + +/* 8*/ + +a_expr_qual_op + : a_expr_unary_qualop (qual_op a_expr_unary_qualop)* + ; + +/* 9*/ + +a_expr_unary_qualop + : qual_op? a_expr_add + ; + +/* 7*/ + +a_expr_add + : a_expr_mul ((MINUS | PLUS) a_expr_mul)* + ; + +/* 6*/ + +a_expr_mul + : a_expr_caret ((STAR | SLASH | PERCENT) a_expr_caret)* + ; + +/* 5*/ + +a_expr_caret + : a_expr_unary_sign (CARET a_expr_unary_sign)? + ; + +/* 4*/ + +a_expr_unary_sign + : (MINUS | PLUS)? a_expr_at_time_zone /* */ + ; + +/* 3*/ + +a_expr_at_time_zone + : a_expr_collate (AT TIME ZONE a_expr)? + ; + +/* 2*/ + +a_expr_collate + : a_expr_typecast (COLLATE any_name)? + ; + +/* 1*/ + +a_expr_typecast + : c_expr (TYPECAST typename)* + ; + +b_expr + : c_expr + | b_expr TYPECAST typename + //right unary plus, unary minus + | (PLUS | MINUS) b_expr + //^ left exponentiation + | b_expr CARET b_expr + //* / % left multiplication, division, modulo + | b_expr (STAR | SLASH | PERCENT) b_expr + //+ - left addition, subtraction + | b_expr (PLUS | MINUS) b_expr + //(any other operator) left all other native and user-defined operators + | b_expr qual_op b_expr + //< > = <= >= <> comparison operators + | b_expr (LT | GT | EQUAL | LESS_EQUALS | GREATER_EQUALS | NOT_EQUALS) b_expr + | qual_op b_expr + | b_expr qual_op + //S ISNULL NOTNULL IS TRUE, IS FALSE, IS NULL, IS DISTINCT FROM, etc + | b_expr IS NOT? (DISTINCT FROM b_expr | OF OPEN_PAREN type_list CLOSE_PAREN | DOCUMENT_P) + ; + +c_expr + : EXISTS select_with_parens # c_expr_exists + | ARRAY (select_with_parens | array_expr) # c_expr_expr + | PARAM opt_indirection # c_expr_expr + // scim-sql local modification: named parameters (:name) are first-class expression values. + | plsqlvariablename # c_expr_namedparam + | GROUPING OPEN_PAREN expr_list CLOSE_PAREN # c_expr_expr + | /*22*/ UNIQUE select_with_parens # c_expr_expr + | columnref # c_expr_expr + | aexprconst # c_expr_expr + | OPEN_PAREN a_expr_in_parens = a_expr CLOSE_PAREN opt_indirection # c_expr_expr + | case_expr # c_expr_case + | func_expr # c_expr_expr + | select_with_parens indirection? # c_expr_expr + | explicit_row # c_expr_expr + | implicit_row # c_expr_expr + | row OVERLAPS row /* 14*/ # c_expr_expr + | DEFAULT # c_expr_expr + ; + +plsqlvariablename + : PLSQLVARIABLENAME + ; + +func_application + : func_name OPEN_PAREN ( + func_arg_list (COMMA VARIADIC func_arg_expr)? sort_clause_? + | VARIADIC func_arg_expr sort_clause_? + | (ALL | DISTINCT) func_arg_list sort_clause_? + | STAR + | + ) CLOSE_PAREN + ; + +func_expr + : func_application within_group_clause? filter_clause? over_clause? + | func_expr_common_subexpr + ; + +func_expr_windowless + : func_application + | func_expr_common_subexpr + ; + +func_expr_common_subexpr + : COLLATION FOR OPEN_PAREN a_expr CLOSE_PAREN + | CURRENT_DATE + | CURRENT_TIME (OPEN_PAREN iconst CLOSE_PAREN)? + | CURRENT_TIMESTAMP (OPEN_PAREN iconst CLOSE_PAREN)? + | LOCALTIME (OPEN_PAREN iconst CLOSE_PAREN)? + | LOCALTIMESTAMP (OPEN_PAREN iconst CLOSE_PAREN)? + | CURRENT_ROLE + | CURRENT_USER + | SESSION_USER + | SYSTEM_USER + | USER + | CURRENT_CATALOG + | CURRENT_SCHEMA + | CAST OPEN_PAREN a_expr AS typename CLOSE_PAREN + | EXTRACT OPEN_PAREN extract_list? CLOSE_PAREN + | NORMALIZE OPEN_PAREN a_expr (COMMA unicode_normal_form)? CLOSE_PAREN + | OVERLAY OPEN_PAREN (overlay_list | func_arg_list? ) CLOSE_PAREN + | POSITION OPEN_PAREN position_list? CLOSE_PAREN + | SUBSTRING OPEN_PAREN (substr_list | func_arg_list?) CLOSE_PAREN + | TREAT OPEN_PAREN a_expr AS typename CLOSE_PAREN + | TRIM OPEN_PAREN (BOTH | LEADING | TRAILING)? trim_list CLOSE_PAREN + | NULLIF OPEN_PAREN a_expr COMMA a_expr CLOSE_PAREN + | COALESCE OPEN_PAREN expr_list CLOSE_PAREN + | GREATEST OPEN_PAREN expr_list CLOSE_PAREN + | LEAST OPEN_PAREN expr_list CLOSE_PAREN + | XMLCONCAT OPEN_PAREN expr_list CLOSE_PAREN + | XMLELEMENT OPEN_PAREN NAME_P colLabel (COMMA (xml_attributes | expr_list))? CLOSE_PAREN + | XMLEXISTS OPEN_PAREN c_expr xmlexists_argument CLOSE_PAREN + | XMLFOREST OPEN_PAREN xml_attribute_list CLOSE_PAREN + | XMLPARSE OPEN_PAREN document_or_content a_expr xml_whitespace_option? CLOSE_PAREN + | XMLPI OPEN_PAREN NAME_P colLabel (COMMA a_expr)? CLOSE_PAREN + | XMLROOT OPEN_PAREN XML_P a_expr COMMA xml_root_version xml_root_standalone_? CLOSE_PAREN + | XMLSERIALIZE OPEN_PAREN document_or_content a_expr AS simpletypename CLOSE_PAREN + | JSON_OBJECT OPEN_PAREN (func_arg_list + | json_name_and_value_list + json_object_constructor_null_clause? + json_key_uniqueness_constraint? + json_returning_clause? + | json_returning_clause? ) + CLOSE_PAREN + | JSON_ARRAY OPEN_PAREN (json_value_expr_list + json_array_constructor_null_clause? + json_returning_clause? + | select_no_parens + json_format_clause? + json_returning_clause? + | json_returning_clause? + ) + CLOSE_PAREN + | JSON '(' json_value_expr json_key_uniqueness_constraint? ')' + | JSON_SCALAR '(' a_expr ')' + | JSON_SERIALIZE '(' json_value_expr json_returning_clause? ')' + | MERGE_ACTION '(' ')' + | JSON_QUERY '(' + json_value_expr ',' a_expr json_passing_clause? + json_returning_clause? + json_wrapper_behavior + json_quotes_clause? + json_behavior_clause? + ')' + | JSON_EXISTS '(' + json_value_expr ',' a_expr json_passing_clause? + json_on_error_clause? + ')' + | JSON_VALUE '(' + json_value_expr ',' a_expr json_passing_clause? + json_returning_clause? + json_behavior_clause? + ')' + ; + +/* SQL/XML support */ + +xml_root_version + : VERSION_P a_expr + | VERSION_P NO VALUE_P + ; + +xml_root_standalone_ + : COMMA STANDALONE_P YES_P + | COMMA STANDALONE_P NO + | COMMA STANDALONE_P NO VALUE_P + ; + +xml_attributes + : XMLATTRIBUTES OPEN_PAREN xml_attribute_list CLOSE_PAREN + ; + +xml_attribute_list + : xml_attribute_el (COMMA xml_attribute_el)* + ; + +xml_attribute_el + : a_expr (AS colLabel)? + ; + +document_or_content + : DOCUMENT_P + | CONTENT_P + ; + +xml_whitespace_option + : PRESERVE WHITESPACE_P + | STRIP_P WHITESPACE_P + + ; + +xmlexists_argument + : PASSING c_expr + | PASSING c_expr xml_passing_mech + | PASSING xml_passing_mech c_expr + | PASSING xml_passing_mech c_expr xml_passing_mech + ; + +xml_passing_mech + : BY (REF | VALUE_P) + ; + +within_group_clause + : WITHIN GROUP_P OPEN_PAREN sort_clause CLOSE_PAREN + + ; + +filter_clause + : FILTER OPEN_PAREN WHERE a_expr CLOSE_PAREN + + ; + +window_clause + : WINDOW window_definition_list + + ; + +window_definition_list + : window_definition (COMMA window_definition)* + ; + +window_definition + : colid AS window_specification + ; + +over_clause + : OVER (window_specification | colid) + + ; + +window_specification + : OPEN_PAREN existing_window_name_? partition_clause_? sort_clause_? frame_clause_? CLOSE_PAREN + ; + +existing_window_name_ + : colid + + ; + +partition_clause_ + : PARTITION BY expr_list + + ; + +frame_clause_ + : RANGE frame_extent window_exclusion_clause_? + | ROWS frame_extent window_exclusion_clause_? + | GROUPS frame_extent window_exclusion_clause_? + + ; + +frame_extent + : frame_bound + | BETWEEN frame_bound AND frame_bound + ; + +frame_bound + : UNBOUNDED (PRECEDING | FOLLOWING) + | CURRENT_P ROW + | a_expr (PRECEDING | FOLLOWING) + ; + +window_exclusion_clause_ + : EXCLUDE (CURRENT_P ROW | GROUP_P | TIES | NO OTHERS) + + ; + +row + : ROW OPEN_PAREN expr_list? CLOSE_PAREN + | OPEN_PAREN expr_list COMMA a_expr CLOSE_PAREN + ; + +explicit_row + : ROW OPEN_PAREN expr_list? CLOSE_PAREN + ; + +/* +TODO: +for some reason v1 +implicit_row: OPEN_PAREN expr_list COMMA a_expr CLOSE_PAREN; +works better than v2 +implicit_row: OPEN_PAREN expr_list CLOSE_PAREN; +while looks like they are almost the same, except v2 requieres at least 2 items in list +while v1 allows single item in list +*/ + +implicit_row + : OPEN_PAREN expr_list COMMA a_expr CLOSE_PAREN + ; + +sub_type + : ANY + | SOME + | ALL + ; + +all_op + : Operator + | mathop + ; + +mathop + : PLUS + | MINUS + | STAR + | SLASH + | PERCENT + | CARET + | LT + | GT + | EQUAL + | LESS_EQUALS + | GREATER_EQUALS + | NOT_EQUALS + ; + +qual_op + : Operator + | OPERATOR OPEN_PAREN any_operator CLOSE_PAREN + ; + +qual_all_op + : all_op + | OPERATOR OPEN_PAREN any_operator CLOSE_PAREN + ; + +subquery_Op + : all_op + | OPERATOR OPEN_PAREN any_operator CLOSE_PAREN + | LIKE + | NOT LIKE + | ILIKE + | NOT ILIKE + ; + +expr_list + : a_expr (COMMA a_expr)* + ; + +func_arg_list + : func_arg_expr (COMMA func_arg_expr)* + ; + +func_arg_expr + : a_expr + | param_name (COLON_EQUALS | EQUALS_GREATER) a_expr + ; + +type_list + : typename (COMMA typename)* + ; + +array_expr + : OPEN_BRACKET (expr_list | array_expr_list)? CLOSE_BRACKET + ; + +array_expr_list + : array_expr (COMMA array_expr)* + ; + +extract_list + : extract_arg FROM a_expr + + ; + +extract_arg + : identifier + | YEAR_P + | MONTH_P + | DAY_P + | HOUR_P + | MINUTE_P + | SECOND_P + | sconst + ; + +unicode_normal_form + : NFC + | NFD + | NFKC + | NFKD + ; + +overlay_list + : a_expr PLACING a_expr FROM a_expr (FOR a_expr)? + ; + +position_list + : b_expr IN_P b_expr + + ; + +substr_list + : a_expr FROM a_expr FOR a_expr + | a_expr FOR a_expr FROM a_expr + | a_expr FROM a_expr + | a_expr FOR a_expr + | a_expr SIMILAR a_expr ESCAPE a_expr + ; + +trim_list + : a_expr FROM expr_list + | FROM expr_list + | expr_list + ; + +in_expr + : select_with_parens # in_expr_select + | OPEN_PAREN expr_list CLOSE_PAREN # in_expr_list + ; + +case_expr + : CASE case_arg? when_clause_list case_default? END_P + ; + +when_clause_list + : when_clause+ + ; + +when_clause + : WHEN a_expr THEN a_expr + ; + +case_default + : ELSE a_expr + + ; + +case_arg + : a_expr + + ; + +columnref + : colid indirection? + ; + +indirection_el + : DOT (attr_name | STAR) + | OPEN_BRACKET (a_expr | slice_bound_? COLON slice_bound_?) CLOSE_BRACKET + ; + +slice_bound_ + : a_expr + + ; + +indirection + : indirection_el+ + ; + +opt_indirection + : indirection_el* + ; + +/* SQL/JSON support */ +json_passing_clause: + PASSING json_arguments + ; + +json_arguments: + json_argument + | json_arguments ',' json_argument + ; + +json_argument: + json_value_expr AS colLabel + ; + +/* ARRAY is a noise word */ +json_wrapper_behavior: + WITHOUT WRAPPER + | WITHOUT ARRAY WRAPPER + | WITH WRAPPER + | WITH ARRAY WRAPPER + | WITH CONDITIONAL ARRAY WRAPPER + | WITH UNCONDITIONAL ARRAY WRAPPER + | WITH CONDITIONAL WRAPPER + | WITH UNCONDITIONAL WRAPPER + | + ; + +json_behavior: + DEFAULT a_expr + | json_behavior_type + ; + +json_behavior_type: + ERROR + | NULL_P + | TRUE_P + | FALSE_P + | UNKNOWN + | EMPTY_P ARRAY + | EMPTY_P OBJECT_P + /* non-standard, for Oracle compatibility only */ + | EMPTY_P + ; + +json_behavior_clause: + json_behavior ON EMPTY_P + | json_behavior ON ERROR + | json_behavior ON EMPTY_P json_behavior ON ERROR + ; + +json_on_error_clause: + json_behavior ON ERROR + ; + +json_value_expr: + a_expr json_format_clause? + ; + +json_format_clause: + FORMAT_LA JSON ENCODING name + | FORMAT_LA JSON + ; + + +json_quotes_clause: + KEEP QUOTES ON SCALAR STRING_P + | KEEP QUOTES + | OMIT QUOTES ON SCALAR STRING_P + | OMIT QUOTES + ; + +json_returning_clause: + RETURNING typename json_format_clause? + ; + +/* + * We must assign the only-JSON production a precedence less than IDENT in + * order to favor shifting over reduction when JSON is followed by VALUE_P, + * OBJECT_P, or SCALAR. (ARRAY doesn't need that treatment, because it's a + * fully reserved word.) Because json_predicate_type_constraint is always + * followed by json_key_uniqueness_constraint_opt, we also need the only-JSON + * production to have precedence less than WITH and WITHOUT. UNBOUNDED isn't + * really related to this syntax, but it's a convenient choice because it + * already has a precedence less than IDENT for other reasons. + */ +json_predicate_type_constraint: + JSON + | JSON VALUE_P + | JSON ARRAY + | JSON OBJECT_P + | JSON SCALAR + ; + +/* + * KEYS is a noise word here. To avoid shift/reduce conflicts, assign the + * KEYS-less productions a precedence less than IDENT (i.e., less than KEYS). + * This prevents reducing them when the next token is KEYS. + */ +json_key_uniqueness_constraint: + WITH UNIQUE KEYS + | WITH UNIQUE + | WITHOUT UNIQUE KEYS + | WITHOUT UNIQUE + ; + +json_name_and_value_list: + json_name_and_value + | json_name_and_value_list ',' json_name_and_value + ; + +json_name_and_value: + c_expr VALUE_P json_value_expr + | + a_expr ':' json_value_expr + ; + +/* empty means false for objects, true for arrays */ +json_object_constructor_null_clause: + NULL_P ON NULL_P + | ABSENT ON NULL_P + ; + +json_array_constructor_null_clause: + NULL_P ON NULL_P + | ABSENT ON NULL_P + ; + +json_value_expr_list: + json_value_expr + | json_value_expr_list ',' json_value_expr + ; + +json_aggregate_func: + JSON_OBJECTAGG '(' + json_name_and_value + json_object_constructor_null_clause? + json_key_uniqueness_constraint? + json_returning_clause + ')' + | JSON_ARRAYAGG '(' + json_value_expr + json_array_aggregate_order_by_clause? + json_array_constructor_null_clause? + json_returning_clause + ')' + ; + +json_array_aggregate_order_by_clause: + ORDER BY sortby_list + ; + +/***************************************************************************** + * + * target list for SELECT + * + *****************************************************************************/ + +target_list_ + : target_list + + ; + +target_list + : target_el (COMMA target_el)* + ; + +target_el + : a_expr (AS colLabel | bareColLabel |) # target_label + | STAR # target_star + ; + +qualified_name_list + : qualified_name (COMMA qualified_name)* + ; + +qualified_name + : colid indirection? + ; + +name_list + : name (COMMA name)* + ; + +name + : colid + ; + +attr_name + : colLabel + ; + +file_name + : sconst + ; + +func_name + : type_function_name + | colid indirection + ; + +aexprconst + : iconst + | fconst + | sconst + | bconst + | xconst + | func_name (sconst | OPEN_PAREN func_arg_list sort_clause_? CLOSE_PAREN sconst) + | consttypename sconst + | constinterval (sconst interval_? | OPEN_PAREN iconst CLOSE_PAREN sconst) + | TRUE_P + | FALSE_P + | NULL_P + ; + +xconst + : HexadecimalStringConstant + ; + +bconst + : BinaryStringConstant + ; + +fconst + : Numeric + ; + +iconst + : Integral + | BinaryIntegral + | OctalIntegral + | HexadecimalIntegral + ; + +sconst + : anysconst uescape_? + ; + +anysconst + : StringConstant + | UnicodeEscapeStringConstant + | BeginDollarStringConstant DollarText* EndDollarStringConstant + | EscapeStringConstant + ; + +uescape_ + : UESCAPE anysconst + + ; + +signediconst + : iconst + | PLUS iconst + | MINUS iconst + ; + +roleid + : rolespec + ; + +rolespec + : nonreservedword + | CURRENT_USER + | SESSION_USER + ; + +role_list + : rolespec (COMMA rolespec)* + ; + +/* + * Name classification hierarchy. + * + * IDENT is the lexeme returned by the lexer for identifiers that match + * no known keyword. In most cases, we can accept certain keywords as + * names, not only IDENTs. We prefer to accept as many such keywords + * as possible to minimize the impact of "reserved words" on programmers. + * So, we divide names into several possible classes. The classification + * is chosen in part to make keywords acceptable as names wherever possible. + */ + +/* Column identifier --- names that can be column, table, etc names. + */ +colid + : identifier + | unreserved_keyword + | col_name_keyword + ; + +/* Type/function identifier --- names that can be type or function names. + */ +type_function_name + : identifier + | unreserved_keyword + | type_func_name_keyword + ; + +/* Any not-fully-reserved word --- these names can be, eg, role names. + */ +nonreservedword + : identifier + | unreserved_keyword + | col_name_keyword + | type_func_name_keyword + ; + +/* Column label --- allowed labels in "AS" clauses. + * This presently includes *all* Postgres keywords. + */ +colLabel + : identifier + | unreserved_keyword + | col_name_keyword + | type_func_name_keyword + | reserved_keyword + | EXIT //NB: not in gram.y official source. + ; + +/* Bare column label --- names that can be column labels without writing "AS". + * This classification is orthogonal to the other keyword categories. + */ +bareColLabel + : identifier + | bare_label_keyword + ; + +/* + * Keyword category lists. Generally, every keyword present in + * the Postgres grammar should appear in exactly one of these lists. + * + * Put a new keyword into the first list that it can go into without causing + * shift or reduce conflicts. The earlier lists define "less reserved" + * categories of keywords. + * + * Make sure that each keyword's category in kwlist.h matches where + * it is listed here. (Someday we may be able to generate these lists and + * kwlist.h's table from one source of truth.) + */ + +/* "Unreserved" keywords --- available for use as any kind of name. + */ +unreserved_keyword + : ABORT_P + | ABSENT + | ABSOLUTE_P + | ACCESS + | ACTION + | ADD_P + | ADMIN + | AFTER + | AGGREGATE + | ALSO + | ALTER + | ALWAYS + | ASENSITIVE + | ASSERTION + | ASSIGNMENT + | AT + | ATOMIC + | ATTACH + | ATTRIBUTE + | BACKWARD + | BEFORE + | BEGIN_P + | BREADTH + | BY + | CACHE + | CALL + | CALLED + | CASCADE + | CASCADED + | CATALOG + | CHAIN + | CHARACTERISTICS + | CHECKPOINT + | CLASS + | CLOSE + | CLUSTER + | COLUMNS + | COMMENT + | COMMENTS + | COMMIT + | COMMITTED + | COMPRESSION + | CONDITIONAL + | CONFIGURATION + | CONFLICT + | CONNECTION + | CONSTRAINTS + | CONTENT_P + | CONTINUE_P + | CONVERSION_P + | COPY + | COST + | CSV + | CUBE + | CURRENT_P + | CURSOR + | CYCLE + | DATA_P + | DATABASE + | DAY_P + | DEALLOCATE + | DECLARE + | DEFAULTS + | DEFERRED + | DEFINER + | DELETE_P + | DELIMITER + | DELIMITERS + | DEPENDS + | DEPTH + | DETACH + | DICTIONARY + | DISABLE_P + | DISCARD + | DOCUMENT_P + | DOMAIN_P + | DOUBLE_P + | DROP + | EACH + | EMPTY_P + | ENABLE_P + | ENCODING + | ENCRYPTED + | ENUM_P + | ERROR + | ESCAPE + | EVENT + | EXCLUDE + | EXCLUDING + | EXCLUSIVE + | EXECUTE + | EXPLAIN + | EXPRESSION + | EXTENSION + | EXTERNAL + | FAMILY + | FILTER + | FINALIZE + | FIRST_P + | FOLLOWING + | FORCE + | FORMAT + | FORWARD + | FUNCTION + | FUNCTIONS + | GENERATED + | GLOBAL + | GRANTED + | GROUPS + | HANDLER + | HEADER_P + | HOLD + | HOUR_P + | IDENTITY_P + | IF_P + | IMMEDIATE + | IMMUTABLE + | IMPLICIT_P + | IMPORT_P + | INCLUDE + | INCLUDING + | INCREMENT + | INDENT + | INDEX + | INDEXES + | INHERIT + | INHERITS + | INLINE_P + | INPUT_P + | INSENSITIVE + | INSERT + | INSTEAD + | INVOKER + | ISOLATION + | KEEP + | KEY + | KEYS + | LABEL + | LANGUAGE + | LARGE_P + | LAST_P + | LEAKPROOF + | LEVEL + | LISTEN + | LOAD + | LOCAL + | LOCATION + | LOCK_P + | LOCKED + | LOGGED + | MAPPING + | MATCH + | MATCHED + | MATERIALIZED + | MAXVALUE + | MERGE + | METHOD + | MINUTE_P + | MINVALUE + | MODE + | MONTH_P + | MOVE + | NAME_P + | NAMES + | NESTED + | NEW + | NEXT + | NFC + | NFD + | NFKC + | NFKD + | NO + | NORMALIZED + | NOTHING + | NOTIFY + | NOWAIT + | NULLS_P + | OBJECT_P + | OF + | OFF + | OIDS + | OLD + | OMIT + | OPERATOR + | OPTION + | OPTIONS + | ORDINALITY + | OTHERS + | OVER + | OVERRIDING + | OWNED + | OWNER + | PARALLEL + | PARAMETER + | PARSER + | PARTIAL + | PARTITION + | PASSING + | PASSWORD + | PATH + | PERIOD + | PLAN + | PLANS + | POLICY + | PRECEDING + | PREPARE + | PREPARED + | PRESERVE + | PRIOR + | PRIVILEGES + | PROCEDURAL + | PROCEDURE + | PROCEDURES + | PROGRAM + | PUBLICATION + | QUOTE + | QUOTES + | RANGE + | READ + | REASSIGN +// | RECHECK + | RECURSIVE + | REF + | REFERENCING + | REFRESH + | REINDEX + | RELATIVE_P + | RELEASE + | RENAME + | REPEATABLE + | REPLACE + | REPLICA + | RESET + | RESTART + | RESTRICT + | RETURN + | RETURNS + | REVOKE + | ROLE + | ROLLBACK + | ROLLUP + | ROUTINE + | ROUTINES + | ROWS + | RULE + | SAVEPOINT + | SCALAR + | SCHEMA + | SCHEMAS + | SCROLL + | SEARCH + | SECOND_P + | SECURITY + | SEQUENCE + | SEQUENCES + | SERIALIZABLE + | SERVER + | SESSION + | SET + | SETS + | SHARE + | SHOW + | SIMPLE + | SKIP_P + | SNAPSHOT + | SOURCE + | SQL_P + | STABLE + | STANDALONE_P + | START + | STATEMENT + | STATISTICS + | STDIN + | STDOUT + | STORAGE + | STORED + | STRICT_P + | STRING_P + | STRIP_P + | SUBSCRIPTION + | SUPPORT + | SYSID + | SYSTEM_P + | TABLES + | TABLESPACE + | TARGET + | TEMP + | TEMPLATE + | TEMPORARY + | TEXT_P + | TIES + | TRANSACTION + | TRANSFORM + | TRIGGER + | TRUNCATE + | TRUSTED + | TYPE_P + | TYPES_P + | UESCAPE + | UNBOUNDED + | UNCOMMITTED + | UNCONDITIONAL + | UNENCRYPTED + | UNKNOWN + | UNLISTEN + | UNLOGGED + | UNTIL + | UPDATE + | VACUUM + | VALID + | VALIDATE + | VALIDATOR + | VALUE_P + | VARYING + | VERSION_P + | VIEW + | VIEWS + | VOLATILE + | WHITESPACE_P + | WITHIN + | WITHOUT + | WORK + | WRAPPER + | WRITE + | XML_P + | YEAR_P + | YES_P + | ZONE + ; + +/* Column identifier --- keywords that can be column, table, etc names. + * + * Many of these keywords will in fact be recognized as type or function + * names too; but they have special productions for the purpose, and so + * can't be treated as "generic" type or function names. + * + * The type names appearing here are not usable as function names + * because they can be followed by '(' in typename productions, which + * looks too much like a function call for an LR(1) parser. + */ +col_name_keyword + : BETWEEN + | BIGINT + | BIT + | BOOLEAN_P + | CHAR_P + | character + | COALESCE + | DEC + | DECIMAL_P + | EXISTS + | EXTRACT + | FLOAT_P + | GREATEST + | GROUPING + | INOUT + | INT_P + | INTEGER + | INTERVAL + | JSON + | JSON_ARRAY + | JSON_ARRAYAGG + | JSON_EXISTS + | JSON_OBJECT + | JSON_OBJECTAGG + | JSON_QUERY + | JSON_SCALAR + | JSON_SERIALIZE + | JSON_TABLE + | JSON_VALUE + | LEAST + | MERGE_ACTION + | NATIONAL + | NCHAR + | NONE + | NORMALIZE + | NULLIF + | NUMERIC + | OUT_P + | OVERLAY + | POSITION + | PRECISION + | REAL + | ROW + | SETOF + | SMALLINT + | SUBSTRING + | TIME + | TIMESTAMP + | TREAT + | TRIM + | VALUES + | VARCHAR + | XMLATTRIBUTES + | XMLCONCAT + | XMLELEMENT + | XMLEXISTS + | XMLFOREST + | XMLNAMESPACES + | XMLPARSE + | XMLPI + | XMLROOT + | XMLSERIALIZE + | XMLTABLE + ; + +/* Type/function identifier --- keywords that can be type or function names. + * + * Most of these are keywords that are used as operators in expressions; + * in general such keywords can't be column names because they would be + * ambiguous with variables, but they are unambiguous as function identifiers. + * + * Do not include POSITION, SUBSTRING, etc here since they have explicit + * productions in a_expr to support the goofy SQL9x argument syntax. + * - thomas 2000-11-28 + */ +type_func_name_keyword + : AUTHORIZATION + | BINARY + | COLLATION + | CONCURRENTLY + | CROSS + | CURRENT_SCHEMA + | FREEZE + | FULL + | ILIKE + | INNER_P + | IS + | ISNULL + | JOIN + | LEFT + | LIKE + | NATURAL + | NOTNULL + | OUTER_P + | OVERLAPS + | RIGHT + | SIMILAR + | TABLESAMPLE + | VERBOSE + ; + +/* Reserved keyword --- these keywords are usable only as a ColLabel. + * + * Keywords appear here if they could not be distinguished from variable, + * type, or function names in some contexts. Don't put things here unless + * forced to. + */ +reserved_keyword + : ALL + | ANALYSE + | ANALYZE + | AND + | ANY + | ARRAY + | AS + | ASC + | ASYMMETRIC + | BOTH + | CASE + | CAST + | CHECK + | COLLATE + | COLUMN + | CONSTRAINT + | CREATE + | CURRENT_CATALOG + | CURRENT_DATE + | CURRENT_ROLE + | CURRENT_TIME + | CURRENT_TIMESTAMP + | CURRENT_USER + | DEFAULT + | DEFERRABLE + | DESC + | DISTINCT + | DO + | ELSE + | END_P + | EXCEPT + | FALSE_P + | FETCH + | FOR + | FOREIGN + | FROM + | GRANT + | GROUP_P + | HAVING + | IN_P + | INITIALLY + | INTERSECT + | INTO + | LATERAL_P + | LEADING + | LIMIT + | LOCALTIME + | LOCALTIMESTAMP + | NOT + | NULL_P + | OFFSET + | ON + | ONLY + | OR + | ORDER + | PLACING + | PRIMARY + | REFERENCES + | RETURNING + | SELECT + | SESSION_USER + | SOME + | SYMMETRIC + | SYSTEM_USER + | TABLE + | THEN + | TO + | TRAILING + | TRUE_P + | UNION + | UNIQUE + | USER + | USING + | VARIADIC + | WHEN + | WHERE + | WINDOW + | WITH + ; + +/* + * While all keywords can be used as column labels when preceded by AS, + * not all of them can be used as a "bare" column label without AS. + * Those that can be used as a bare label must be listed here, + * in addition to appearing in one of the category lists above. + * + * Always add a new keyword to this list if possible. Mark it BARE_LABEL + * in kwlist.h if it is included here, or AS_LABEL if it is not. + */ +bare_label_keyword + : ABORT_P + | ABSENT + | ABSOLUTE_P + | ACCESS + | ACTION + | ADD_P + | ADMIN + | AFTER + | AGGREGATE + | ALL + | ALSO + | ALTER + | ALWAYS + | ANALYSE + | ANALYZE + | AND + | ANY + | ASC + | ASENSITIVE + | ASSERTION + | ASSIGNMENT + | ASYMMETRIC + | AT + | ATOMIC + | ATTACH + | ATTRIBUTE + | AUTHORIZATION + | BACKWARD + | BEFORE + | BEGIN_P + | BETWEEN + | BIGINT + | BINARY + | BIT + | BOOLEAN_P + | BOTH + | BREADTH + | BY + | CACHE + | CALL + | CALLED + | CASCADE + | CASCADED + | CASE + | CAST + | CATALOG + | CHAIN + | CHARACTERISTICS + | CHECK + | CHECKPOINT + | CLASS + | CLOSE + | CLUSTER + | COALESCE + | COLLATE + | COLLATION + | COLUMN + | COLUMNS + | COMMENT + | COMMENTS + | COMMIT + | COMMITTED + | COMPRESSION + | CONCURRENTLY + | CONDITIONAL + | CONFIGURATION + | CONFLICT + | CONNECTION + | CONSTRAINT + | CONSTRAINTS + | CONTENT_P + | CONTINUE_P + | CONVERSION_P + | COPY + | COST + | CROSS + | CSV + | CUBE + | CURRENT_CATALOG + | CURRENT_DATE + | CURRENT_P + | CURRENT_ROLE + | CURRENT_SCHEMA + | CURRENT_TIME + | CURRENT_TIMESTAMP + | CURRENT_USER + | CURSOR + | CYCLE + | DATA_P + | DATABASE + | DEALLOCATE + | DEC + | DECIMAL_P + | DECLARE + | DEFAULT + | DEFAULTS + | DEFERRABLE + | DEFERRED + | DEFINER + | DELETE_P + | DELIMITER + | DELIMITERS + | DEPENDS + | DEPTH + | DESC + | DETACH + | DICTIONARY + | DISABLE_P + | DISCARD + | DISTINCT + | DO + | DOCUMENT_P + | DOMAIN_P + | DOUBLE_P + | DROP + | EACH + | ELSE + | EMPTY_P + | ENABLE_P + | ENCODING + | ENCRYPTED + | END_P + | ENUM_P + | ERROR + | ESCAPE + | EVENT + | EXCLUDE + | EXCLUDING + | EXCLUSIVE + | EXECUTE + | EXISTS + | EXPLAIN + | EXPRESSION + | EXTENSION + | EXTERNAL + | EXTRACT + | FALSE_P + | FAMILY + | FINALIZE + | FIRST_P + | FLOAT_P + | FOLLOWING + | FORCE + | FOREIGN + | FORMAT + | FORWARD + | FREEZE + | FULL + | FUNCTION + | FUNCTIONS + | GENERATED + | GLOBAL + | GRANTED + | GREATEST + | GROUPING + | GROUPS + | HANDLER + | HEADER_P + | HOLD + | IDENTITY_P + | IF_P + | ILIKE + | IMMEDIATE + | IMMUTABLE + | IMPLICIT_P + | IMPORT_P + | IN_P + | INCLUDE + | INCLUDING + | INCREMENT + | INDENT + | INDEX + | INDEXES + | INHERIT + | INHERITS + | INITIALLY + | INLINE_P + | INNER_P + | INOUT + | INPUT_P + | INSENSITIVE + | INSERT + | INSTEAD + | INT_P + | INTEGER + | INTERVAL + | INVOKER + | IS + | ISOLATION + | JOIN + | JSON + | JSON_ARRAY + | JSON_ARRAYAGG + | JSON_EXISTS + | JSON_OBJECT + | JSON_OBJECTAGG + | JSON_QUERY + | JSON_SCALAR + | JSON_SERIALIZE + | JSON_TABLE + | JSON_VALUE + | KEEP + | KEY + | KEYS + | LABEL + | LANGUAGE + | LARGE_P + | LAST_P + | LATERAL_P + | LEADING + | LEAKPROOF + | LEAST + | LEFT + | LEVEL + | LIKE + | LISTEN + | LOAD + | LOCAL + | LOCALTIME + | LOCALTIMESTAMP + | LOCATION + | LOCK_P + | LOCKED + | LOGGED + | MAPPING + | MATCH + | MATCHED + | MATERIALIZED + | MAXVALUE + | MERGE + | MERGE_ACTION + | METHOD + | MINVALUE + | MODE + | MOVE + | NAME_P + | NAMES + | NATIONAL + | NATURAL + | NCHAR + | NESTED + | NEW + | NEXT + | NFC + | NFD + | NFKC + | NFKD + | NO + | NONE + | NORMALIZE + | NORMALIZED + | NOT + | NOTHING + | NOTIFY + | NOWAIT + | NULL_P + | NULLIF + | NULLS_P + | NUMERIC + | OBJECT_P + | OF + | OFF + | OIDS + | OLD + | OMIT + | ONLY + | OPERATOR + | OPTION + | OPTIONS + | OR + | ORDINALITY + | OTHERS + | OUT_P + | OUTER_P + | OVERLAY + | OVERRIDING + | OWNED + | OWNER + | PARALLEL + | PARAMETER + | PARSER + | PARTIAL + | PARTITION + | PASSING + | PASSWORD + | PATH + | PERIOD + | PLACING + | PLAN + | PLANS + | POLICY + | POSITION + | PRECEDING + | PREPARE + | PREPARED + | PRESERVE + | PRIMARY + | PRIOR + | PRIVILEGES + | PROCEDURAL + | PROCEDURE + | PROCEDURES + | PROGRAM + | PUBLICATION + | QUOTE + | QUOTES + | RANGE + | READ + | REAL + | REASSIGN + | RECURSIVE + | REF + | REFERENCES + | REFERENCING + | REFRESH + | REINDEX + | RELATIVE_P + | RELEASE + | RENAME + | REPEATABLE + | REPLACE + | REPLICA + | RESET + | RESTART + | RESTRICT + | RETURN + | RETURNS + | REVOKE + | RIGHT + | ROLE + | ROLLBACK + | ROLLUP + | ROUTINE + | ROUTINES + | ROW + | ROWS + | RULE + | SAVEPOINT + | SCALAR + | SCHEMA + | SCHEMAS + | SCROLL + | SEARCH + | SECURITY + | SELECT + | SEQUENCE + | SEQUENCES + | SERIALIZABLE + | SERVER + | SESSION + | SESSION_USER + | SET + | SETOF + | SETS + | SHARE + | SHOW + | SIMILAR + | SIMPLE + | SKIP_P + | SMALLINT + | SNAPSHOT + | SOME + | SOURCE + | SQL_P + | STABLE + | STANDALONE_P + | START + | STATEMENT + | STATISTICS + | STDIN + | STDOUT + | STORAGE + | STORED + | STRICT_P + | STRING_P + | STRIP_P + | SUBSCRIPTION + | SUBSTRING + | SUPPORT + | SYMMETRIC + | SYSID + | SYSTEM_P + | SYSTEM_USER + | TABLE + | TABLES + | TABLESAMPLE + | TABLESPACE + | TARGET + | TEMP + | TEMPLATE + | TEMPORARY + | TEXT_P + | THEN + | TIES + | TIME + | TIMESTAMP + | TRAILING + | TRANSACTION + | TRANSFORM + | TREAT + | TRIGGER + | TRIM + | TRUE_P + | TRUNCATE + | TRUSTED + | TYPE_P + | TYPES_P + | UESCAPE + | UNBOUNDED + | UNCOMMITTED + | UNCONDITIONAL + | UNENCRYPTED + | UNIQUE + | UNKNOWN + | UNLISTEN + | UNLOGGED + | UNTIL + | UPDATE + | USER + | USING + | VACUUM + | VALID + | VALIDATE + | VALIDATOR + | VALUE_P + | VALUES + | VARCHAR + | VARIADIC + | VERBOSE + | VERSION_P + | VIEW + | VIEWS + | VOLATILE + | WHEN + | WHITESPACE_P + | WORK + | WRAPPER + | WRITE + | XML_P + | XMLATTRIBUTES + | XMLCONCAT + | XMLELEMENT + | XMLEXISTS + | XMLFOREST + | XMLNAMESPACES + | XMLPARSE + | XMLPI + | XMLROOT + | XMLSERIALIZE + | XMLTABLE + | YES_P + | ZONE + ; + + +any_identifier + : colid + ; + +identifier + : Identifier + | QuotedIdentifier + | UnicodeQuotedIdentifier uescape_? + // scim-sql local modification: PLSQLVARIABLENAME removed so :name is always a named + // parameter expression (see c_expr_namedparam), never an identifier, alias, or relation. + ; + diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java new file mode 100644 index 0000000..1b38d7b --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -0,0 +1,463 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import ai.singlr.postgresql.parser.PostgreSQLParser; +import ai.singlr.postgresql.parser.PostgreSQLParser.Alias_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.ColumnrefContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Common_table_exprContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.DeletestmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.For_locking_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_alias_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_applicationContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_expr_common_subexprContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_expr_windowlessContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_nameContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Func_tableContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Insert_column_itemContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Insert_targetContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.InsertstmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Into_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Locked_rels_listContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.MergestmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Over_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.PlsqlvariablenameContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Qualified_nameContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Qualified_name_listContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Relation_exprContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Relation_expr_opt_aliasContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.RootContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Select_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Select_no_parensContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Select_with_parensContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.SelectstmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Set_targetContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Simple_select_intersectContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Simple_select_pramaryContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.StmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Table_refContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Tablesample_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Target_starContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.UpdatestmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Values_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Window_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.With_clauseContext; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; + +final class AnalysisCollector { + + private static final Set DDL_RULES = + Set.of( + "definestmt", + "indexstmt", + "viewstmt", + "rulestmt", + "renamestmt", + "removeaggrstmt", + "removefuncstmt", + "removeoperstmt", + "commentstmt", + "seclabelstmt", + "grantstmt", + "revokestmt", + "grantrolestmt", + "revokerolestmt", + "importforeignschemastmt"); + + private final List relations = new ArrayList<>(); + private final List columns = new ArrayList<>(); + private final List functions = new ArrayList<>(); + private final Set parameters = new LinkedHashSet<>(); + private final EnumSet features = EnumSet.noneOf(QueryFeature.class); + + QueryAnalysis collect(RootContext root, String normalizedSql) { + List statements = root.stmtblock().stmtmulti().stmt(); + if (statements.isEmpty()) { + throw new QueryAnalysisException("no sql statement found", -1, -1); + } + if (statements.size() > 1) { + features.add(QueryFeature.MULTIPLE_STATEMENTS); + } + for (StmtContext statement : statements) { + scan(statement, Set.of()); + } + return new QueryAnalysis( + classify(statements.getFirst()), + statements.size(), + relations, + columns, + functions, + parameters, + features, + normalizedSql); + } + + private static StatementKind classify(StmtContext statement) { + if (!(statement.getChild(0) instanceof ParserRuleContext child)) { + return StatementKind.UNKNOWN; + } + String rule = PostgreSQLParser.ruleNames[child.getRuleIndex()]; + return switch (rule) { + case "selectstmt" -> StatementKind.SELECT; + case "insertstmt" -> StatementKind.INSERT; + case "updatestmt" -> StatementKind.UPDATE; + case "deletestmt" -> StatementKind.DELETE; + case "mergestmt" -> StatementKind.MERGE; + default -> + rule.startsWith("create") + || rule.startsWith("alter") + || rule.startsWith("drop") + || DDL_RULES.contains(rule) + ? StatementKind.DDL + : StatementKind.UTILITY; + }; + } + + private void scan(ParseTree node, Set cteScope) { + if (node instanceof TerminalNode) { + return; + } + With_clauseContext withClause = ownedWithClause(node); + if (withClause != null) { + scanWithOwner((ParserRuleContext) node, withClause, cteScope); + return; + } + inspect(node, cteScope); + for (int i = 0; i < node.getChildCount(); i++) { + scan(node.getChild(i), cteScope); + } + } + + private static With_clauseContext ownedWithClause(ParseTree node) { + return switch (node) { + case Select_no_parensContext c -> c.with_clause(); + case InsertstmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; + case UpdatestmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; + case DeletestmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; + default -> null; + }; + } + + private void scanWithOwner( + ParserRuleContext owner, With_clauseContext withClause, Set outerScope) { + features.add(QueryFeature.CTE); + boolean recursive = withClause.RECURSIVE() != null; + if (recursive) { + features.add(QueryFeature.RECURSIVE_CTE); + } + List ctes = withClause.cte_list().common_table_expr(); + List names = ctes.stream().map(cte -> identifierText(cte.name().colid())).toList(); + for (int i = 0; i < ctes.size(); i++) { + Common_table_exprContext cte = ctes.get(i); + if (cte.preparablestmt().selectstmt() == null) { + features.add(QueryFeature.WRITABLE_CTE); + } + Set bodyScope = union(outerScope, names.subList(0, recursive ? i + 1 : i)); + scan(cte.preparablestmt(), bodyScope); + } + Set fullScope = union(outerScope, names); + ParseTree skip = withClause; + while (skip.getParent() != owner) { + skip = (ParseTree) skip.getParent(); + } + for (int i = 0; i < owner.getChildCount(); i++) { + ParseTree child = owner.getChild(i); + if (child != skip) { + scan(child, fullScope); + } + } + } + + private static Set union(Set scope, List names) { + if (names.isEmpty()) { + return scope; + } + Set merged = new HashSet<>(scope); + merged.addAll(names); + return merged; + } + + private void inspect(ParseTree node, Set cteScope) { + switch (node) { + case Qualified_nameContext relation -> addRelation(relation, cteScope); + case ColumnrefContext column -> addColumn(column); + case Func_applicationContext call -> addFunction(call); + case Func_expr_common_subexprContext special -> + functions.add( + new FunctionReference( + null, + special.getStart().getText().toLowerCase(Locale.ROOT), + special.getStart().getLine(), + special.getStart().getCharPositionInLine())); + case PlsqlvariablenameContext parameter -> parameters.add(parameter.getText().substring(1)); + case Func_tableContext functionRelation -> addFunctionRelation(functionRelation); + case Target_starContext star -> { + features.add(QueryFeature.STAR_PROJECTION); + columns.add(new ColumnReference(null, "*")); + } + case Insert_column_itemContext item -> + columns.add(new ColumnReference(null, identifierText(item.colid()))); + case Set_targetContext target -> + columns.add(new ColumnReference(null, identifierText(target.colid()))); + case Select_with_parensContext subquery -> { + var parent = subquery.getParent(); + if (!(parent instanceof SelectstmtContext + || parent instanceof Select_with_parensContext + || parent instanceof Simple_select_pramaryContext)) { + features.add(QueryFeature.SUBQUERY); + } + } + case Select_clauseContext setOp -> { + if (setOp.simple_select_intersect().size() > 1) { + features.add(QueryFeature.SET_OPERATION); + } + } + case Simple_select_intersectContext intersect -> { + if (intersect.simple_select_pramary().size() > 1) { + features.add(QueryFeature.SET_OPERATION); + } + } + case Into_clauseContext into -> features.add(QueryFeature.SELECT_INTO); + case For_locking_clauseContext locking -> { + if (locking.for_locking_items() != null) { + features.add(QueryFeature.ROW_LOCK); + } + } + case Table_refContext tableRef -> { + if (tableRef.LATERAL_P() != null) { + features.add(QueryFeature.LATERAL); + } + } + case Values_clauseContext values -> { + if (hasTableRefAncestor(values)) { + features.add(QueryFeature.VALUES_RELATION); + } + } + case Over_clauseContext over -> features.add(QueryFeature.WINDOW); + case Window_clauseContext window -> features.add(QueryFeature.WINDOW); + default -> {} + } + } + + private void addRelation(Qualified_nameContext relation, Set cteScope) { + if (relation.getParent() instanceof Qualified_name_listContext list + && list.getParent() instanceof Locked_rels_listContext) { + return; + } + List parts = nameParts(relation); + String name = parts.removeLast(); + String schema = parts.isEmpty() ? null : String.join(".", parts); + RelationReference.Kind kind = + schema == null && cteScope.contains(name) + ? RelationReference.Kind.CTE + : RelationReference.Kind.PHYSICAL; + relations.add(new RelationReference(schema, name, aliasFor(relation), kind)); + } + + private void addColumn(ColumnrefContext column) { + List parts = new ArrayList<>(); + parts.add(identifierText(column.colid())); + boolean star = false; + if (column.indirection() != null) { + for (var element : column.indirection().indirection_el()) { + if (element.attr_name() != null) { + parts.add(identifierText(element.attr_name())); + } else if (element.STAR() != null) { + star = true; + break; + } else { + break; + } + } + } + if (star) { + features.add(QueryFeature.STAR_PROJECTION); + columns.add(new ColumnReference(String.join(".", parts), "*")); + } else { + String name = parts.removeLast(); + columns.add(new ColumnReference(parts.isEmpty() ? null : String.join(".", parts), name)); + } + } + + private void addFunction(Func_applicationContext call) { + List parts = funcNameParts(call.func_name()); + String name = parts.removeLast(); + functions.add( + new FunctionReference( + parts.isEmpty() ? null : String.join(".", parts), + name, + call.getStart().getLine(), + call.getStart().getCharPositionInLine())); + } + + private void addFunctionRelation(Func_tableContext functionRelation) { + features.add(QueryFeature.FUNCTION_RELATION); + String alias = funcTableAlias(functionRelation); + for (Func_expr_windowlessContext windowless : windowlessFunctions(functionRelation)) { + if (windowless.func_application() != null) { + List parts = funcNameParts(windowless.func_application().func_name()); + String name = parts.removeLast(); + relations.add( + new RelationReference( + parts.isEmpty() ? null : String.join(".", parts), + name, + alias, + RelationReference.Kind.FUNCTION)); + } else { + relations.add( + new RelationReference( + null, + windowless.getStart().getText().toLowerCase(Locale.ROOT), + alias, + RelationReference.Kind.FUNCTION)); + } + } + } + + private static List windowlessFunctions( + Func_tableContext functionRelation) { + if (functionRelation.func_expr_windowless() != null) { + return List.of(functionRelation.func_expr_windowless()); + } + return functionRelation.rowsfrom_list().rowsfrom_item().stream() + .map(PostgreSQLParser.Rowsfrom_itemContext::func_expr_windowless) + .toList(); + } + + private static String funcTableAlias(Func_tableContext functionRelation) { + if (!(functionRelation.getParent() instanceof Table_refContext tableRef)) { + return null; + } + Func_alias_clauseContext aliasClause = firstSiblingAlias(tableRef, functionRelation); + if (aliasClause == null) { + return null; + } + if (aliasClause.alias_clause() != null) { + return identifierText(aliasClause.alias_clause().colid()); + } + return aliasClause.colid() != null ? identifierText(aliasClause.colid()) : null; + } + + private static Func_alias_clauseContext firstSiblingAlias( + Table_refContext tableRef, ParseTree child) { + for (int i = indexOf(tableRef, child) + 1; i < tableRef.getChildCount(); i++) { + if (tableRef.getChild(i) instanceof Func_alias_clauseContext alias) { + return alias; + } + break; + } + return null; + } + + private static String aliasFor(Qualified_nameContext relation) { + var parent = relation.getParent(); + if (parent instanceof Insert_targetContext target) { + return target.colid() != null ? identifierText(target.colid()) : null; + } + if (parent instanceof MergestmtContext merge) { + return followingAlias(merge, relation); + } + if (parent instanceof Relation_exprContext relationExpr) { + var grandParent = relationExpr.getParent(); + if (grandParent instanceof Relation_expr_opt_aliasContext optAlias) { + return optAlias.colid() != null ? identifierText(optAlias.colid()) : null; + } + if (grandParent instanceof Table_refContext tableRef) { + return followingAlias(tableRef, relationExpr); + } + } + return null; + } + + private static String followingAlias(ParserRuleContext parent, ParseTree child) { + for (int i = indexOf(parent, child) + 1; i < parent.getChildCount(); i++) { + ParseTree sibling = parent.getChild(i); + if (sibling instanceof Alias_clauseContext alias) { + return identifierText(alias.colid()); + } + if (!(sibling instanceof Tablesample_clauseContext)) { + return null; + } + } + return null; + } + + private static int indexOf(ParserRuleContext parent, ParseTree child) { + for (int i = 0; i < parent.getChildCount(); i++) { + if (parent.getChild(i) == child) { + return i; + } + } + return -1; + } + + private static boolean hasTableRefAncestor(ParserRuleContext context) { + for (var ancestor = context.getParent(); ancestor != null; ancestor = ancestor.getParent()) { + if (ancestor instanceof Table_refContext) { + return true; + } + if (ancestor instanceof StmtContext) { + return false; + } + } + return false; + } + + private static List nameParts(Qualified_nameContext relation) { + List parts = new ArrayList<>(); + parts.add(identifierText(relation.colid())); + if (relation.indirection() != null) { + for (var element : relation.indirection().indirection_el()) { + if (element.attr_name() != null) { + parts.add(identifierText(element.attr_name())); + } else { + break; + } + } + } + return parts; + } + + private static List funcNameParts(Func_nameContext functionName) { + if (functionName.type_function_name() != null) { + List parts = new ArrayList<>(); + parts.add(identifierText(functionName.type_function_name())); + return parts; + } + List parts = new ArrayList<>(); + parts.add(identifierText(functionName.colid())); + for (var element : functionName.indirection().indirection_el()) { + if (element.attr_name() != null) { + parts.add(identifierText(element.attr_name())); + } else { + break; + } + } + return parts; + } + + private static String identifierText(ParserRuleContext identifier) { + String raw = identifier.getText(); + if (raw.length() >= 2 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') { + return raw.substring(1, raw.length() - 1).replace("\"\"", "\""); + } + if (raw.length() >= 4 + && (raw.startsWith("U&\"") || raw.startsWith("u&\"")) + && raw.charAt(raw.length() - 1) == '"') { + return raw.substring(3, raw.length() - 1).replace("\"\"", "\""); + } + return raw.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/ai/singlr/postgresql/ColumnReference.java b/src/main/java/ai/singlr/postgresql/ColumnReference.java new file mode 100644 index 0000000..c4fc4f6 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/ColumnReference.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import java.util.Objects; + +/** + * A syntactic reference to a column. + * + *

Unquoted identifiers are reported case-folded to lowercase, matching PostgreSQL semantics; + * quoted identifiers are reported exactly. Star usage is reported with {@code "*"} as the name. + * Ownership of unqualified columns is not resolved — a bare column name may belong to any relation + * in scope. + * + * @param qualifier dotted qualifier preceding the column name, or null when unqualified + * @param name the column name, or {@code "*"} for star usage + */ +public record ColumnReference(String qualifier, String name) { + + public ColumnReference { + Objects.requireNonNull(name, "name must not be null"); + } + + /** True when this reference is a star projection ({@code *} or {@code alias.*}). */ + public boolean star() { + return "*".equals(name); + } +} diff --git a/src/main/java/ai/singlr/postgresql/FunctionReference.java b/src/main/java/ai/singlr/postgresql/FunctionReference.java new file mode 100644 index 0000000..3914470 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/FunctionReference.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import java.util.Objects; + +/** + * A syntactic reference to a function call. + * + *

Unquoted identifiers are reported case-folded to lowercase, matching PostgreSQL semantics; + * quoted identifiers are reported exactly. Special-form functions ({@code CAST}, {@code COALESCE}, + * {@code EXTRACT}, {@code CURRENT_DATE}, and similar) are reported by their lowercase keyword with + * a null schema. + * + * @param schema dotted qualifier preceding the function name, or null when unqualified + * @param name the function name + * @param line 1-based line of the call site + * @param column 0-based column of the call site + */ +public record FunctionReference(String schema, String name, int line, int column) { + + public FunctionReference { + Objects.requireNonNull(name, "name must not be null"); + } +} diff --git a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java new file mode 100644 index 0000000..5393594 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import ai.singlr.postgresql.parser.PostgreSQLLexer; +import ai.singlr.postgresql.parser.PostgreSQLParser; +import java.util.Locale; +import java.util.Set; +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.misc.ParseCancellationException; + +/** + * Parses complete PostgreSQL statements and reports structural facts. + * + *

{@link #analyze(String)} parses the whole input through EOF — trailing garbage is a syntax + * error — and returns an immutable {@link QueryAnalysis}. Prohibited but syntactically valid + * statements (DDL, COPY, multiple statements, and so on) analyze successfully so callers can + * produce precise policy errors; only malformed or unsafe input is rejected with a {@link + * QueryAnalysisException}. + * + *

Input is bounded to {@value #MAX_LENGTH} characters, {@value #MAX_TOKENS} tokens, and {@value + * #MAX_NESTING_DEPTH} levels of bracket nesting, which together bound parse time and memory on + * hostile input. SQL text and literal content never appear in exception messages and are never + * logged. + */ +public final class PostgresQueryAnalyzer { + + /** Maximum accepted input length in characters. */ + public static final int MAX_LENGTH = 200_000; + + /** Maximum accepted number of lexed tokens. */ + public static final int MAX_TOKENS = 50_000; + + /** Maximum accepted parenthesis and bracket nesting depth. */ + public static final int MAX_NESTING_DEPTH = 128; + + private static final Set VERBATIM_TOKEN_TYPES = + Set.of( + PostgreSQLLexer.QuotedIdentifier, + PostgreSQLLexer.UnicodeQuotedIdentifier, + PostgreSQLLexer.StringConstant, + PostgreSQLLexer.UnicodeEscapeStringConstant, + PostgreSQLLexer.EscapeStringConstant, + PostgreSQLLexer.BinaryStringConstant, + PostgreSQLLexer.HexadecimalStringConstant, + PostgreSQLLexer.BeginDollarStringConstant, + PostgreSQLLexer.DollarText, + PostgreSQLLexer.EndDollarStringConstant, + PostgreSQLLexer.PLSQLVARIABLENAME, + PostgreSQLLexer.PLSQLIDENTIFIER, + PostgreSQLLexer.PARAM); + + private PostgresQueryAnalyzer() {} + + /** + * Analyzes a complete PostgreSQL SQL string. + * + * @param sql one or more complete SQL statements + * @return the structural analysis + * @throws QueryAnalysisException on null, blank, or oversized input, unrecognized tokens, syntax + * errors, excessive nesting, or when no statement is present + */ + public static QueryAnalysis analyze(String sql) { + if (sql == null || sql.isBlank()) { + throw new QueryAnalysisException("sql must not be null or blank", -1, -1); + } + if (sql.length() > MAX_LENGTH) { + throw new QueryAnalysisException("sql exceeds " + MAX_LENGTH + " characters", -1, -1); + } + try { + var tokens = lex(sql); + var root = parse(tokens); + return new AnalysisCollector().collect(root, normalize(tokens)); + } catch (StackOverflowError e) { + throw new QueryAnalysisException("sql nesting exceeds parser capacity", -1, -1); + } + } + + private static CommonTokenStream lex(String sql) { + var lexer = new PostgreSQLLexer(CharStreams.fromString(sql)); + lexer.removeErrorListeners(); + lexer.addErrorListener( + new BaseErrorListener() { + @Override + public void syntaxError( + Recognizer recognizer, + Object offendingSymbol, + int line, + int charPositionInLine, + String msg, + RecognitionException e) { + throw new QueryAnalysisException("unrecognized token", line, charPositionInLine); + } + }); + var tokens = new CommonTokenStream(lexer); + tokens.fill(); + checkBounds(tokens); + return tokens; + } + + private static void checkBounds(CommonTokenStream tokens) { + if (tokens.size() > MAX_TOKENS) { + throw new QueryAnalysisException("sql exceeds " + MAX_TOKENS + " tokens", -1, -1); + } + int depth = 0; + for (Token token : tokens.getTokens()) { + int type = token.getType(); + if (type == PostgreSQLLexer.OPEN_PAREN || type == PostgreSQLLexer.OPEN_BRACKET) { + depth++; + if (depth > MAX_NESTING_DEPTH) { + throw new QueryAnalysisException( + "sql exceeds nesting depth of " + MAX_NESTING_DEPTH, + token.getLine(), + token.getCharPositionInLine()); + } + } else if (type == PostgreSQLLexer.CLOSE_PAREN || type == PostgreSQLLexer.CLOSE_BRACKET) { + depth = Math.max(0, depth - 1); + } + } + } + + private static PostgreSQLParser.RootContext parse(CommonTokenStream tokens) { + var parser = new PostgreSQLParser(tokens); + parser.removeErrorListeners(); + parser.setErrorHandler(new BailErrorStrategy()); + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + try { + return parser.root(); + } catch (ParseCancellationException sllFailure) { + tokens.seek(0); + parser.reset(); + parser.getInterpreter().setPredictionMode(PredictionMode.LL); + try { + return parser.root(); + } catch (ParseCancellationException llFailure) { + throw syntaxError(llFailure); + } + } + } + + private static QueryAnalysisException syntaxError(ParseCancellationException failure) { + if (failure.getCause() instanceof RecognitionException recognition + && recognition.getOffendingToken() != null) { + var token = recognition.getOffendingToken(); + return new QueryAnalysisException( + "invalid sql syntax", token.getLine(), token.getCharPositionInLine()); + } + return new QueryAnalysisException("invalid sql syntax", -1, -1); + } + + private static String normalize(CommonTokenStream tokens) { + var normalized = new StringBuilder(); + int previousType = Token.INVALID_TYPE; + for (Token token : tokens.getTokens()) { + int type = token.getType(); + if (type == Token.EOF || token.getChannel() != Token.DEFAULT_CHANNEL) { + continue; + } + if (!normalized.isEmpty() && !insideDollarString(previousType, type)) { + normalized.append(isStringLiteral(previousType) && isStringLiteral(type) ? '\n' : ' '); + } + var text = token.getText(); + normalized.append(VERBATIM_TOKEN_TYPES.contains(type) ? text : text.toLowerCase(Locale.ROOT)); + previousType = type; + } + return normalized.toString(); + } + + private static boolean insideDollarString(int previousType, int type) { + return (previousType == PostgreSQLLexer.BeginDollarStringConstant + || previousType == PostgreSQLLexer.DollarText) + && (type == PostgreSQLLexer.DollarText || type == PostgreSQLLexer.EndDollarStringConstant); + } + + private static boolean isStringLiteral(int type) { + return type == PostgreSQLLexer.StringConstant + || type == PostgreSQLLexer.EscapeStringConstant + || type == PostgreSQLLexer.UnicodeEscapeStringConstant + || type == PostgreSQLLexer.BinaryStringConstant + || type == PostgreSQLLexer.HexadecimalStringConstant; + } +} diff --git a/src/main/java/ai/singlr/postgresql/QueryAnalysis.java b/src/main/java/ai/singlr/postgresql/QueryAnalysis.java new file mode 100644 index 0000000..952f506 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/QueryAnalysis.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Immutable structural description of an analyzed SQL string. + * + *

When the input holds multiple statements, {@link #statementKind()} describes the first + * statement, {@link QueryFeature#MULTIPLE_STATEMENTS} is set, and references and features are + * aggregated across all statements. + * + * @param statementKind classification of the first statement + * @param statementCount number of statements; semicolons inside strings, dollar-quoted values, and + * comments do not create statements + * @param relations every syntactically reachable relation reference, in source order + * @param columns every syntactically reachable column reference, including star usage + * @param functions every syntactically reachable function call, in source order + * @param parameters deduplicated named-parameter names, without the leading colon, exactly as + * written + * @param features policy-relevant syntactic constructs detected at any depth + * @param normalizedSql deterministic single-line form, stable across whitespace, comments, and + * keyword or unquoted-identifier case, suitable for hashing and audit comparison + */ +public record QueryAnalysis( + StatementKind statementKind, + int statementCount, + List relations, + List columns, + List functions, + Set parameters, + Set features, + String normalizedSql) { + + public QueryAnalysis { + Objects.requireNonNull(statementKind, "statementKind must not be null"); + Objects.requireNonNull(normalizedSql, "normalizedSql must not be null"); + if (statementCount < 1) { + throw new IllegalArgumentException("statementCount must be at least 1"); + } + relations = List.copyOf(relations); + columns = List.copyOf(columns); + functions = List.copyOf(functions); + parameters = Collections.unmodifiableSet(new LinkedHashSet<>(parameters)); + features = + features.isEmpty() ? Set.of() : Collections.unmodifiableSet(EnumSet.copyOf(features)); + } +} diff --git a/src/main/java/ai/singlr/postgresql/QueryAnalysisException.java b/src/main/java/ai/singlr/postgresql/QueryAnalysisException.java new file mode 100644 index 0000000..f61deaa --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/QueryAnalysisException.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +/** + * Thrown when SQL cannot be analyzed: null, blank, or oversized input, unrecognized tokens, syntax + * errors, excessive nesting, or an empty statement list. + * + *

The message carries only a stable reason and, when available, a line and column. It never + * contains SQL text, literals, or parser internals, so it is safe to log and to return to callers. + */ +public class QueryAnalysisException extends RuntimeException { + + private final String reason; + private final int line; + private final int column; + + public QueryAnalysisException(String reason, int line, int column) { + super(line >= 1 ? "%s at line %d, column %d".formatted(reason, line, column) : reason); + this.reason = reason; + this.line = line; + this.column = column; + } + + /** Stable, content-free description of the failure. */ + public String reason() { + return reason; + } + + /** 1-based line of the failure, or -1 when not applicable. */ + public int line() { + return line; + } + + /** 0-based column of the failure, or -1 when not applicable. */ + public int column() { + return column; + } +} diff --git a/src/main/java/ai/singlr/postgresql/QueryFeature.java b/src/main/java/ai/singlr/postgresql/QueryFeature.java new file mode 100644 index 0000000..ccbc126 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/QueryFeature.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +/** Syntactic constructs a policy layer may need to distinguish, detected at any nesting depth. */ +public enum QueryFeature { + /** A {@code WITH} clause is present. */ + CTE, + /** A {@code WITH RECURSIVE} clause is present. */ + RECURSIVE_CTE, + /** A CTE body is a non-SELECT statement (INSERT, UPDATE, or DELETE). */ + WRITABLE_CTE, + /** A parenthesized SELECT is used as an expression, derived table, or set-returning source. */ + SUBQUERY, + /** {@code UNION}, {@code INTERSECT}, or {@code EXCEPT} combines query branches. */ + SET_OPERATION, + /** A window function {@code OVER} clause or a {@code WINDOW} definition is present. */ + WINDOW, + /** {@code SELECT ... INTO} creates a table from the result set. */ + SELECT_INTO, + /** A row-locking clause such as {@code FOR UPDATE} or {@code FOR SHARE} is present. */ + ROW_LOCK, + /** A {@code LATERAL} relation is present in a FROM clause. */ + LATERAL, + /** A set-returning function is used as a relation in a FROM clause. */ + FUNCTION_RELATION, + /** A star projection ({@code *} or {@code alias.*}) is present. */ + STAR_PROJECTION, + /** A {@code VALUES} list is used as a relation in a FROM clause. */ + VALUES_RELATION, + /** The input contains more than one statement. */ + MULTIPLE_STATEMENTS +} diff --git a/src/main/java/ai/singlr/postgresql/RelationReference.java b/src/main/java/ai/singlr/postgresql/RelationReference.java new file mode 100644 index 0000000..00b2e18 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/RelationReference.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import java.util.Objects; + +/** + * A syntactic reference to a relation. + * + *

Unquoted identifiers are reported case-folded to lowercase, matching PostgreSQL semantics; + * quoted identifiers are reported exactly. An unqualified name that matches an in-scope CTE name is + * reported as {@link Kind#CTE}; classification is purely syntactic and does not resolve catalog + * objects. + * + * @param schema dotted qualifier preceding the relation name, or null when unqualified + * @param name the relation, CTE, or function name + * @param alias the alias bound to this relation, or null + * @param kind how the relation is referenced + */ +public record RelationReference(String schema, String name, String alias, Kind kind) { + + /** How a relation is referenced. */ + public enum Kind { + /** A schema-qualified or unqualified physical relation name. */ + PHYSICAL, + /** An unqualified name matching a common table expression in scope. */ + CTE, + /** A set-returning function used as a relation. */ + FUNCTION + } + + public RelationReference { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(kind, "kind must not be null"); + } +} diff --git a/src/main/java/ai/singlr/postgresql/StatementKind.java b/src/main/java/ai/singlr/postgresql/StatementKind.java new file mode 100644 index 0000000..058e710 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/StatementKind.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +/** + * Coarse classification of the first statement in an analyzed SQL string. + * + *

{@code CREATE}, {@code ALTER}, {@code DROP}, and other schema- or privilege-changing + * statements (including {@code GRANT}/{@code REVOKE}, {@code COMMENT ON}, and {@code CREATE INDEX}) + * classify as {@link #DDL}. Everything else that is not DML — {@code COPY}, {@code CALL}, {@code + * DO}, {@code SET}, {@code SHOW}, {@code EXPLAIN}, {@code TRUNCATE}, transaction control, cursors, + * and similar commands — classifies as {@link #UTILITY}. + */ +public enum StatementKind { + SELECT, + INSERT, + UPDATE, + DELETE, + MERGE, + DDL, + UTILITY, + UNKNOWN +} diff --git a/src/main/java/ai/singlr/postgresql/package-info.java b/src/main/java/ai/singlr/postgresql/package-info.java new file mode 100644 index 0000000..4960d30 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +/** + * Syntactic analysis of complete PostgreSQL statements. + * + *

{@link ai.singlr.postgresql.PostgresQueryAnalyzer#analyze(String)} parses a full SQL string + * through EOF and returns an immutable {@link ai.singlr.postgresql.QueryAnalysis} describing what + * the SQL is: statement kind and count, every syntactically reachable relation, column, + * and function reference, named parameters ({@code :name}), policy-relevant features, and a + * deterministic normalized form suitable for hashing and audit comparison. + * + *

This package parses and describes SQL. It does not execute SQL, resolve catalog objects, + * authorize access, or decide query cost — callers own policy. Analysis is purely syntactic: + * relations that look like in-scope CTE names are reported as CTE references, everything else as + * physical or function relations, without pretending to resolve database objects. + * + *

The grammar is the ANTLR grammars-v4 PostgreSQL grammar, vendored at a pinned upstream commit + * with one deliberate extension: named parameters such as {@code :start_at} are first-class + * expression values. See the repository NOTICE.md for provenance and the exact local modifications. + */ +package ai.singlr.postgresql; diff --git a/src/main/java/ai/singlr/postgresql/parser/LexerDispatchingErrorListener.java b/src/main/java/ai/singlr/postgresql/parser/LexerDispatchingErrorListener.java new file mode 100644 index 0000000..89dff45 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/parser/LexerDispatchingErrorListener.java @@ -0,0 +1,79 @@ +/* +PostgreSQL grammar. +The MIT License (MIT). +Copyright (c) 2021-2023, Oleksii Kovalov (Oleksii.Kovalov@outlook.com). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// scim-sql local modification: added package declaration (upstream files have no package). +package ai.singlr.postgresql.parser; + +import java.util.BitSet; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.atn.*; +import org.antlr.v4.runtime.dfa.*; +import org.antlr.v4.runtime.misc.*; + +public class LexerDispatchingErrorListener implements ANTLRErrorListener +{ + Lexer _parent; + + public LexerDispatchingErrorListener(Lexer parent) + { + _parent = parent; + } + + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); + } + + public void reportAmbiguity(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + boolean exact, + BitSet ambigAlts, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportAmbiguity(recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs); + } + + public void reportAttemptingFullContext(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + BitSet conflictingAlts, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportAttemptingFullContext(recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs); + } + + public void reportContextSensitivity(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + int prediction, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportContextSensitivity(recognizer, dfa, startIndex, stopIndex, prediction, configs); + } +} diff --git a/src/main/java/ai/singlr/postgresql/parser/ParserDispatchingErrorListener.java b/src/main/java/ai/singlr/postgresql/parser/ParserDispatchingErrorListener.java new file mode 100644 index 0000000..ae1b4f4 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/parser/ParserDispatchingErrorListener.java @@ -0,0 +1,79 @@ +/* +PostgreSQL grammar. +The MIT License (MIT). +Copyright (c) 2021-2023, Oleksii Kovalov (Oleksii.Kovalov@outlook.com). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// scim-sql local modification: added package declaration (upstream files have no package). +package ai.singlr.postgresql.parser; + +import java.util.BitSet; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.atn.*; +import org.antlr.v4.runtime.dfa.*; +import org.antlr.v4.runtime.misc.*; + +public class ParserDispatchingErrorListener implements ANTLRErrorListener +{ + Parser _parent; + + public ParserDispatchingErrorListener(Parser parent) + { + _parent = parent; + } + + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) + { + var foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); + } + + public void reportAmbiguity(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + boolean exact, + BitSet ambigAlts, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportAmbiguity(recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs); + } + + public void reportAttemptingFullContext(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + BitSet conflictingAlts, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportAttemptingFullContext(recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs); + } + + public void reportContextSensitivity(Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + int prediction, + ATNConfigSet configs) + { + ProxyErrorListener foo = new ProxyErrorListener(_parent.getErrorListeners()); + foo.reportContextSensitivity(recognizer, dfa, startIndex, stopIndex, prediction, configs); + } +} diff --git a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java new file mode 100644 index 0000000..779d0ea --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java @@ -0,0 +1,92 @@ +/* +PostgreSQL grammar. +The MIT License (MIT). +Copyright (c) 2021-2023, Oleksii Kovalov (Oleksii.Kovalov@outlook.com). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// scim-sql local modification: added package declaration (upstream files have no package). +package ai.singlr.postgresql.parser; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Lexer; + +import java.util.Stack; + +public abstract class PostgreSQLLexerBase extends Lexer { + protected final Stack tags = new Stack<>(); + + protected PostgreSQLLexerBase(CharStream input) { + super(input); + + } + + public void PushTag() { + tags.push(getText()); + } + + public boolean IsTag() { + return getText().equals(tags.peek()); + } + + public void PopTag() { + tags.pop(); + } + + public void UnterminatedBlockCommentDebugAssert() { + //Debug.Assert(InputStream.LA(1) == -1 /*EOF*/); + } + + public boolean CheckLaMinus() { + return getInputStream().LA(1) != '-'; + } + + public boolean CheckLaStar() { + return getInputStream().LA(1) != '*'; + } + + public boolean CharIsLetter() { + return Character.isLetter(getInputStream().LA(-1)); + } + + public void HandleNumericFail() { + getInputStream().seek(getInputStream().index() - 2); + setType(PostgreSQLLexer.Integral); + } + + public void HandleLessLessGreaterGreater() { + if (getText() == "<<") setType(PostgreSQLLexer.LESS_LESS); + if (getText() == ">>") setType(PostgreSQLLexer.GREATER_GREATER); + } + + public boolean CheckIfUtf32Letter() { + int codePoint = getInputStream().LA(-2) << 8 + getInputStream().LA(-1); + char[] c; + if (codePoint < 0x10000) { + c = new char[]{(char) codePoint}; + } else { + codePoint -= 0x10000; + c = new char[]{(char) (codePoint / 0x400 + 0xd800), (char) (codePoint % 0x400 + 0xdc00)}; + } + return Character.isLetter(c[0]); + } + + public boolean IsSemiColon() + { + return ';' == (char)getInputStream().LA(1); + } +} diff --git a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java new file mode 100644 index 0000000..dc0b557 --- /dev/null +++ b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java @@ -0,0 +1,138 @@ +/* +PostgreSQL grammar. +The MIT License (MIT). +Copyright (c) 2021-2023, Oleksii Kovalov (Oleksii.Kovalov@outlook.com). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// scim-sql local modification: added package declaration (upstream files have no package). +package ai.singlr.postgresql.parser; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.antlr.v4.runtime.*; + +public abstract class PostgreSQLParserBase extends Parser { + + public PostgreSQLParserBase(TokenStream input) { + super(input); + } + + ParserRuleContext GetParsedSqlTree(String script, int line) { + PostgreSQLParser ph = GetPostgreSQLParser(script); + ParserRuleContext result = ph.root(); + return result; + } + + public void ParseRoutineBody() { + PostgreSQLParser.Createfunc_opt_listContext _localctx = (PostgreSQLParser.Createfunc_opt_listContext) this.getContext(); + String lang = null; + for (PostgreSQLParser.Createfunc_opt_itemContext coi : _localctx.createfunc_opt_item()) { + if (coi.LANGUAGE() != null) { + if (coi.nonreservedword_or_sconst() != null) + if (coi.nonreservedword_or_sconst().nonreservedword() != null) + if (coi.nonreservedword_or_sconst().nonreservedword().identifier() != null) + if (coi.nonreservedword_or_sconst().nonreservedword().identifier() + .Identifier() != null) { + lang = coi.nonreservedword_or_sconst().nonreservedword().identifier() + .Identifier().getText(); + break; + } + } + } + if (null == lang) return; + PostgreSQLParser.Createfunc_opt_itemContext func_as = null; + for (PostgreSQLParser.Createfunc_opt_itemContext a : _localctx.createfunc_opt_item()) { + if (a.func_as() != null) { + func_as = a; + break; + + } + + } + if (func_as != null) { + String txt = GetRoutineBodyString(func_as.func_as().sconst(0)); + switch (lang) { + case "plpgsql": + //NB: Cannot be done this way. + //PostgreSQLParser ph = GetPostgreSQLParser(txt); + //func_as.func_as().Definition = ph.plsqlroot(); + break; + case "sql": + //func_as.func_as().Definition = ph.root(); + break; + } + } + } + + private String TrimQuotes(String s) { + return (s == null || s.isEmpty()) ? s : s.substring(1, s.length() - 1); + } + + public String unquote(String s) { + int slength = s.length(); + StringBuilder r = new StringBuilder(slength); + int i = 0; + while (i < slength) { + Character c = s.charAt(i); + r.append(c); + if (c == '\'' && i < slength - 1 && (s.charAt(i + 1) == '\'')) i++; + i++; + } + return r.toString(); + } + + public String GetRoutineBodyString(PostgreSQLParser.SconstContext rule) { + PostgreSQLParser.AnysconstContext anysconst = rule.anysconst(); + org.antlr.v4.runtime.tree.TerminalNode StringConstant = anysconst.StringConstant(); + if (null != StringConstant) return unquote(TrimQuotes(StringConstant.getText())); + org.antlr.v4.runtime.tree.TerminalNode UnicodeEscapeStringConstant = anysconst.UnicodeEscapeStringConstant(); + if (null != UnicodeEscapeStringConstant) return TrimQuotes(UnicodeEscapeStringConstant.getText()); + org.antlr.v4.runtime.tree.TerminalNode EscapeStringConstant = anysconst.EscapeStringConstant(); + if (null != EscapeStringConstant) return TrimQuotes(EscapeStringConstant.getText()); + String result = ""; + List dollartext = anysconst.DollarText(); + for (org.antlr.v4.runtime.tree.TerminalNode s : dollartext) { + result += s.getText(); + } + return result; + } + + public PostgreSQLParser GetPostgreSQLParser(String script) { + CharStream charStream = CharStreams.fromString(script); + Lexer lexer = new PostgreSQLLexer(charStream); + CommonTokenStream tokens = new CommonTokenStream(lexer); + PostgreSQLParser parser = new PostgreSQLParser(tokens); + lexer.removeErrorListeners(); + parser.removeErrorListeners(); + LexerDispatchingErrorListener listener_lexer = new LexerDispatchingErrorListener((Lexer)(((CommonTokenStream)(this.getInputStream())).getTokenSource())); + ParserDispatchingErrorListener listener_parser = new ParserDispatchingErrorListener(this); + lexer.addErrorListener(listener_lexer); + parser.addErrorListener(listener_parser); + return parser; + } + + public boolean OnlyAcceptableOps() + { + var c = ((CommonTokenStream)this.getInputStream()).LT(1); + var text = c.getText(); + return text.equals("!") || text.equals("!!") + || text.equals("!=-") + ; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 027721d..612f523 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -4,15 +4,22 @@ */ /** - * SCIM filter expression to parameterized SQL converter. + * SCIM filter expression to parameterized SQL converter and PostgreSQL query analyzer. * - *

Parses SCIM filtering - * expressions and converts them to parameterized SQL WHERE clauses. Supports all SCIM comparison - * operators (eq, ne, gt, lt, ge, le, co, sw, ew), logical operators (and, or, not), presence (pr), - * and the in operator with typed values (UUID, timestamp, JSON, boolean, number, string). + *

The {@code ai.singlr.scimsql} package parses SCIM filtering expressions and + * converts them to parameterized SQL WHERE clauses. Supports all SCIM comparison operators (eq, ne, + * gt, lt, ge, le, co, sw, ew), logical operators (and, or, not), presence (pr), and the in operator + * with typed values (UUID, timestamp, JSON, boolean, number, string). + * + *

The {@code ai.singlr.postgresql} package parses complete PostgreSQL statements and reports + * structural facts — statement kind and count, referenced relations, columns, functions, named + * parameters, and policy-relevant syntactic features — without executing SQL or resolving catalog + * objects. */ module ai.singlr.scimsql { requires org.antlr.antlr4.runtime; exports ai.singlr.scimsql; + exports ai.singlr.postgresql; } diff --git a/src/test/java/ai/singlr/postgresql/FeatureDetectionTest.java b/src/test/java/ai/singlr/postgresql/FeatureDetectionTest.java new file mode 100644 index 0000000..cd05a53 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/FeatureDetectionTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Feature detection at any depth") +class FeatureDetectionTest { + + private static void assertFeature(String sql, QueryFeature feature) { + var analysis = PostgresQueryAnalyzer.analyze(sql); + assertTrue(analysis.features().contains(feature), feature + " expected for: " + sql); + } + + private static void assertNoFeature(String sql, QueryFeature feature) { + var analysis = PostgresQueryAnalyzer.analyze(sql); + assertFalse(analysis.features().contains(feature), feature + " unexpected for: " + sql); + } + + @Test + @DisplayName("writable CTE is flagged for insert, update and delete bodies") + void shouldFlagWritableCte() { + assertFeature( + "WITH gone AS (DELETE FROM sessions WHERE expired RETURNING id) SELECT * FROM gone", + QueryFeature.WRITABLE_CTE); + assertFeature( + "WITH ins AS (INSERT INTO t VALUES (1) RETURNING id) SELECT * FROM ins", + QueryFeature.WRITABLE_CTE); + assertFeature( + "WITH upd AS (UPDATE t SET a = 1 RETURNING id) SELECT * FROM upd", + QueryFeature.WRITABLE_CTE); + assertNoFeature("WITH ro AS (SELECT 1) SELECT * FROM ro", QueryFeature.WRITABLE_CTE); + } + + @Test + @DisplayName("writable CTE is flagged when nested deep inside a subquery") + void shouldFlagNestedWritableCte() { + assertFeature( + "SELECT * FROM t WHERE x IN (" + + "SELECT y FROM (" + + "WITH w AS (DELETE FROM inner_t RETURNING y) SELECT y FROM w) sub)", + QueryFeature.WRITABLE_CTE); + } + + @Test + @DisplayName("select into is flagged") + void shouldFlagSelectInto() { + assertFeature("SELECT * INTO backup_users FROM users", QueryFeature.SELECT_INTO); + } + + @Test + @DisplayName("row locking clauses are flagged") + void shouldFlagRowLocks() { + assertFeature("SELECT * FROM t FOR UPDATE", QueryFeature.ROW_LOCK); + assertFeature("SELECT * FROM t FOR NO KEY UPDATE", QueryFeature.ROW_LOCK); + assertFeature("SELECT * FROM t FOR SHARE NOWAIT", QueryFeature.ROW_LOCK); + assertFeature("SELECT * FROM t FOR KEY SHARE SKIP LOCKED", QueryFeature.ROW_LOCK); + assertNoFeature("SELECT * FROM t FOR READ ONLY", QueryFeature.ROW_LOCK); + } + + @Test + @DisplayName("row lock nested in a subquery is flagged") + void shouldFlagNestedRowLock() { + assertFeature( + "SELECT * FROM outer_t WHERE id IN (SELECT id FROM inner_t FOR UPDATE)", + QueryFeature.ROW_LOCK); + } + + @Test + @DisplayName("lateral relations are flagged") + void shouldFlagLateral() { + assertFeature( + "SELECT * FROM users u, LATERAL (SELECT * FROM orders o WHERE o.user_id = u.id) x", + QueryFeature.LATERAL); + assertFeature( + "SELECT * FROM users u JOIN LATERAL generate_series(1, u.n) g ON true", + QueryFeature.LATERAL); + } + + @Test + @DisplayName("function relations are flagged and reported") + void shouldFlagFunctionRelation() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT * FROM generate_series(1, 10) AS g(n)"); + + assertTrue(analysis.features().contains(QueryFeature.FUNCTION_RELATION)); + assertEquals( + List.of( + new RelationReference(null, "generate_series", "g", RelationReference.Kind.FUNCTION)), + analysis.relations()); + } + + @Test + @DisplayName("schema-qualified function relation keeps schema") + void shouldFlagQualifiedFunctionRelation() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT * FROM pg_catalog.pg_ls_dir('.') d"); + + assertEquals( + List.of( + new RelationReference("pg_catalog", "pg_ls_dir", "d", RelationReference.Kind.FUNCTION)), + analysis.relations()); + } + + @Test + @DisplayName("rows from lists every function relation") + void shouldFlagRowsFrom() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT * FROM ROWS FROM (generate_series(1, 2), generate_series(3, 4)) AS t(a, b)"); + + assertTrue(analysis.features().contains(QueryFeature.FUNCTION_RELATION)); + assertEquals(2, analysis.relations().size()); + assertEquals(RelationReference.Kind.FUNCTION, analysis.relations().getFirst().kind()); + } + + @Test + @DisplayName("values as a relation is flagged, insert values is not") + void shouldFlagValuesRelation() { + assertFeature( + "SELECT * FROM (VALUES (1, 'a'), (2, 'b')) AS v(id, label)", QueryFeature.VALUES_RELATION); + assertNoFeature("INSERT INTO t VALUES (1, 'a')", QueryFeature.VALUES_RELATION); + } + + @Test + @DisplayName("star projections are flagged anywhere") + void shouldFlagStarProjection() { + assertFeature("SELECT * FROM t", QueryFeature.STAR_PROJECTION); + assertFeature("SELECT t.* FROM t", QueryFeature.STAR_PROJECTION); + assertFeature( + "WITH d AS (DELETE FROM t RETURNING *) SELECT 1 FROM d", QueryFeature.STAR_PROJECTION); + assertFeature("SELECT 1 FROM t WHERE x IN (SELECT * FROM s)", QueryFeature.STAR_PROJECTION); + assertNoFeature("SELECT count(*) FROM t", QueryFeature.STAR_PROJECTION); + } + + @Test + @DisplayName("subqueries are flagged, plain selects are not") + void shouldFlagSubquery() { + assertFeature("SELECT (SELECT max(id) FROM t) AS m", QueryFeature.SUBQUERY); + assertFeature("SELECT * FROM (SELECT 1) sub", QueryFeature.SUBQUERY); + assertFeature("SELECT * FROM t WHERE id IN (SELECT id FROM s)", QueryFeature.SUBQUERY); + assertNoFeature("SELECT 1", QueryFeature.SUBQUERY); + assertNoFeature("(SELECT 1)", QueryFeature.SUBQUERY); + assertNoFeature("WITH a AS (SELECT 1) SELECT * FROM a", QueryFeature.SUBQUERY); + } + + @Test + @DisplayName("set operations nested in subqueries are flagged") + void shouldFlagNestedSetOperation() { + assertFeature( + "SELECT * FROM t WHERE id IN (SELECT id FROM a UNION SELECT id FROM b)", + QueryFeature.SET_OPERATION); + } + + @Test + @DisplayName("multiple statements are counted and flagged") + void shouldFlagMultipleStatements() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT 1; SELECT * FROM t; DELETE FROM t"); + + assertEquals(3, analysis.statementCount()); + assertEquals(StatementKind.SELECT, analysis.statementKind()); + assertTrue(analysis.features().contains(QueryFeature.MULTIPLE_STATEMENTS)); + assertEquals(2, analysis.relations().size()); + } + + @Test + @DisplayName("cte flags are detected inside insert, update and delete statements") + void shouldFlagCteOnDml() { + assertFeature( + "WITH src AS (SELECT * FROM staging) INSERT INTO t SELECT * FROM src", QueryFeature.CTE); + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH src AS (SELECT id FROM staging) UPDATE t SET a = 1" + + " WHERE id IN (SELECT id FROM src)"); + assertTrue(analysis.features().contains(QueryFeature.CTE)); + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("staging:PHYSICAL", "t:PHYSICAL", "src:CTE"), kinds); + } +} diff --git a/src/test/java/ai/singlr/postgresql/GrammarWarningsTest.java b/src/test/java/ai/singlr/postgresql/GrammarWarningsTest.java new file mode 100644 index 0000000..00e9833 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/GrammarWarningsTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.antlr.v4.Tool; +import org.antlr.v4.tool.ANTLRMessage; +import org.antlr.v4.tool.ANTLRToolListener; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Pins the expected ANTLR warnings for the vendored PostgreSQL grammar. Upstream ships two benign + * warning-146 lexer rules; any new warning or error after a grammar upgrade or local modification + * must be reviewed and either fixed or pinned here deliberately. + */ +@DisplayName("Vendored grammar warnings are pinned") +class GrammarWarningsTest { + + private static final Path GRAMMAR_DIR = + Path.of("src", "main", "antlr4", "ai", "singlr", "postgresql", "parser"); + + private static final List EXPECTED_WARNINGS = + List.of( + "146:AfterEscapeStringConstantMode_NotContinued", + "146:AfterEscapeStringConstantWithNewlineMode_NotContinued"); + + @Test + @DisplayName("lexer and parser grammars produce exactly the pinned warnings") + void shouldMatchPinnedWarnings(@TempDir Path outputDir) throws Exception { + assertTrue(Files.exists(GRAMMAR_DIR.resolve("PostgreSQLLexer.g4"))); + for (var grammar : List.of("PostgreSQLLexer.g4", "PostgreSQLParser.g4")) { + Files.copy(GRAMMAR_DIR.resolve(grammar), outputDir.resolve(grammar)); + } + + var warnings = new ArrayList(); + var errors = new ArrayList(); + + runTool(outputDir, warnings, errors, "PostgreSQLLexer.g4"); + runTool(outputDir, warnings, errors, "PostgreSQLParser.g4"); + + assertEquals(List.of(), errors); + assertEquals(EXPECTED_WARNINGS, warnings); + } + + private static void runTool( + Path outputDir, List warnings, List errors, String grammar) { + var tool = + new Tool( + new String[] { + "-o", + outputDir.toString(), + "-lib", + outputDir.toString(), + outputDir.resolve(grammar).toString() + }); + tool.removeListeners(); + tool.addListener( + new ANTLRToolListener() { + @Override + public void info(String msg) {} + + @Override + public void error(ANTLRMessage msg) { + errors.add(render(msg)); + } + + @Override + public void warning(ANTLRMessage msg) { + warnings.add(render(msg)); + } + }); + tool.processGrammarsOnCommandLine(); + } + + private static String render(ANTLRMessage msg) { + var args = msg.getArgs(); + return msg.getErrorType().code + (args.length > 0 ? ":" + args[0] : ""); + } +} diff --git a/src/test/java/ai/singlr/postgresql/HostileInputTest.java b/src/test/java/ai/singlr/postgresql/HostileInputTest.java new file mode 100644 index 0000000..6583a21 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/HostileInputTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Hostile and malformed input") +class HostileInputTest { + + @Test + @DisplayName("null and blank input are rejected") + void shouldRejectNullAndBlank() { + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(null)); + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("")); + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(" \n\t")); + } + + @Test + @DisplayName("oversized input is rejected before parsing") + void shouldRejectOversizedInput() { + var oversized = "SELECT " + "1,".repeat(PostgresQueryAnalyzer.MAX_LENGTH / 2) + "1"; + var exception = + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(oversized)); + assertTrue(exception.reason().contains("characters")); + } + + @Test + @DisplayName("token floods are rejected") + void shouldRejectTokenFlood() { + var flood = "SELECT " + "1,".repeat(60_000) + "1"; + assertTrue(flood.length() <= PostgresQueryAnalyzer.MAX_LENGTH); + var exception = + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(flood)); + assertTrue(exception.reason().contains("tokens")); + } + + @Test + @DisplayName("deep nesting is rejected deterministically and quickly") + void shouldRejectDeepNesting() { + var depth = PostgresQueryAnalyzer.MAX_NESTING_DEPTH + 1; + var nested = "SELECT " + "(".repeat(depth) + "1" + ")".repeat(depth); + assertTimeoutPreemptively( + Duration.ofSeconds(10), + () -> { + var exception = + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(nested)); + assertTrue(exception.reason().contains("nesting")); + }); + } + + @Test + @DisplayName("nesting at the limit parses") + void shouldParseModerateNesting() { + var nested = "SELECT " + "(".repeat(40) + "1" + ")".repeat(40); + assertEquals(1, PostgresQueryAnalyzer.analyze(nested).statementCount()); + } + + @Test + @DisplayName("semicolons inside every string form do not create statements") + void shouldIgnoreEmbeddedSemicolons() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT 'a;b', e'c;d', $$e;f$$, $tag$g;h$tag$, \"col;name\" FROM t" + + " -- trailing ; comment\n /* block ; comment */"); + + assertEquals(1, analysis.statementCount()); + } + + @Test + @DisplayName("trailing semicolons do not add statements") + void shouldIgnoreTrailingSemicolons() { + assertEquals(1, PostgresQueryAnalyzer.analyze("SELECT 1;").statementCount()); + assertEquals(1, PostgresQueryAnalyzer.analyze("SELECT 1;;;").statementCount()); + } + + @Test + @DisplayName("only semicolons or comments is rejected as no statement") + void shouldRejectEmptyStatementList() { + for (var sql : new String[] {";;", "-- just a comment", "/* nothing */"}) { + var exception = + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(sql)); + assertTrue( + exception.reason().contains("no sql statement") || exception.reason().contains("blank"), + sql + " -> " + exception.reason()); + } + } + + @Test + @DisplayName("trailing garbage is rejected with a position") + void shouldRejectTrailingGarbage() { + var exception = + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT 1 FROM t klaatu barada nikto")); + assertEquals("invalid sql syntax", exception.reason()); + assertEquals(1, exception.line()); + } + + @Test + @DisplayName("syntax errors never leak sql content") + void shouldNotLeakSqlContent() { + var secret = "SuperSecretLiteral12345"; + var exception = + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT WHERE '" + secret + "' &&& %%%")); + assertFalse(exception.getMessage().contains(secret)); + assertFalse(String.valueOf(exception.reason()).contains(secret)); + } + + @Test + @DisplayName("unterminated strings and dollar quotes fail cleanly") + void shouldRejectUnterminatedLiterals() { + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 'abc")); + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT $tag$abc")); + } + + @Test + @DisplayName("unicode and exotic identifiers parse") + void shouldParseUnicodeIdentifiers() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT \"名前\", \"a\"\"b\" FROM \"таблица\""); + + assertEquals("таблица", analysis.relations().getFirst().name()); + assertEquals("名前", analysis.columns().getFirst().name()); + assertEquals("a\"b", analysis.columns().get(1).name()); + } + + @Test + @DisplayName("nested block comments are handled") + void shouldHandleNestedBlockComments() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT 1 /* outer /* inner ; */ still outer */"); + + assertEquals(1, analysis.statementCount()); + } + + @Test + @DisplayName("dollar tag confusion does not break statement counting") + void shouldHandleDollarTagTricks() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT $a$ $b$ ; $a$, $b$x$b$ FROM t"); + + assertEquals(1, analysis.statementCount()); + } + + @Test + @DisplayName("hostile inputs complete within the time budget") + void shouldStayWithinTimeBudget() { + var wide = "SELECT " + "abs(x) + ".repeat(2_000) + "1 FROM t"; + assertTimeoutPreemptively( + Duration.ofSeconds(30), + () -> assertEquals(1, PostgresQueryAnalyzer.analyze(wide).statementCount())); + } + + @Test + @DisplayName("exception positions are 1-based line and 0-based column") + void shouldReportErrorPosition() { + var exception = + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT 1\nFROM t WHERE")); + assertTrue(exception.line() >= 1); + assertTrue(exception.column() >= 0); + assertTrue(exception.getMessage().contains("line")); + } +} diff --git a/src/test/java/ai/singlr/postgresql/ModuleDescriptorTest.java b/src/test/java/ai/singlr/postgresql/ModuleDescriptorTest.java new file mode 100644 index 0000000..9154ea1 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/ModuleDescriptorTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.module.ModuleFinder; +import java.nio.file.Path; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Module descriptor") +class ModuleDescriptorTest { + + @Test + @DisplayName("public packages are exported, the vendored parser package is not") + void shouldExportOnlyPublicPackages() { + var module = + ModuleFinder.of(Path.of("target", "classes")).find("ai.singlr.scimsql").orElseThrow(); + + Set exports = + module.descriptor().exports().stream() + .map(java.lang.module.ModuleDescriptor.Exports::source) + .collect(java.util.stream.Collectors.toSet()); + + assertTrue(exports.contains("ai.singlr.scimsql")); + assertTrue(exports.contains("ai.singlr.postgresql")); + assertFalse(exports.contains("ai.singlr.postgresql.parser")); + } +} diff --git a/src/test/java/ai/singlr/postgresql/NamedParameterTest.java b/src/test/java/ai/singlr/postgresql/NamedParameterTest.java new file mode 100644 index 0000000..7150e8b --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/NamedParameterTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Named parameters") +class NamedParameterTest { + + @Test + @DisplayName("parameters are captured and deduplicated in order") + void shouldCaptureAndDeduplicate() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT * FROM events WHERE created_at >= :start_at AND created_at < :end_at" + + " AND user_id = :user_id AND tenant_id = :user_id"); + + assertEquals(List.of("start_at", "end_at", "user_id"), List.copyOf(analysis.parameters())); + } + + @Test + @DisplayName("parameter names are reported exactly as written") + void shouldPreserveParameterCase() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT :userId, :USER_ID"); + + assertEquals(Set.of("userId", "USER_ID"), analysis.parameters()); + } + + @Test + @DisplayName("parameters work in every expression position") + void shouldCaptureParametersEverywhere() { + var analysis = + PostgresQueryAnalyzer.analyze( + "INSERT INTO t (a, b) VALUES (:a, coalesce(:b, 0));" + + "UPDATE t SET a = :c WHERE id = ANY(ARRAY[:d]);" + + "SELECT * FROM t WHERE x IN (:e, :f) ORDER BY y LIMIT :g OFFSET :h"); + + assertEquals(Set.of("a", "b", "c", "d", "e", "f", "g", "h"), analysis.parameters()); + } + + @Test + @DisplayName("typecast, json operators and slices do not produce parameters") + void shouldNotConfuseOperators() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT a::text, b ->> 'k', c #>> '{x,y}', arr[1:2] FROM t WHERE d = :real_param"); + + assertEquals(Set.of("real_param"), analysis.parameters()); + } + + @Test + @DisplayName("colon-like content in strings, comments and quoted identifiers is inert") + void shouldIgnoreColonContent() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT ':not_a_param', \":also_not\", e':nope', $$ :still_not $$" + + " -- :comment_param\n" + + " /* :block_param */ FROM t WHERE x = :yes"); + + assertEquals(Set.of("yes"), analysis.parameters()); + } + + @Test + @DisplayName("parameters are captured at any depth including CTE bodies") + void shouldCaptureNestedParameters() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH recent AS (SELECT * FROM events WHERE at > :since)" + + " SELECT * FROM recent WHERE kind = ANY(SELECT kind FROM kinds" + + " WHERE weight > :min_weight)"); + + assertEquals(Set.of("since", "min_weight"), analysis.parameters()); + } + + @Test + @DisplayName("bare colon fails") + void shouldRejectBareColon() { + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT * FROM t WHERE id = :")); + } + + @Test + @DisplayName("colon followed by space and name fails") + void shouldRejectDetachedName() { + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT * FROM t WHERE id = : user_id")); + } + + @Test + @DisplayName("quoted parameter syntax fails") + void shouldRejectQuotedParameter() { + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT * FROM t WHERE id = :\"user_id\"")); + } + + @Test + @DisplayName("parameter cannot be used as a relation or alias") + void shouldRejectParameterAsIdentifier() { + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 1 FROM :tbl")); + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 1 AS :alias")); + } + + @Test + @DisplayName("positional dollar parameters are not named parameters") + void shouldIgnorePositionalParameters() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT * FROM t WHERE a = $1 AND b = :b"); + + assertEquals(Set.of("b"), analysis.parameters()); + } + + @Test + @DisplayName("values are never bound or substituted") + void shouldOnlyReportNames() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT :p"); + + assertTrue(analysis.normalizedSql().contains(":p")); + } +} diff --git a/src/test/java/ai/singlr/postgresql/NormalizationTest.java b/src/test/java/ai/singlr/postgresql/NormalizationTest.java new file mode 100644 index 0000000..9bb01d3 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/NormalizationTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Normalization") +class NormalizationTest { + + private static String normalize(String sql) { + return PostgresQueryAnalyzer.analyze(sql).normalizedSql(); + } + + @Test + @DisplayName("whitespace and comments do not change the normalized form") + void shouldBeStableAcrossFormatting() { + var canonical = normalize("SELECT id, name FROM users WHERE active = true"); + + assertEquals(canonical, normalize("SELECT id,\n\tname\nFROM users\nWHERE active = true")); + assertEquals( + canonical, + normalize("SELECT id, -- projection\n name /* the name */ FROM users WHERE active = true")); + } + + @Test + @DisplayName("keyword and unquoted identifier case do not change the normalized form") + void shouldBeStableAcrossCase() { + assertEquals(normalize("select id from Users"), normalize("SELECT ID FROM USERS")); + } + + @Test + @DisplayName("quoted identifiers, strings and parameter names are preserved verbatim") + void shouldPreserveSemanticText() { + var normalized = normalize("SELECT \"MixedCase\", 'Literal TEXT', :ParamName FROM \"T\""); + + assertEquals("select \"MixedCase\" , 'Literal TEXT' , :ParamName from \"T\"", normalized); + } + + @Test + @DisplayName("semantic changes produce different normalized forms") + void shouldDifferOnSemanticChanges() { + var canonical = normalize("SELECT a FROM t WHERE x > 1"); + + assertNotEquals(canonical, normalize("SELECT b FROM t WHERE x > 1")); + assertNotEquals(canonical, normalize("SELECT a FROM t2 WHERE x > 1")); + assertNotEquals(canonical, normalize("SELECT a FROM t WHERE x >= 1")); + assertNotEquals(canonical, normalize("SELECT a FROM t WHERE x > 2")); + } + + @Test + @DisplayName("quoted identifier case changes the normalized form") + void shouldDifferOnQuotedIdentifierCase() { + assertNotEquals( + normalize("SELECT \"Users\".id FROM \"Users\""), + normalize("SELECT \"users\".id FROM \"users\"")); + } + + @Test + @DisplayName("parameter name changes the normalized form") + void shouldDifferOnParameterName() { + assertNotEquals( + normalize("SELECT * FROM t WHERE id = :a"), normalize("SELECT * FROM t WHERE id = :b")); + } + + @Test + @DisplayName("normalization keeps dollar-quoted content verbatim") + void shouldPreserveDollarQuotedContent() { + var normalized = normalize("SELECT $tag$Keep CASE and -- fake comment$tag$"); + + assertEquals("select $tag$Keep CASE and -- fake comment$tag$", normalized); + } +} diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java new file mode 100644 index 0000000..8b4a3cd --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("PostgresQueryAnalyzer core analysis") +class PostgresQueryAnalyzerTest { + + @Test + @DisplayName("minimal select produces exact analysis") + void shouldAnalyzeMinimalSelect() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT 1"); + + assertEquals(StatementKind.SELECT, analysis.statementKind()); + assertEquals(1, analysis.statementCount()); + assertEquals(List.of(), analysis.relations()); + assertEquals(List.of(), analysis.columns()); + assertEquals(List.of(), analysis.functions()); + assertEquals(Set.of(), analysis.parameters()); + assertEquals(Set.of(), analysis.features()); + assertEquals("select 1", analysis.normalizedSql()); + } + + @Test + @DisplayName("simple select reports relation and columns") + void shouldAnalyzeSimpleSelect() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT id, name FROM users WHERE active = true"); + + assertEquals(StatementKind.SELECT, analysis.statementKind()); + assertEquals( + List.of(new RelationReference(null, "users", null, RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertEquals( + List.of( + new ColumnReference(null, "id"), + new ColumnReference(null, "name"), + new ColumnReference(null, "active")), + analysis.columns()); + } + + @Test + @DisplayName("join preserves aliases and qualifiers") + void shouldAnalyzeJoin() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT u.id, o.total FROM users u JOIN orders o ON o.user_id = u.id"); + + assertEquals( + List.of( + new RelationReference(null, "users", "u", RelationReference.Kind.PHYSICAL), + new RelationReference(null, "orders", "o", RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertEquals( + List.of( + new ColumnReference("u", "id"), + new ColumnReference("o", "total"), + new ColumnReference("o", "user_id"), + new ColumnReference("u", "id")), + analysis.columns()); + } + + @Test + @DisplayName("aggregate query captures functions in select, group by and having") + void shouldAnalyzeAggregate() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT count(id), lower(name) FROM users GROUP BY lower(name)" + + " HAVING max(age) > 21"); + + var names = analysis.functions().stream().map(FunctionReference::name).toList(); + assertEquals(List.of("count", "lower", "lower", "max"), names); + assertEquals(Set.of(), analysis.features()); + } + + @Test + @DisplayName("window function flags WINDOW and captures function") + void shouldAnalyzeWindow() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT rank() OVER (PARTITION BY dept ORDER BY salary) FROM emp"); + + assertTrue(analysis.features().contains(QueryFeature.WINDOW)); + assertEquals("rank", analysis.functions().getFirst().name()); + } + + @Test + @DisplayName("named window clause flags WINDOW") + void shouldAnalyzeNamedWindow() { + var analysis = + PostgresQueryAnalyzer.analyze("SELECT sum(x) OVER w FROM t WINDOW w AS (PARTITION BY y)"); + + assertTrue(analysis.features().contains(QueryFeature.WINDOW)); + } + + @Test + @DisplayName("set operations flag SET_OPERATION") + void shouldAnalyzeSetOperations() { + for (var op : List.of("UNION", "UNION ALL", "INTERSECT", "EXCEPT")) { + var analysis = PostgresQueryAnalyzer.analyze("SELECT a FROM t1 " + op + " SELECT a FROM t2"); + assertTrue( + analysis.features().contains(QueryFeature.SET_OPERATION), op + " should be flagged"); + assertEquals(2, analysis.relations().size(), op); + } + } + + @Test + @DisplayName("quoted and schema-qualified identifiers are preserved") + void shouldPreserveQuotedIdentifiers() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT \"Weird Name\", MixedCase FROM \"MySchema\".\"MyTable\" mt," + + " analytics.events"); + + assertEquals( + List.of( + new RelationReference("MySchema", "MyTable", "mt", RelationReference.Kind.PHYSICAL), + new RelationReference("analytics", "events", null, RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertEquals("MySchema", analysis.relations().getFirst().schema()); + assertEquals( + List.of(new ColumnReference(null, "Weird Name"), new ColumnReference(null, "mixedcase")), + analysis.columns()); + assertEquals("analytics", analysis.relations().get(1).schema()); + } + + @Test + @DisplayName("nested correlated subquery keeps physical relations at all depths") + void shouldAnalyzeCorrelatedSubquery() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT u.id FROM users u WHERE EXISTS (" + + "SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > (" + + "SELECT avg(total) FROM orders))"); + + assertTrue(analysis.features().contains(QueryFeature.SUBQUERY)); + var names = analysis.relations().stream().map(RelationReference::name).toList(); + assertEquals(List.of("users", "orders", "orders"), names); + } + + @Test + @DisplayName("CTE references are distinguished from physical relations") + void shouldDistinguishCteFromPhysical() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH active AS (SELECT * FROM users WHERE active) SELECT * FROM active, orders"); + + assertEquals( + List.of( + new RelationReference(null, "users", null, RelationReference.Kind.PHYSICAL), + new RelationReference(null, "active", null, RelationReference.Kind.CTE), + new RelationReference(null, "orders", null, RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertTrue(analysis.features().contains(QueryFeature.CTE)); + } + + @Test + @DisplayName("non-recursive CTE body referencing its own name is physical") + void shouldTreatSelfReferenceInNonRecursiveCteAsPhysical() { + var analysis = + PostgresQueryAnalyzer.analyze("WITH users AS (SELECT * FROM users) SELECT * FROM users"); + + assertEquals( + List.of( + new RelationReference(null, "users", null, RelationReference.Kind.PHYSICAL), + new RelationReference(null, "users", null, RelationReference.Kind.CTE)), + analysis.relations()); + } + + @Test + @DisplayName("recursive CTE self-reference is a CTE reference") + void shouldTreatSelfReferenceInRecursiveCteAsCte() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH RECURSIVE tree AS (" + + "SELECT id, parent_id FROM nodes WHERE parent_id IS NULL" + + " UNION ALL SELECT n.id, n.parent_id FROM nodes n JOIN tree t" + + " ON n.parent_id = t.id) SELECT * FROM tree"); + + assertTrue(analysis.features().contains(QueryFeature.RECURSIVE_CTE)); + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("nodes:PHYSICAL", "nodes:PHYSICAL", "tree:CTE", "tree:CTE"), kinds); + } + + @Test + @DisplayName("later CTE sees earlier sibling CTE") + void shouldScopeSiblingCtes() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH a AS (SELECT 1 AS x), b AS (SELECT x FROM a) SELECT * FROM b"); + + assertEquals( + List.of( + new RelationReference(null, "a", null, RelationReference.Kind.CTE), + new RelationReference(null, "b", null, RelationReference.Kind.CTE)), + analysis.relations()); + } + + @Test + @DisplayName("inner CTE shadows outer physical relation without losing outer physical refs") + void shouldHandleShadowedCteNames() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT * FROM users WHERE id IN (" + + "WITH users AS (SELECT owner_id FROM projects)" + + " SELECT owner_id FROM users)"); + + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("users:PHYSICAL", "projects:PHYSICAL", "users:CTE"), kinds); + } + + @Test + @DisplayName("schema-qualified reference never matches a CTE name") + void shouldNotMatchSchemaQualifiedAsCte() { + var analysis = + PostgresQueryAnalyzer.analyze("WITH users AS (SELECT 1) SELECT * FROM public.users, users"); + + assertEquals( + List.of( + new RelationReference("public", "users", null, RelationReference.Kind.PHYSICAL), + new RelationReference(null, "users", null, RelationReference.Kind.CTE)), + analysis.relations()); + } + + @Test + @DisplayName("functions are captured in select, where, join, group, window and from") + void shouldCaptureFunctionsEverywhere() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT lower(a), rank() OVER (ORDER BY nullif(b, 0))" + + " FROM t JOIN generate_series(1, 10) g ON abs(t.x) = g" + + " WHERE coalesce(t.y, now()) IS NOT NULL" + + " GROUP BY lower(a), date_trunc('day', t.created_at)"); + + var names = analysis.functions().stream().map(FunctionReference::name).toList(); + assertTrue( + names.containsAll( + List.of( + "lower", + "rank", + "nullif", + "generate_series", + "abs", + "coalesce", + "now", + "date_trunc")), + names.toString()); + } + + @Test + @DisplayName("schema-qualified function keeps schema and location") + void shouldCaptureSchemaQualifiedFunction() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT pg_catalog.now()"); + + var function = analysis.functions().getFirst(); + assertEquals("pg_catalog", function.schema()); + assertEquals("now", function.name()); + assertEquals(1, function.line()); + assertEquals(7, function.column()); + } + + @Test + @DisplayName("special-form functions are reported by keyword") + void shouldCaptureSpecialFormFunctions() { + var analysis = + PostgresQueryAnalyzer.analyze("SELECT CAST(a AS int), EXTRACT(YEAR FROM b) FROM t"); + + var names = analysis.functions().stream().map(FunctionReference::name).toList(); + assertEquals(List.of("cast", "extract"), names); + } + + @Test + @DisplayName("insert reports target relation, columns and parameters") + void shouldAnalyzeInsert() { + var analysis = + PostgresQueryAnalyzer.analyze( + "INSERT INTO audit.events (kind, payload) VALUES (:kind, :payload)"); + + assertEquals(StatementKind.INSERT, analysis.statementKind()); + assertEquals( + List.of(new RelationReference("audit", "events", null, RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertEquals( + List.of(new ColumnReference(null, "kind"), new ColumnReference(null, "payload")), + analysis.columns()); + assertEquals(Set.of("kind", "payload"), analysis.parameters()); + } + + @Test + @DisplayName("update reports set targets and alias") + void shouldAnalyzeUpdate() { + var analysis = + PostgresQueryAnalyzer.analyze("UPDATE users u SET name = :name WHERE u.id = :id"); + + assertEquals(StatementKind.UPDATE, analysis.statementKind()); + assertEquals( + List.of(new RelationReference(null, "users", "u", RelationReference.Kind.PHYSICAL)), + analysis.relations()); + assertEquals( + List.of(new ColumnReference(null, "name"), new ColumnReference("u", "id")), + analysis.columns()); + } + + @Test + @DisplayName("delete using reports both relations") + void shouldAnalyzeDeleteUsing() { + var analysis = + PostgresQueryAnalyzer.analyze( + "DELETE FROM sessions s USING users u WHERE s.user_id = u.id AND u.banned"); + + assertEquals(StatementKind.DELETE, analysis.statementKind()); + assertEquals( + List.of( + new RelationReference(null, "sessions", "s", RelationReference.Kind.PHYSICAL), + new RelationReference(null, "users", "u", RelationReference.Kind.PHYSICAL)), + analysis.relations()); + } + + @Test + @DisplayName("alias without AS keyword is preserved") + void shouldCaptureBareAlias() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT * FROM users AS u"); + + assertEquals("u", analysis.relations().getFirst().alias()); + } + + @Test + @DisplayName("star projection reports star column reference") + void shouldReportStarColumn() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT *, t.* FROM t"); + + assertTrue(analysis.features().contains(QueryFeature.STAR_PROJECTION)); + assertEquals( + List.of(new ColumnReference(null, "*"), new ColumnReference("t", "*")), analysis.columns()); + assertTrue(analysis.columns().getFirst().star()); + assertNull(analysis.columns().getFirst().qualifier()); + } + + @Test + @DisplayName("array subscript keeps column name without subscript") + void shouldHandleArraySubscript() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT tags[1] FROM posts"); + + assertEquals(List.of(new ColumnReference(null, "tags")), analysis.columns()); + } +} diff --git a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java new file mode 100644 index 0000000..1292dd9 --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayName("Statement classification") +class StatementClassificationTest { + + @ParameterizedTest(name = "{1}: {0}") + @CsvSource( + delimiter = '|', + textBlock = + """ + SELECT 1 | SELECT + TABLE users | SELECT + (SELECT 1) | SELECT + INSERT INTO t VALUES (1) | INSERT + UPDATE t SET a = 1 | UPDATE + DELETE FROM t | DELETE + MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = 1 WHEN NOT MATCHED THEN INSERT VALUES (1) | MERGE + CREATE TABLE t (id int) | DDL + CREATE INDEX idx ON t (id) | DDL + CREATE VIEW v AS SELECT 1 | DDL + CREATE MATERIALIZED VIEW mv AS SELECT 1 | DDL + CREATE ROLE reporting | DDL + CREATE FUNCTION f() RETURNS int AS 'select 1' LANGUAGE sql | DDL + ALTER TABLE t ADD COLUMN b int | DDL + ALTER TABLE t RENAME TO t2 | DDL + DROP TABLE t | DDL + DROP INDEX idx | DDL + GRANT SELECT ON t TO reporting | DDL + REVOKE SELECT ON t FROM reporting | DDL + COMMENT ON TABLE t IS 'x' | DDL + COPY t FROM STDIN | UTILITY + COPY (SELECT * FROM t) TO STDOUT | UTILITY + CALL do_things(1) | UTILITY + DO 'begin end' | UTILITY + SET search_path = public | UTILITY + SET LOCAL statement_timeout = 100 | UTILITY + RESET search_path | UTILITY + SHOW server_version | UTILITY + BEGIN | UTILITY + COMMIT | UTILITY + ROLLBACK | UTILITY + SAVEPOINT sp | UTILITY + TRUNCATE t | UTILITY + LOCK TABLE t | UTILITY + EXPLAIN SELECT 1 | UTILITY + VACUUM t | UTILITY + ANALYZE t | UTILITY + PREPARE p AS SELECT 1 | UTILITY + EXECUTE p | UTILITY + DEALLOCATE p | UTILITY + LISTEN chan | UTILITY + NOTIFY chan | UTILITY + CHECKPOINT | UTILITY + """) + void shouldClassify(String sql, StatementKind expected) { + assertEquals(expected, PostgresQueryAnalyzer.analyze(sql).statementKind()); + } + + @ParameterizedTest(name = "relations survive in {1}: {0}") + @CsvSource( + delimiter = '|', + textBlock = + """ + EXPLAIN SELECT * FROM users | users + CREATE VIEW v AS SELECT * FROM users | users + CREATE TABLE copy_t AS SELECT * FROM users | users + """) + void shouldCaptureRelationsInWrappedStatements(String sql, String relation) { + var names = + PostgresQueryAnalyzer.analyze(sql).relations().stream() + .map(RelationReference::name) + .toList(); + org.junit.jupiter.api.Assertions.assertTrue(names.contains(relation), names.toString()); + } +} diff --git a/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java b/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java new file mode 100644 index 0000000..469266b --- /dev/null +++ b/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Singular + * SPDX-License-Identifier: MIT + */ + +package ai.singlr.postgresql; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL regression examples vendored from ANTLR grammars-v4 at commit + * 76093c04af6a51f38a67d14f7e71ff0a9b4400da (sql/postgresql/examples). Each file must analyze end to + * end; failures indicate a regression in the vendored grammar or the analyzer. + */ +@DisplayName("Upstream regression corpus") +class UpstreamCorpusTest { + + @ParameterizedTest + @ValueSource( + strings = { + "select.sql", + "join.sql", + "aggregates.sql", + "union.sql", + "with.sql", + "window.sql", + "insert.sql", + "update.sql", + "delete.sql", + "case.sql", + "subselect.sql", + "limit.sql", + "transactions.sql", + "select_having.sql", + "select_distinct.sql" + }) + @DisplayName("corpus file analyzes end to end") + void shouldAnalyzeCorpusFile(String file) { + var analysis = PostgresQueryAnalyzer.analyze(read(file)); + + assertNotNull(analysis.normalizedSql()); + assertTrue(analysis.statementCount() > 0, file); + } + + private static String read(String file) { + try (var in = UpstreamCorpusTest.class.getResourceAsStream("/postgresql-corpus/" + file)) { + assertNotNull(in, file + " missing from corpus"); + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/test/resources/postgresql-corpus/aggregates.sql b/src/test/resources/postgresql-corpus/aggregates.sql new file mode 100644 index 0000000..9910287 --- /dev/null +++ b/src/test/resources/postgresql-corpus/aggregates.sql @@ -0,0 +1,1218 @@ +-- +-- AGGREGATES +-- + +-- avoid bit-exact output here because operations may not be bit-exact. +SET extra_float_digits = 0; + +SELECT avg(four) AS avg_1 FROM onek; + +SELECT avg(a) AS avg_32 FROM aggtest WHERE a < 100; + +-- In 7.1, avg(float4) is computed using float8 arithmetic. +-- Round the result to 3 digits to avoid platform-specific results. + +SELECT avg(b)::numeric(10,3) AS avg_107_943 FROM aggtest; + +SELECT avg(gpa) AS avg_3_4 FROM ONLY student; + + +SELECT sum(four) AS sum_1500 FROM onek; +SELECT sum(a) AS sum_198 FROM aggtest; +SELECT sum(b) AS avg_431_773 FROM aggtest; +SELECT sum(gpa) AS avg_6_8 FROM ONLY student; + +SELECT max(four) AS max_3 FROM onek; +SELECT max(a) AS max_100 FROM aggtest; +SELECT max(aggtest.b) AS max_324_78 FROM aggtest; +SELECT max(student.gpa) AS max_3_7 FROM student; + +SELECT stddev_pop(b) FROM aggtest; +SELECT stddev_samp(b) FROM aggtest; +SELECT var_pop(b) FROM aggtest; +SELECT var_samp(b) FROM aggtest; + +SELECT stddev_pop(b::numeric) FROM aggtest; +SELECT stddev_samp(b::numeric) FROM aggtest; +SELECT var_pop(b::numeric) FROM aggtest; +SELECT var_samp(b::numeric) FROM aggtest; + +-- population variance is defined for a single tuple, sample variance +-- is not +SELECT var_pop(1.0::float8), var_samp(2.0::float8); +SELECT stddev_pop(3.0::float8), stddev_samp(4.0::float8); +SELECT var_pop('inf'::float8), var_samp('inf'::float8); +SELECT stddev_pop('inf'::float8), stddev_samp('inf'::float8); +SELECT var_pop('nan'::float8), var_samp('nan'::float8); +SELECT stddev_pop('nan'::float8), stddev_samp('nan'::float8); +SELECT var_pop(1.0::float4), var_samp(2.0::float4); +SELECT stddev_pop(3.0::float4), stddev_samp(4.0::float4); +SELECT var_pop('inf'::float4), var_samp('inf'::float4); +SELECT stddev_pop('inf'::float4), stddev_samp('inf'::float4); +SELECT var_pop('nan'::float4), var_samp('nan'::float4); +SELECT stddev_pop('nan'::float4), stddev_samp('nan'::float4); +SELECT var_pop(1.0::numeric), var_samp(2.0::numeric); +SELECT stddev_pop(3.0::numeric), stddev_samp(4.0::numeric); +SELECT var_pop('inf'::numeric), var_samp('inf'::numeric); +SELECT stddev_pop('inf'::numeric), stddev_samp('inf'::numeric); +SELECT var_pop('nan'::numeric), var_samp('nan'::numeric); +SELECT stddev_pop('nan'::numeric), stddev_samp('nan'::numeric); + +-- verify correct results for null and NaN inputs +select sum(null::int4) from generate_series(1,3); +select sum(null::int8) from generate_series(1,3); +select sum(null::numeric) from generate_series(1,3); +select sum(null::float8) from generate_series(1,3); +select avg(null::int4) from generate_series(1,3); +select avg(null::int8) from generate_series(1,3); +select avg(null::numeric) from generate_series(1,3); +select avg(null::float8) from generate_series(1,3); +select sum('NaN'::numeric) from generate_series(1,3); +select avg('NaN'::numeric) from generate_series(1,3); + +-- verify correct results for infinite inputs +SELECT sum(x::float8), avg(x::float8), var_pop(x::float8) +FROM (VALUES ('1'), ('infinity')) v(x); +SELECT sum(x::float8), avg(x::float8), var_pop(x::float8) +FROM (VALUES ('infinity'), ('1')) v(x); +SELECT sum(x::float8), avg(x::float8), var_pop(x::float8) +FROM (VALUES ('infinity'), ('infinity')) v(x); +SELECT sum(x::float8), avg(x::float8), var_pop(x::float8) +FROM (VALUES ('-infinity'), ('infinity')) v(x); +SELECT sum(x::float8), avg(x::float8), var_pop(x::float8) +FROM (VALUES ('-infinity'), ('-infinity')) v(x); +SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric) +FROM (VALUES ('1'), ('infinity')) v(x); +SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric) +FROM (VALUES ('infinity'), ('1')) v(x); +SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric) +FROM (VALUES ('infinity'), ('infinity')) v(x); +SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric) +FROM (VALUES ('-infinity'), ('infinity')) v(x); +SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric) +FROM (VALUES ('-infinity'), ('-infinity')) v(x); + +-- test accuracy with a large input offset +SELECT avg(x::float8), var_pop(x::float8) +FROM (VALUES (100000003), (100000004), (100000006), (100000007)) v(x); +SELECT avg(x::float8), var_pop(x::float8) +FROM (VALUES (7000000000005), (7000000000007)) v(x); + +-- SQL2003 binary aggregates +SELECT regr_count(b, a) FROM aggtest; +SELECT regr_sxx(b, a) FROM aggtest; +SELECT regr_syy(b, a) FROM aggtest; +SELECT regr_sxy(b, a) FROM aggtest; +SELECT regr_avgx(b, a), regr_avgy(b, a) FROM aggtest; +SELECT regr_r2(b, a) FROM aggtest; +SELECT regr_slope(b, a), regr_intercept(b, a) FROM aggtest; +SELECT covar_pop(b, a), covar_samp(b, a) FROM aggtest; +SELECT corr(b, a) FROM aggtest; + +-- check single-tuple behavior +SELECT covar_pop(1::float8,2::float8), covar_samp(3::float8,4::float8); +SELECT covar_pop(1::float8,'inf'::float8), covar_samp(3::float8,'inf'::float8); +SELECT covar_pop(1::float8,'nan'::float8), covar_samp(3::float8,'nan'::float8); + +-- test accum and combine functions directly +CREATE TABLE regr_test (x float8, y float8); +INSERT INTO regr_test VALUES (10,150),(20,250),(30,350),(80,540),(100,200); +SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) +FROM regr_test WHERE x IN (10,20,30,80); +SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) +FROM regr_test; +SELECT float8_accum('{4,140,2900}'::float8[], 100); +SELECT float8_regr_accum('{4,140,2900,1290,83075,15050}'::float8[], 200, 100); +SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) +FROM regr_test WHERE x IN (10,20,30); +SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) +FROM regr_test WHERE x IN (80,100); +SELECT float8_combine('{3,60,200}'::float8[], '{0,0,0}'::float8[]); +SELECT float8_combine('{0,0,0}'::float8[], '{2,180,200}'::float8[]); +SELECT float8_combine('{3,60,200}'::float8[], '{2,180,200}'::float8[]); +SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], + '{0,0,0,0,0,0}'::float8[]); +SELECT float8_regr_combine('{0,0,0,0,0,0}'::float8[], + '{2,180,200,740,57800,-3400}'::float8[]); +SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], + '{2,180,200,740,57800,-3400}'::float8[]); +DROP TABLE regr_test; + +-- test count, distinct +SELECT count(four) AS cnt_1000 FROM onek; +SELECT count(DISTINCT four) AS cnt_4 FROM onek; + +select ten, count(*), sum(four) from onek +group by ten order by ten; + +select ten, count(four), sum(DISTINCT four) from onek +group by ten order by ten; + +-- user-defined aggregates +SELECT newavg(four) AS avg_1 FROM onek; +SELECT newsum(four) AS sum_1500 FROM onek; +SELECT newcnt(four) AS cnt_1000 FROM onek; +SELECT newcnt(*) AS cnt_1000 FROM onek; +SELECT oldcnt(*) AS cnt_1000 FROM onek; +SELECT sum2(q1,q2) FROM int8_tbl; + +-- test for outer-level aggregates + +-- this should work +select ten, sum(distinct four) from onek a +group by ten +having exists (select 1 from onek b where sum(distinct a.four) = b.four); + +-- this should fail because subquery has an agg of its own in WHERE +select ten, sum(distinct four) from onek a +group by ten +having exists (select 1 from onek b + where sum(distinct a.four + b.four) = b.four); + +-- Test handling of sublinks within outer-level aggregates. +-- Per bug report from Daniel Grace. +select + (select max((select i.unique2 from tenk1 i where i.unique1 = o.unique1))) +from tenk1 o; + +-- Test handling of Params within aggregate arguments in hashed aggregation. +-- Per bug report from Jeevan Chalke. +explain (verbose, costs off) +select s1, s2, sm +from generate_series(1, 3) s1, + lateral (select s2, sum(s1 + s2) sm + from generate_series(1, 3) s2 group by s2) ss +order by 1, 2; +select s1, s2, sm +from generate_series(1, 3) s1, + lateral (select s2, sum(s1 + s2) sm + from generate_series(1, 3) s2 group by s2) ss +order by 1, 2; + +explain (verbose, costs off) +select array(select sum(x+y) s + from generate_series(1,3) y group by y order by s) + from generate_series(1,3) x; +select array(select sum(x+y) s + from generate_series(1,3) y group by y order by s) + from generate_series(1,3) x; + +-- +-- test for bitwise integer aggregates +-- +CREATE TEMPORARY TABLE bitwise_test( + i2 INT2, + i4 INT4, + i8 INT8, + i INTEGER, + x INT2, + y BIT(4) +); + +-- empty case +SELECT + BIT_AND(i2) AS aaa, + BIT_OR(i4) AS baaa +FROM bitwise_test; + +COPY bitwise_test FROM STDIN NULL 'null'; + +SELECT + BIT_AND(i2) AS "1", + BIT_AND(i4) AS "1", + BIT_AND(i8) AS "1", + BIT_AND(i) AS "?", + BIT_AND(x) AS "0", + BIT_AND(y) AS "0100", + + BIT_OR(i2) AS "7", + BIT_OR(i4) AS "7", + BIT_OR(i8) AS "7", + BIT_OR(i) AS "?", + BIT_OR(x) AS "7", + BIT_OR(y) AS "1101" +FROM bitwise_test; + +-- +-- test boolean aggregates +-- +-- first test all possible transition and final states + +SELECT + -- boolean and transitions + -- null because strict + booland_statefunc(NULL, NULL) IS NULL AS "t", + booland_statefunc(TRUE, NULL) IS NULL AS "t", + booland_statefunc(FALSE, NULL) IS NULL AS "t", + booland_statefunc(NULL, TRUE) IS NULL AS "t", + booland_statefunc(NULL, FALSE) IS NULL AS "t", + -- and actual computations + booland_statefunc(TRUE, TRUE) AS "t", + NOT booland_statefunc(TRUE, FALSE) AS "t", + NOT booland_statefunc(FALSE, TRUE) AS "t", + NOT booland_statefunc(FALSE, FALSE) AS "t"; + +SELECT + -- boolean or transitions + -- null because strict + boolor_statefunc(NULL, NULL) IS NULL AS "t", + boolor_statefunc(TRUE, NULL) IS NULL AS "t", + boolor_statefunc(FALSE, NULL) IS NULL AS "t", + boolor_statefunc(NULL, TRUE) IS NULL AS "t", + boolor_statefunc(NULL, FALSE) IS NULL AS "t", + -- actual computations + boolor_statefunc(TRUE, TRUE) AS "t", + boolor_statefunc(TRUE, FALSE) AS "t", + boolor_statefunc(FALSE, TRUE) AS "t", + NOT boolor_statefunc(FALSE, FALSE) AS "t"; + +CREATE TEMPORARY TABLE bool_test( + b1 BOOL, + b2 BOOL, + b3 BOOL, + b4 BOOL); + +-- empty case +SELECT + BOOL_AND(b1) AS "n", + BOOL_OR(b3) AS "n" +FROM bool_test; + +COPY bool_test FROM STDIN NULL 'null'; + +SELECT + BOOL_AND(b1) AS "f", + BOOL_AND(b2) AS "t", + BOOL_AND(b3) AS "f", + BOOL_AND(b4) AS "n", + BOOL_AND(NOT b2) AS "f", + BOOL_AND(NOT b3) AS "t" +FROM bool_test; + +SELECT + EVERY(b1) AS "f", + EVERY(b2) AS "t", + EVERY(b3) AS "f", + EVERY(b4) AS "n", + EVERY(NOT b2) AS "f", + EVERY(NOT b3) AS "t" +FROM bool_test; + +SELECT + BOOL_OR(b1) AS "t", + BOOL_OR(b2) AS "t", + BOOL_OR(b3) AS "f", + BOOL_OR(b4) AS "n", + BOOL_OR(NOT b2) AS "f", + BOOL_OR(NOT b3) AS "t" +FROM bool_test; + +-- +-- Test cases that should be optimized into indexscans instead of +-- the generic aggregate implementation. +-- + +-- Basic cases +explain (costs off) + select min(unique1) from tenk1; +select min(unique1) from tenk1; +explain (costs off) + select max(unique1) from tenk1; +select max(unique1) from tenk1; +explain (costs off) + select max(unique1) from tenk1 where unique1 < 42; +select max(unique1) from tenk1 where unique1 < 42; +explain (costs off) + select max(unique1) from tenk1 where unique1 > 42; +select max(unique1) from tenk1 where unique1 > 42; + +-- the planner may choose a generic aggregate here if parallel query is +-- enabled, since that plan will be parallel safe and the "optimized" +-- plan, which has almost identical cost, will not be. we want to test +-- the optimized plan, so temporarily disable parallel query. +begin; +set local max_parallel_workers_per_gather = 0; +explain (costs off) + select max(unique1) from tenk1 where unique1 > 42000; +select max(unique1) from tenk1 where unique1 > 42000; +rollback; + +-- multi-column index (uses tenk1_thous_tenthous) +explain (costs off) + select max(tenthous) from tenk1 where thousand = 33; +select max(tenthous) from tenk1 where thousand = 33; +explain (costs off) + select min(tenthous) from tenk1 where thousand = 33; +select min(tenthous) from tenk1 where thousand = 33; + +-- check parameter propagation into an indexscan subquery +explain (costs off) + select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt + from int4_tbl; +select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt + from int4_tbl; + +-- check some cases that were handled incorrectly in 8.3.0 +explain (costs off) + select distinct max(unique2) from tenk1; +select distinct max(unique2) from tenk1; +explain (costs off) + select max(unique2) from tenk1 order by 1; +select max(unique2) from tenk1 order by 1; +explain (costs off) + select max(unique2) from tenk1 order by max(unique2); +select max(unique2) from tenk1 order by max(unique2); +explain (costs off) + select max(unique2) from tenk1 order by max(unique2)+1; +select max(unique2) from tenk1 order by max(unique2)+1; +explain (costs off) + select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; +select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; + +-- interesting corner case: constant gets optimized into a seqscan +explain (costs off) + select max(100) from tenk1; +select max(100) from tenk1; + +-- try it on an inheritance tree +create table minmaxtest(f1 int); +create table minmaxtest1() inherits (minmaxtest); +create table minmaxtest2() inherits (minmaxtest); +create table minmaxtest3() inherits (minmaxtest); +create index minmaxtesti on minmaxtest(f1); +create index minmaxtest1i on minmaxtest1(f1); +create index minmaxtest2i on minmaxtest2(f1 desc); +create index minmaxtest3i on minmaxtest3(f1) where f1 is not null; + +insert into minmaxtest values(11), (12); +insert into minmaxtest1 values(13), (14); +insert into minmaxtest2 values(15), (16); +insert into minmaxtest3 values(17), (18); + +explain (costs off) + select min(f1), max(f1) from minmaxtest; +select min(f1), max(f1) from minmaxtest; + +-- DISTINCT doesn't do anything useful here, but it shouldn't fail +explain (costs off) + select distinct min(f1), max(f1) from minmaxtest; +select distinct min(f1), max(f1) from minmaxtest; + +drop table minmaxtest cascade; + +-- check for correct detection of nested-aggregate errors +select max(min(unique1)) from tenk1; +select (select max(min(unique1)) from int8_tbl) from tenk1; + +-- +-- Test removal of redundant GROUP BY columns +-- + +create temp table t1 (a int, b int, c int, d int, primary key (a, b)); +create temp table t2 (x int, y int, z int, primary key (x, y)); +create temp table t3 (a int, b int, c int, primary key(a, b) deferrable); + +-- Non-primary-key columns can be removed from GROUP BY +explain (costs off) select * from t1 group by a,b,c,d; + +-- No removal can happen if the complete PK is not present in GROUP BY +explain (costs off) select a,c from t1 group by a,c,d; + +-- Test removal across multiple relations +explain (costs off) select * +from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y +group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z; + +-- Test case where t1 can be optimized but not t2 +explain (costs off) select t1.*,t2.x,t2.z +from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y +group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z; + +-- Cannot optimize when PK is deferrable +explain (costs off) select * from t3 group by a,b,c; + +create temp table t1c () inherits (t1); + +-- Ensure we don't remove any columns when t1 has a child table +explain (costs off) select * from t1 group by a,b,c,d; + +-- Okay to remove columns if we're only querying the parent. +explain (costs off) select * from only t1 group by a,b,c,d; + +create temp table p_t1 ( + a int, + b int, + c int, + d int, + primary key(a,b) +) partition by list(a); +create temp table p_t1_1 partition of p_t1 for values in(1); +create temp table p_t1_2 partition of p_t1 for values in(2); + +-- Ensure we can remove non-PK columns for partitioned tables. +explain (costs off) select * from p_t1 group by a,b,c,d; + +drop table t1 cascade; +drop table t2; +drop table t3; +drop table p_t1; + +-- +-- Test GROUP BY matching of join columns that are type-coerced due to USING +-- + +create temp table t1(f1 int, f2 bigint); +create temp table t2(f1 bigint, f22 bigint); + +select f1 from t1 left join t2 using (f1) group by f1; +select f1 from t1 left join t2 using (f1) group by t1.f1; +select t1.f1 from t1 left join t2 using (f1) group by t1.f1; +-- only this one should fail: +select t1.f1 from t1 left join t2 using (f1) group by f1; + +drop table t1, t2; + +-- +-- Test combinations of DISTINCT and/or ORDER BY +-- + +select array_agg(a order by b) + from (values (1,4),(2,3),(3,1),(4,2)) v(a,b); +select array_agg(a order by a) + from (values (1,4),(2,3),(3,1),(4,2)) v(a,b); +select array_agg(a order by a desc) + from (values (1,4),(2,3),(3,1),(4,2)) v(a,b); +select array_agg(b order by a desc) + from (values (1,4),(2,3),(3,1),(4,2)) v(a,b); + +select array_agg(distinct a) + from (values (1),(2),(1),(3),(null),(2)) v(a); +select array_agg(distinct a order by a) + from (values (1),(2),(1),(3),(null),(2)) v(a); +select array_agg(distinct a order by a desc) + from (values (1),(2),(1),(3),(null),(2)) v(a); +select array_agg(distinct a order by a desc nulls last) + from (values (1),(2),(1),(3),(null),(2)) v(a); + +-- multi-arg aggs, strict/nonstrict, distinct/order by + +select aggfstr(a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); +select aggfns(a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); + +select aggfstr(distinct a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; +select aggfns(distinct a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; + +select aggfstr(distinct a,b,c order by b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; +select aggfns(distinct a,b,c order by b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; + +-- test specific code paths + +select aggfns(distinct a,a,c order by c using ~<~,a) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; +select aggfns(distinct a,a,c order by c using ~<~) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; +select aggfns(distinct a,a,c order by a) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; +select aggfns(distinct a,b,c order by a,c using ~<~,b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; + +-- check node I/O via view creation and usage, also deparsing logic + +create view agg_view1 as + select aggfns(a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(distinct a,b,c) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(distinct a,b,c order by b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,3) i; + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(a,b,c order by b+1) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(a,a,c order by b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(a,b,c order by c using ~<~) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c); + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +create or replace view agg_view1 as + select aggfns(distinct a,b,c order by a,c using ~<~,b) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; + +select * from agg_view1; +select pg_get_viewdef('agg_view1'::regclass); + +drop view agg_view1; + +-- incorrect DISTINCT usage errors + +select aggfns(distinct a,b,c order by i) + from (values (1,1,'foo')) v(a,b,c), generate_series(1,2) i; +select aggfns(distinct a,b,c order by a,b+1) + from (values (1,1,'foo')) v(a,b,c), generate_series(1,2) i; +select aggfns(distinct a,b,c order by a,b,i,c) + from (values (1,1,'foo')) v(a,b,c), generate_series(1,2) i; +select aggfns(distinct a,a,c order by a,b) + from (values (1,1,'foo')) v(a,b,c), generate_series(1,2) i; + +-- string_agg tests +select string_agg(a,',') from (values('aaaa'),('bbbb'),('cccc')) g(a); +select string_agg(a,',') from (values('aaaa'),(null),('bbbb'),('cccc')) g(a); +select string_agg(a,'AB') from (values(null),(null),('bbbb'),('cccc')) g(a); +select string_agg(a,',') from (values(null),(null)) g(a); + +-- check some implicit casting cases, as per bug #5564 +select string_agg(distinct f1, ',' order by f1) from varchar_tbl; -- ok +select string_agg(distinct f1::text, ',' order by f1) from varchar_tbl; -- not ok +select string_agg(distinct f1, ',' order by f1::text) from varchar_tbl; -- not ok +select string_agg(distinct f1::text, ',' order by f1::text) from varchar_tbl; -- ok + +-- string_agg bytea tests +create table bytea_test_table(v bytea); + +select string_agg(v, '') from bytea_test_table; + +insert into bytea_test_table values(decode('ff','hex')); + +select string_agg(v, '') from bytea_test_table; + +insert into bytea_test_table values(decode('aa','hex')); + +select string_agg(v, '') from bytea_test_table; +select string_agg(v, NULL) from bytea_test_table; +select string_agg(v, decode('ee', 'hex')) from bytea_test_table; + +drop table bytea_test_table; + +-- FILTER tests + +select min(unique1) filter (where unique1 > 100) from tenk1; + +select sum(1/ten) filter (where ten > 0) from tenk1; + +--select ten, sum(distinct four) filter (where four::text ~ '123') from onek a +--group by ten; + +select ten, sum(distinct four) filter (where four > 10) from onek a +group by ten +having exists (select 1 from onek b where sum(distinct a.four) = b.four); + +select max(foo COLLATE "C") filter (where (bar collate "POSIX") > '0') +from (values ('a', 'b')) AS v(foo,bar); + +-- outer reference in FILTER (PostgreSQL extension) +select (select count(*) + from (values (1)) t0(inner_c)) +from (values (2),(3)) t1(outer_c); -- inner query is aggregation query +select (select count(*) filter (where outer_c <> 0) + from (values (1)) t0(inner_c)) +from (values (2),(3)) t1(outer_c); -- outer query is aggregation query +select (select count(inner_c) filter (where outer_c <> 0) + from (values (1)) t0(inner_c)) +from (values (2),(3)) t1(outer_c); -- inner query is aggregation query +select + (select max((select i.unique2 from tenk1 i where i.unique1 = o.unique1)) + filter (where o.unique1 < 10)) +from tenk1 o; -- outer query is aggregation query + +-- subquery in FILTER clause (PostgreSQL extension) +select sum(unique1) FILTER (WHERE + unique1 IN (SELECT unique1 FROM onek where unique1 < 100)) FROM tenk1; + +-- exercise lots of aggregate parts with FILTER +select aggfns(distinct a,b,c order by a,c using ~<~,b) filter (where a > 1) + from (values (1,3,'foo'),(0,null,null),(2,2,'bar'),(3,1,'baz')) v(a,b,c), + generate_series(1,2) i; + +-- ordered-set aggregates + +select p, percentile_cont(p) within group (order by x::float8) +from generate_series(1,5) x, + (values (0::float8),(0.1),(0.25),(0.4),(0.5),(0.6),(0.75),(0.9),(1)) v(p) +group by p order by p; + +select p, percentile_cont(p order by p) within group (order by x) -- error +from generate_series(1,5) x, + (values (0::float8),(0.1),(0.25),(0.4),(0.5),(0.6),(0.75),(0.9),(1)) v(p) +group by p order by p; + +select p, sum() within group (order by x::float8) -- error +from generate_series(1,5) x, + (values (0::float8),(0.1),(0.25),(0.4),(0.5),(0.6),(0.75),(0.9),(1)) v(p) +group by p order by p; + +select p, percentile_cont(p,p) -- error +from generate_series(1,5) x, + (values (0::float8),(0.1),(0.25),(0.4),(0.5),(0.6),(0.75),(0.9),(1)) v(p) +group by p order by p; + +select percentile_cont(0.5) within group (order by b) from aggtest; +select percentile_cont(0.5) within group (order by b), sum(b) from aggtest; +select percentile_cont(0.5) within group (order by thousand) from tenk1; +select percentile_disc(0.5) within group (order by thousand) from tenk1; +select rank(3) within group (order by x) +from (values (1),(1),(2),(2),(3),(3),(4)) v(x); +select cume_dist(3) within group (order by x) +from (values (1),(1),(2),(2),(3),(3),(4)) v(x); +select percent_rank(3) within group (order by x) +from (values (1),(1),(2),(2),(3),(3),(4),(5)) v(x); +select dense_rank(3) within group (order by x) +from (values (1),(1),(2),(2),(3),(3),(4)) v(x); + +select percentile_disc(array[0,0.1,0.25,0.5,0.75,0.9,1]) within group (order by thousand) +from tenk1; +select percentile_cont(array[0,0.25,0.5,0.75,1]) within group (order by thousand) +from tenk1; +select percentile_disc(array[[null,1,0.5],[0.75,0.25,null]]) within group (order by thousand) +from tenk1; +select percentile_cont(array[0,1,0.25,0.75,0.5,1,0.3,0.32,0.35,0.38,0.4]) within group (order by x) +from generate_series(1,6) x; + +select ten, mode() within group (order by string4) from tenk1 group by ten; + +select percentile_disc(array[0.25,0.5,0.75]) within group (order by x) +from unnest('{fred,jim,fred,jack,jill,fred,jill,jim,jim,sheila,jim,sheila}'::text[]) u(x); + +-- check collation propagates up in suitable cases: +select pg_collation_for(percentile_disc(1) within group (order by x collate "POSIX")) + from (values ('fred'),('jim')) v(x); + +-- ordered-set aggs created with CREATE AGGREGATE +select test_rank(3) within group (order by x) +from (values (1),(1),(2),(2),(3),(3),(4)) v(x); +select test_percentile_disc(0.5) within group (order by thousand) from tenk1; + +-- ordered-set aggs can't use ungrouped vars in direct args: +select rank(x) within group (order by x) from generate_series(1,5) x; + +-- outer-level agg can't use a grouped arg of a lower level, either: +select array(select percentile_disc(a) within group (order by x) + from (values (0.3),(0.7)) v(a) group by a) + from generate_series(1,5) g(x); + +-- agg in the direct args is a grouping violation, too: +select rank(sum(x)) within group (order by x) from generate_series(1,5) x; + +-- hypothetical-set type unification and argument-count failures: +select rank(3) within group (order by x) from (values ('fred'),('jim')) v(x); +select rank(3) within group (order by stringu1,stringu2) from tenk1; +select rank('fred') within group (order by x) from generate_series(1,5) x; +select rank('adam'::text collate "C") within group (order by x collate "POSIX") + from (values ('fred'),('jim')) v(x); +-- hypothetical-set type unification successes: +select rank('adam'::varchar) within group (order by x) from (values ('fred'),('jim')) v(x); +select rank('3') within group (order by x) from generate_series(1,5) x; + +-- divide by zero check +select percent_rank(0) within group (order by x) from generate_series(1,0) x; + +-- deparse and multiple features: +create view aggordview1 as +select ten, + percentile_disc(0.5) within group (order by thousand) as p50, + percentile_disc(0.5) within group (order by thousand) filter (where hundred=1) as px, + rank(5,'AZZZZ',50) within group (order by hundred, string4 desc, hundred) + from tenk1 + group by ten order by ten; + +select pg_get_viewdef('aggordview1'); +select * from aggordview1 order by ten; +drop view aggordview1; + +-- variadic aggregates +select least_agg(q1,q2) from int8_tbl; +select least_agg(variadic array[q1,q2]) from int8_tbl; + +select cleast_agg(q1,q2) from int8_tbl; +select cleast_agg(4.5,f1) from int4_tbl; +select cleast_agg(variadic array[4.5,f1]) from int4_tbl; +select pg_typeof(cleast_agg(variadic array[4.5,f1])) from int4_tbl; + +-- test aggregates with common transition functions share the same states +begin work; + +create type avg_state as (total bigint, count bigint); + +create or replace function avg_transfn(state avg_state, n int) returns avg_state as +$$ +declare new_state avg_state; +begin + raise notice 'avg_transfn called with %', n; + if state is null then + if n is not null then + new_state.total := n; + new_state.count := 1; + return new_state; + end if; + return null; + elsif n is not null then + state.total := state.total + n; + state.count := state.count + 1; + return state; + end if; + + return null; +end +$$ language plpgsql; + +create function avg_finalfn(state avg_state) returns int4 as +$$ +begin + if state is null then + return NULL; + else + return state.total / state.count; + end if; +end +$$ language plpgsql; + +create function sum_finalfn(state avg_state) returns int4 as +$$ +begin + if state is null then + return NULL; + else + return state.total; + end if; +end +$$ language plpgsql; + +create aggregate my_avg(int4) +( + stype = avg_state, + sfunc = avg_transfn, + finalfunc = avg_finalfn +); + +create aggregate my_sum(int4) +( + stype = avg_state, + sfunc = avg_transfn, + finalfunc = sum_finalfn +); + +-- aggregate state should be shared as aggs are the same. +select my_avg(one),my_avg(one) from (values(1),(3)) t(one); + +-- aggregate state should be shared as transfn is the same for both aggs. +select my_avg(one),my_sum(one) from (values(1),(3)) t(one); + +-- same as previous one, but with DISTINCT, which requires sorting the input. +select my_avg(distinct one),my_sum(distinct one) from (values(1),(3),(1)) t(one); + +-- shouldn't share states due to the distinctness not matching. +select my_avg(distinct one),my_sum(one) from (values(1),(3)) t(one); + +-- shouldn't share states due to the filter clause not matching. +select my_avg(one) filter (where one > 1),my_sum(one) from (values(1),(3)) t(one); + +-- this should not share the state due to different input columns. +select my_avg(one),my_sum(two) from (values(1,2),(3,4)) t(one,two); + +-- exercise cases where OSAs share state +select + percentile_cont(0.5) within group (order by a), + percentile_disc(0.5) within group (order by a) +from (values(1::float8),(3),(5),(7)) t(a); + +select + percentile_cont(0.25) within group (order by a), + percentile_disc(0.5) within group (order by a) +from (values(1::float8),(3),(5),(7)) t(a); + +-- these can't share state currently +select + rank(4) within group (order by a), + dense_rank(4) within group (order by a) +from (values(1),(3),(5),(7)) t(a); + +-- test that aggs with the same sfunc and initcond share the same agg state +create aggregate my_sum_init(int4) +( + stype = avg_state, + sfunc = avg_transfn, + finalfunc = sum_finalfn, + initcond = '(10,0)' +); + +create aggregate my_avg_init(int4) +( + stype = avg_state, + sfunc = avg_transfn, + finalfunc = avg_finalfn, + initcond = '(10,0)' +); + +create aggregate my_avg_init2(int4) +( + stype = avg_state, + sfunc = avg_transfn, + finalfunc = avg_finalfn, + initcond = '(4,0)' +); + +-- state should be shared if INITCONDs are matching +select my_sum_init(one),my_avg_init(one) from (values(1),(3)) t(one); + +-- Varying INITCONDs should cause the states not to be shared. +select my_sum_init(one),my_avg_init2(one) from (values(1),(3)) t(one); + +rollback; + +-- test aggregate state sharing to ensure it works if one aggregate has a +-- finalfn and the other one has none. +begin work; + +create or replace function sum_transfn(state int4, n int4) returns int4 as +$$ +declare new_state int4; +begin + raise notice 'sum_transfn called with %', n; + if state is null then + if n is not null then + new_state := n; + return new_state; + end if; + return null; + elsif n is not null then + state := state + n; + return state; + end if; + + return null; +end +$$ language plpgsql; + +create function halfsum_finalfn(state int4) returns int4 as +$$ +begin + if state is null then + return NULL; + else + return state / 2; + end if; +end +$$ language plpgsql; + +create aggregate my_sum(int4) +( + stype = int4, + sfunc = sum_transfn +); + +create aggregate my_half_sum(int4) +( + stype = int4, + sfunc = sum_transfn, + finalfunc = halfsum_finalfn +); + +-- Agg state should be shared even though my_sum has no finalfn +select my_sum(one),my_half_sum(one) from (values(1),(2),(3),(4)) t(one); + +rollback; + + +-- test that the aggregate transition logic correctly handles +-- transition / combine functions returning NULL + +-- First test the case of a normal transition function returning NULL +BEGIN; +CREATE FUNCTION balkifnull(int8, int4) +RETURNS int8 +STRICT +LANGUAGE plpgsql AS $$ +BEGIN + IF $1 IS NULL THEN + RAISE 'erroneously called with NULL argument'; + END IF; + RETURN NULL; +END$$; + +CREATE AGGREGATE balk(int4) +( + SFUNC = balkifnull(int8, int4), + STYPE = int8, + PARALLEL = SAFE, + INITCOND = '0' +); + +SELECT balk(hundred) FROM tenk1; + +ROLLBACK; + +-- Secondly test the case of a parallel aggregate combiner function +-- returning NULL. For that use normal transition function, but a +-- combiner function returning NULL. +BEGIN ISOLATION LEVEL REPEATABLE READ; +CREATE FUNCTION balkifnull(int8, int8) +RETURNS int8 +PARALLEL SAFE +STRICT +LANGUAGE plpgsql AS $$ +BEGIN + IF $1 IS NULL THEN + RAISE 'erroneously called with NULL argument'; + END IF; + RETURN NULL; +END$$; + +CREATE AGGREGATE balk(int4) +( + SFUNC = int4_sum(int8, int4), + STYPE = int8, + COMBINEFUNC = balkifnull(int8, int8), + PARALLEL = SAFE, + INITCOND = '0' +); + +-- force use of parallelism +ALTER TABLE tenk1 set (parallel_workers = 4); +SET LOCAL parallel_setup_cost=0; +SET LOCAL max_parallel_workers_per_gather=4; + +EXPLAIN (COSTS OFF) SELECT balk(hundred) FROM tenk1; +SELECT balk(hundred) FROM tenk1; + +ROLLBACK; + +-- test coverage for aggregate combine/serial/deserial functions +BEGIN ISOLATION LEVEL REPEATABLE READ; + +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 4; +SET parallel_leader_participation = off; +SET enable_indexonlyscan = off; + +-- variance(int4) covers numeric_poly_combine +-- sum(int8) covers int8_avg_combine +-- regr_count(float8, float8) covers int8inc_float8_float8 and aggregates with > 1 arg +EXPLAIN (COSTS OFF, VERBOSE) +SELECT variance(unique1::int4), sum(unique1::int8), regr_count(unique1::float8, unique1::float8) +FROM (SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1) u; + +SELECT variance(unique1::int4), sum(unique1::int8), regr_count(unique1::float8, unique1::float8) +FROM (SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1) u; + +-- variance(int8) covers numeric_combine +-- avg(numeric) covers numeric_avg_combine +EXPLAIN (COSTS OFF, VERBOSE) +SELECT variance(unique1::int8), avg(unique1::numeric) +FROM (SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1) u; + +SELECT variance(unique1::int8), avg(unique1::numeric) +FROM (SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1 + UNION ALL SELECT * FROM tenk1) u; + +ROLLBACK; + +-- test coverage for dense_rank +SELECT dense_rank(x) WITHIN GROUP (ORDER BY x) FROM (VALUES (1),(1),(2),(2),(3),(3)) v(x) GROUP BY (x) ORDER BY 1; + + +-- Ensure that the STRICT checks for aggregates does not take NULLness +-- of ORDER BY columns into account. See bug report around +-- 2a505161-2727-2473-7c46-591ed108ac52@email.cz +SELECT min(x ORDER BY y) FROM (VALUES(1, NULL)) AS d(x,y); +SELECT min(x ORDER BY y) FROM (VALUES(1, 2)) AS d(x,y); + +-- check collation-sensitive matching between grouping expressions +select v||'a', case v||'a' when 'aa' then 1 else 0 end, count(*) + from unnest(array['a','b']) u(v) + group by v||'a' order by 1; +select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*) + from unnest(array['a','b']) u(v) + group by v||'a' order by 1; + +-- Make sure that generation of HashAggregate for uniqification purposes +-- does not lead to array overflow due to unexpected duplicate hash keys +-- see CAFeeJoKKu0u+A_A9R9316djW-YW3-+Gtgvy3ju655qRHR3jtdA@mail.gmail.com +explain (costs off) + select 1 from tenk1 + where (hundred, thousand) in (select twothousand, twothousand from onek); + +-- +-- Hash Aggregation Spill tests +-- + +set enable_sort=false; +set work_mem='64kB'; + +select unique1, count(*), sum(twothousand) from tenk1 +group by unique1 +having sum(fivethous) > 4975 +order by sum(twothousand); + +set work_mem to default; +set enable_sort to default; + +-- +-- Compare results between plans using sorting and plans using hash +-- aggregation. Force spilling in both cases by setting work_mem low. +-- + +set work_mem='64kB'; + +create table agg_data_2k as +select g from generate_series(0, 1999) g; +analyze agg_data_2k; + +create table agg_data_20k as +select g from generate_series(0, 19999) g; +analyze agg_data_20k; + +-- Produce results with sorting. + +set enable_hashagg = false; + +set jit_above_cost = 0; + +explain (costs off) +select g%10000 as c1, sum(g::numeric) as c2, count(*) as c3 + from agg_data_20k group by g%10000; + +create table agg_group_1 as +select g%10000 as c1, sum(g::numeric) as c2, count(*) as c3 + from agg_data_20k group by g%10000; + +create table agg_group_2 as +select * from + (values (100), (300), (500)) as r(a), + lateral ( + select (g/2)::numeric as c1, + array_agg(g::numeric) as c2, + count(*) as c3 + from agg_data_2k + where g < r.a + group by g/2) as s; + +set jit_above_cost to default; + +create table agg_group_3 as +select (g/2)::numeric as c1, sum(7::int4) as c2, count(*) as c3 + from agg_data_2k group by g/2; + +create table agg_group_4 as +select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 + from agg_data_2k group by g/2; + +-- Produce results with hash aggregation + +set enable_hashagg = true; +set enable_sort = false; + +set jit_above_cost = 0; + +explain (costs off) +select g%10000 as c1, sum(g::numeric) as c2, count(*) as c3 + from agg_data_20k group by g%10000; + +create table agg_hash_1 as +select g%10000 as c1, sum(g::numeric) as c2, count(*) as c3 + from agg_data_20k group by g%10000; + +create table agg_hash_2 as +select * from + (values (100), (300), (500)) as r(a), + lateral ( + select (g/2)::numeric as c1, + array_agg(g::numeric) as c2, + count(*) as c3 + from agg_data_2k + where g < r.a + group by g/2) as s; + +set jit_above_cost to default; + +create table agg_hash_3 as +select (g/2)::numeric as c1, sum(7::int4) as c2, count(*) as c3 + from agg_data_2k group by g/2; + +create table agg_hash_4 as +select (g/2)::numeric as c1, array_agg(g::numeric) as c2, count(*) as c3 + from agg_data_2k group by g/2; + +set enable_sort = true; +set work_mem to default; + +-- Compare group aggregation results to hash aggregation results + +(select * from agg_hash_1 except select * from agg_group_1) + union all +(select * from agg_group_1 except select * from agg_hash_1); + +(select * from agg_hash_2 except select * from agg_group_2) + union all +(select * from agg_group_2 except select * from agg_hash_2); + +(select * from agg_hash_3 except select * from agg_group_3) + union all +(select * from agg_group_3 except select * from agg_hash_3); + +(select * from agg_hash_4 except select * from agg_group_4) + union all +(select * from agg_group_4 except select * from agg_hash_4); + +drop table agg_group_1; +drop table agg_group_2; +drop table agg_group_3; +drop table agg_group_4; +drop table agg_hash_1; +drop table agg_hash_2; +drop table agg_hash_3; +drop table agg_hash_4; diff --git a/src/test/resources/postgresql-corpus/case.sql b/src/test/resources/postgresql-corpus/case.sql new file mode 100644 index 0000000..17436c5 --- /dev/null +++ b/src/test/resources/postgresql-corpus/case.sql @@ -0,0 +1,254 @@ +-- +-- CASE +-- Test the case statement +-- + +CREATE TABLE CASE_TBL ( + i integer, + f double precision +); + +CREATE TABLE CASE2_TBL ( + i integer, + j integer +); + +INSERT INTO CASE_TBL VALUES (1, 10.1); +INSERT INTO CASE_TBL VALUES (2, 20.2); +INSERT INTO CASE_TBL VALUES (3, -30.3); +INSERT INTO CASE_TBL VALUES (4, NULL); + +INSERT INTO CASE2_TBL VALUES (1, -1); +INSERT INTO CASE2_TBL VALUES (2, -2); +INSERT INTO CASE2_TBL VALUES (3, -3); +INSERT INTO CASE2_TBL VALUES (2, -4); +INSERT INTO CASE2_TBL VALUES (1, NULL); +INSERT INTO CASE2_TBL VALUES (NULL, -6); + +-- +-- Simplest examples without tables +-- + +SELECT '3' AS "One", + CASE + WHEN 1 < 2 THEN 3 + END AS "Simple WHEN"; + +SELECT '' AS "One", + CASE + WHEN 1 > 2 THEN 3 + END AS "Simple default"; + +SELECT '3' AS "One", + CASE + WHEN 1 < 2 THEN 3 + ELSE 4 + END AS "Simple ELSE"; + +SELECT '4' AS "One", + CASE + WHEN 1 > 2 THEN 3 + ELSE 4 + END AS "ELSE default"; + +SELECT '6' AS "One", + CASE + WHEN 1 > 2 THEN 3 + WHEN 4 < 5 THEN 6 + ELSE 7 + END AS "Two WHEN with default"; + + +SELECT '7' AS "None", + CASE WHEN random() < 0 THEN 1 + END AS "NULL on no matches"; + +-- Constant-expression folding shouldn't evaluate unreachable subexpressions +SELECT CASE WHEN 1=0 THEN 1/0 WHEN 1=1 THEN 1 ELSE 2/0 END; +SELECT CASE 1 WHEN 0 THEN 1/0 WHEN 1 THEN 1 ELSE 2/0 END; + +-- However we do not currently suppress folding of potentially +-- reachable subexpressions +SELECT CASE WHEN i > 100 THEN 1/0 ELSE 0 END FROM case_tbl; + +-- Test for cases involving untyped literals in test expression +SELECT CASE 'a' WHEN 'a' THEN 1 ELSE 2 END; + +-- +-- Examples of targets involving tables +-- + +SELECT '' AS "Five", + CASE + WHEN i >= 3 THEN i + END AS ">= 3 or Null" + FROM CASE_TBL; + +SELECT '' AS "Five", + CASE WHEN i >= 3 THEN (i + i) + ELSE i + END AS "Simplest Math" + FROM CASE_TBL; + +SELECT '' AS "Five", i AS "Value", + CASE WHEN (i < 0) THEN 'small' + WHEN (i = 0) THEN 'zero' + WHEN (i = 1) THEN 'one' + WHEN (i = 2) THEN 'two' + ELSE 'big' + END AS "Category" + FROM CASE_TBL; + +SELECT '' AS "Five", + CASE WHEN ((i < 0) or (i < 0)) THEN 'small' + WHEN ((i = 0) or (i = 0)) THEN 'zero' + WHEN ((i = 1) or (i = 1)) THEN 'one' + WHEN ((i = 2) or (i = 2)) THEN 'two' + ELSE 'big' + END AS "Category" + FROM CASE_TBL; + +-- +-- Examples of qualifications involving tables +-- + +-- +-- NULLIF() and COALESCE() +-- Shorthand forms for typical CASE constructs +-- defined in the SQL standard. +-- + +SELECT * FROM CASE_TBL WHERE COALESCE(f,i) = 4; + +SELECT * FROM CASE_TBL WHERE NULLIF(f,i) = 2; + +SELECT COALESCE(a.f, b.i, b.j) + FROM CASE_TBL a, CASE2_TBL b; + +SELECT * + FROM CASE_TBL a, CASE2_TBL b + WHERE COALESCE(a.f, b.i, b.j) = 2; + +SELECT '' AS Five, NULLIF(a.i,b.i) AS "NULLIF(a.i,b.i)", + NULLIF(b.i, 4) AS "NULLIF(b.i,4)" + FROM CASE_TBL a, CASE2_TBL b; + +SELECT '' AS "Two", * + FROM CASE_TBL a, CASE2_TBL b + WHERE COALESCE(f,b.i) = 2; + +-- +-- Examples of updates involving tables +-- + +UPDATE CASE_TBL + SET i = CASE WHEN i >= 3 THEN (- i) + ELSE (2 * i) END; + +SELECT * FROM CASE_TBL; + +UPDATE CASE_TBL + SET i = CASE WHEN i >= 2 THEN (2 * i) + ELSE (3 * i) END; + +SELECT * FROM CASE_TBL; + +UPDATE CASE_TBL + SET i = CASE WHEN b.i >= 2 THEN (2 * j) + ELSE (3 * j) END + FROM CASE2_TBL b + WHERE j = -CASE_TBL.i; + +SELECT * FROM CASE_TBL; + +-- +-- Nested CASE expressions +-- + +-- This test exercises a bug caused by aliasing econtext->caseValue_isNull +-- with the isNull argument of the inner CASE's CaseExpr evaluation. After +-- evaluating the vol(null) expression in the inner CASE's second WHEN-clause, +-- the isNull flag for the case test value incorrectly became true, causing +-- the third WHEN-clause not to match. The volatile function calls are needed +-- to prevent constant-folding in the planner, which would hide the bug. + +-- Wrap this in a single transaction so the transient '=' operator doesn't +-- cause problems in concurrent sessions +BEGIN; + +CREATE FUNCTION vol(text) returns text as + 'begin return $1; end' language plpgsql volatile; + +SELECT CASE + (CASE vol('bar') + WHEN 'foo' THEN 'it was foo!' + WHEN vol(null) THEN 'null input' + WHEN 'bar' THEN 'it was bar!' END + ) + WHEN 'it was foo!' THEN 'foo recognized' + WHEN 'it was bar!' THEN 'bar recognized' + ELSE 'unrecognized' END; + +-- In this case, we can't inline the SQL function without confusing things. +CREATE DOMAIN foodomain AS text; + +CREATE FUNCTION volfoo(text) returns foodomain as + 'begin return $1::foodomain; end' language plpgsql volatile; + +CREATE FUNCTION inline_eq(foodomain, foodomain) returns boolean as + 'SELECT CASE $2::text WHEN $1::text THEN true ELSE false END' language sql; + +CREATE OPERATOR = (procedure = inline_eq, + leftarg = foodomain, rightarg = foodomain); + +SELECT CASE volfoo('bar') WHEN 'foo'::foodomain THEN 'is foo' ELSE 'is not foo' END; + +ROLLBACK; + +-- Test multiple evaluation of a CASE arg that is a read/write object (#14472) +-- Wrap this in a single transaction so the transient '=' operator doesn't +-- cause problems in concurrent sessions +BEGIN; + +CREATE DOMAIN arrdomain AS int[]; + +CREATE FUNCTION make_ad(int,int) returns arrdomain as + 'declare x arrdomain; + begin + x := array[$1,$2]; + return x; + end' language plpgsql volatile; + +CREATE FUNCTION ad_eq(arrdomain, arrdomain) returns boolean as + 'begin return array_eq($1, $2); end' language plpgsql; + +CREATE OPERATOR = (procedure = ad_eq, + leftarg = arrdomain, rightarg = arrdomain); + +SELECT CASE make_ad(1,2) + WHEN array[2,4]::arrdomain THEN 'wrong' + WHEN array[2,5]::arrdomain THEN 'still wrong' + WHEN array[1,2]::arrdomain THEN 'right' + END; + +ROLLBACK; + +-- Test interaction of CASE with ArrayCoerceExpr (bug #15471) +BEGIN; + +CREATE TYPE casetestenum AS ENUM ('e', 'f', 'g'); + +SELECT + CASE 'foo'::text + WHEN 'foo' THEN ARRAY['a', 'b', 'c', 'd'] || enum_range(NULL::casetestenum)::text[] + ELSE ARRAY['x', 'y'] + END; + +ROLLBACK; + +-- +-- Clean up +-- + +DROP TABLE CASE_TBL; +DROP TABLE CASE2_TBL; diff --git a/src/test/resources/postgresql-corpus/delete.sql b/src/test/resources/postgresql-corpus/delete.sql new file mode 100644 index 0000000..d8cb99e --- /dev/null +++ b/src/test/resources/postgresql-corpus/delete.sql @@ -0,0 +1,25 @@ +CREATE TABLE delete_test ( + id SERIAL PRIMARY KEY, + a INT, + b text +); + +INSERT INTO delete_test (a) VALUES (10); +INSERT INTO delete_test (a, b) VALUES (50, repeat('x', 10000)); +INSERT INTO delete_test (a) VALUES (100); + +-- allow an alias to be specified for DELETE's target table +DELETE FROM delete_test AS dt WHERE dt.a > 75; + +-- if an alias is specified, don't allow the original table name +-- to be referenced +DELETE FROM delete_test dt WHERE delete_test.a > 25; + +SELECT id, a, char_length(b) FROM delete_test; + +-- delete a row with a TOASTed value +DELETE FROM delete_test WHERE a > 25; + +SELECT id, a, char_length(b) FROM delete_test; + +DROP TABLE delete_test; diff --git a/src/test/resources/postgresql-corpus/insert.sql b/src/test/resources/postgresql-corpus/insert.sql new file mode 100644 index 0000000..67c263c --- /dev/null +++ b/src/test/resources/postgresql-corpus/insert.sql @@ -0,0 +1,608 @@ +-- +-- insert with DEFAULT in the target_list +-- +create table inserttest (col1 int4, col2 int4 NOT NULL, col3 text default 'testing'); +insert into inserttest (col1, col2, col3) values (DEFAULT, DEFAULT, DEFAULT); +insert into inserttest (col2, col3) values (3, DEFAULT); +insert into inserttest (col1, col2, col3) values (DEFAULT, 5, DEFAULT); +insert into inserttest values (DEFAULT, 5, 'test'); +insert into inserttest values (DEFAULT, 7); + +select * from inserttest; + +-- +-- insert with similar expression / target_list values (all fail) +-- +insert into inserttest (col1, col2, col3) values (DEFAULT, DEFAULT); +insert into inserttest (col1, col2, col3) values (1, 2); +insert into inserttest (col1) values (1, 2); +insert into inserttest (col1) values (DEFAULT, DEFAULT); + +select * from inserttest; + +-- +-- VALUES test +-- +insert into inserttest values(10, 20, '40'), (-1, 2, DEFAULT), + ((select 2), (select i from (values(3)) as foo (i)), 'values are fun!'); + +select * from inserttest; + +-- +-- TOASTed value test +-- +insert into inserttest values(30, 50, repeat('x', 10000)); + +select col1, col2, char_length(col3) from inserttest; + +drop table inserttest; + +-- +-- check indirection (field/array assignment), cf bug #14265 +-- +-- these tests are aware that transformInsertStmt has 3 separate code paths +-- + +create type insert_test_type as (if1 int, if2 text[]); + +create table inserttest (f1 int, f2 int[], + f3 insert_test_type, f4 insert_test_type[]); + +insert into inserttest (f2[1], f2[2]) values (1,2); +insert into inserttest (f2[1], f2[2]) values (3,4), (5,6); +insert into inserttest (f2[1], f2[2]) select 7,8; +insert into inserttest (f2[1], f2[2]) values (1,default); -- not supported + +insert into inserttest (f3.if1, f3.if2) values (1,array['foo']); +insert into inserttest (f3.if1, f3.if2) values (1,'{foo}'), (2,'{bar}'); +insert into inserttest (f3.if1, f3.if2) select 3, '{baz,quux}'; +insert into inserttest (f3.if1, f3.if2) values (1,default); -- not supported + +insert into inserttest (f3.if2[1], f3.if2[2]) values ('foo', 'bar'); +insert into inserttest (f3.if2[1], f3.if2[2]) values ('foo', 'bar'), ('baz', 'quux'); +insert into inserttest (f3.if2[1], f3.if2[2]) select 'bear', 'beer'; + +insert into inserttest (f4[1].if2[1], f4[1].if2[2]) values ('foo', 'bar'); +insert into inserttest (f4[1].if2[1], f4[1].if2[2]) values ('foo', 'bar'), ('baz', 'quux'); +insert into inserttest (f4[1].if2[1], f4[1].if2[2]) select 'bear', 'beer'; + +select * from inserttest; + +-- also check reverse-listing +create table inserttest2 (f1 bigint, f2 text); +create rule irule1 as on insert to inserttest2 do also + insert into inserttest (f3.if2[1], f3.if2[2]) + values (new.f1,new.f2); +create rule irule2 as on insert to inserttest2 do also + insert into inserttest (f4[1].if1, f4[1].if2[2]) + values (1,'fool'),(new.f1,new.f2); +create rule irule3 as on insert to inserttest2 do also + insert into inserttest (f4[1].if1, f4[1].if2[2]) + select new.f1, new.f2; +\d+ inserttest2 + +drop table inserttest2; +drop table inserttest; +drop type insert_test_type; + +-- direct partition inserts should check partition bound constraint +create table range_parted ( + a text, + b int +) partition by range (a, (b+0)); + +-- no partitions, so fail +insert into range_parted values ('a', 11); + +create table part1 partition of range_parted for values from ('a', 1) to ('a', 10); +create table part2 partition of range_parted for values from ('a', 10) to ('a', 20); +create table part3 partition of range_parted for values from ('b', 1) to ('b', 10); +create table part4 partition of range_parted for values from ('b', 10) to ('b', 20); + +-- fail +insert into part1 values ('a', 11); +insert into part1 values ('b', 1); +-- ok +insert into part1 values ('a', 1); +-- fail +insert into part4 values ('b', 21); +insert into part4 values ('a', 10); +-- ok +insert into part4 values ('b', 10); + +-- fail (partition key a has a NOT NULL constraint) +insert into part1 values (null); +-- fail (expression key (b+0) cannot be null either) +insert into part1 values (1); + +create table list_parted ( + a text, + b int +) partition by list (lower(a)); +create table part_aa_bb partition of list_parted FOR VALUES IN ('aa', 'bb'); +create table part_cc_dd partition of list_parted FOR VALUES IN ('cc', 'dd'); +create table part_null partition of list_parted FOR VALUES IN (null); + +-- fail +insert into part_aa_bb values ('cc', 1); +insert into part_aa_bb values ('AAa', 1); +insert into part_aa_bb values (null); +-- ok +insert into part_cc_dd values ('cC', 1); +insert into part_null values (null, 0); + +-- check in case of multi-level partitioned table +create table part_ee_ff partition of list_parted for values in ('ee', 'ff') partition by range (b); +create table part_ee_ff1 partition of part_ee_ff for values from (1) to (10); +create table part_ee_ff2 partition of part_ee_ff for values from (10) to (20); + +-- test default partition +create table part_default partition of list_parted default; +-- Negative test: a row, which would fit in other partition, does not fit +-- default partition, even when inserted directly +insert into part_default values ('aa', 2); +insert into part_default values (null, 2); +-- ok +insert into part_default values ('Zz', 2); +-- test if default partition works as expected for multi-level partitioned +-- table as well as when default partition itself is further partitioned +drop table part_default; +create table part_xx_yy partition of list_parted for values in ('xx', 'yy') partition by list (a); +create table part_xx_yy_p1 partition of part_xx_yy for values in ('xx'); +create table part_xx_yy_defpart partition of part_xx_yy default; +create table part_default partition of list_parted default partition by range(b); +create table part_default_p1 partition of part_default for values from (20) to (30); +create table part_default_p2 partition of part_default for values from (30) to (40); + +-- fail +insert into part_ee_ff1 values ('EE', 11); +insert into part_default_p2 values ('gg', 43); +-- fail (even the parent's, ie, part_ee_ff's partition constraint applies) +insert into part_ee_ff1 values ('cc', 1); +insert into part_default values ('gg', 43); +-- ok +insert into part_ee_ff1 values ('ff', 1); +insert into part_ee_ff2 values ('ff', 11); +insert into part_default_p1 values ('cd', 25); +insert into part_default_p2 values ('de', 35); +insert into list_parted values ('ab', 21); +insert into list_parted values ('xx', 1); +insert into list_parted values ('yy', 2); +select tableoid::regclass, * from list_parted; + +-- Check tuple routing for partitioned tables + +-- fail +insert into range_parted values ('a', 0); +-- ok +insert into range_parted values ('a', 1); +insert into range_parted values ('a', 10); +-- fail +insert into range_parted values ('a', 20); +-- ok +insert into range_parted values ('b', 1); +insert into range_parted values ('b', 10); +-- fail (partition key (b+0) is null) +insert into range_parted values ('a'); + +-- Check default partition +create table part_def partition of range_parted default; +-- fail +insert into part_def values ('b', 10); +-- ok +insert into part_def values ('c', 10); +insert into range_parted values (null, null); +insert into range_parted values ('a', null); +insert into range_parted values (null, 19); +insert into range_parted values ('b', 20); + +select tableoid::regclass, * from range_parted; +-- ok +insert into list_parted values (null, 1); +insert into list_parted (a) values ('aA'); +-- fail (partition of part_ee_ff not found in both cases) +insert into list_parted values ('EE', 0); +insert into part_ee_ff values ('EE', 0); +-- ok +insert into list_parted values ('EE', 1); +insert into part_ee_ff values ('EE', 10); +select tableoid::regclass, * from list_parted; + +-- some more tests to exercise tuple-routing with multi-level partitioning +create table part_gg partition of list_parted for values in ('gg') partition by range (b); +create table part_gg1 partition of part_gg for values from (minvalue) to (1); +create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b); +create table part_gg2_1 partition of part_gg2 for values from (1) to (5); +create table part_gg2_2 partition of part_gg2 for values from (5) to (10); + +create table part_ee_ff3 partition of part_ee_ff for values from (20) to (30) partition by range (b); +create table part_ee_ff3_1 partition of part_ee_ff3 for values from (20) to (25); +create table part_ee_ff3_2 partition of part_ee_ff3 for values from (25) to (30); + +truncate list_parted; +insert into list_parted values ('aa'), ('cc'); +insert into list_parted select 'Ff', s.a from generate_series(1, 29) s(a); +insert into list_parted select 'gg', s.a from generate_series(1, 9) s(a); +insert into list_parted (b) values (1); +select tableoid::regclass::text, a, min(b) as min_b, max(b) as max_b from list_parted group by 1, 2 order by 1; + +-- direct partition inserts should check hash partition bound constraint + +-- Use hand-rolled hash functions and operator classes to get predictable +-- result on different machines. The hash function for int4 simply returns +-- the sum of the values passed to it and the one for text returns the length +-- of the non-empty string value passed to it or 0. + +create or replace function part_hashint4_noop(value int4, seed int8) +returns int8 as $$ +select value + seed; +$$ language sql immutable; + +create operator class part_test_int4_ops +for type int4 +using hash as +operator 1 =, +function 2 part_hashint4_noop(int4, int8); + +create or replace function part_hashtext_length(value text, seed int8) +RETURNS int8 AS $$ +select length(coalesce(value, ''))::int8 +$$ language sql immutable; + +create operator class part_test_text_ops +for type text +using hash as +operator 1 =, +function 2 part_hashtext_length(text, int8); + +create table hash_parted ( + a int +) partition by hash (a part_test_int4_ops); +create table hpart0 partition of hash_parted for values with (modulus 4, remainder 0); +create table hpart1 partition of hash_parted for values with (modulus 4, remainder 1); +create table hpart2 partition of hash_parted for values with (modulus 4, remainder 2); +create table hpart3 partition of hash_parted for values with (modulus 4, remainder 3); + +insert into hash_parted values(generate_series(1,10)); + +-- direct insert of values divisible by 4 - ok; +insert into hpart0 values(12),(16); +-- fail; +insert into hpart0 values(11); +-- 11 % 4 -> 3 remainder i.e. valid data for hpart3 partition +insert into hpart3 values(11); + +-- view data +select tableoid::regclass as part, a, a%4 as "remainder = a % 4" +from hash_parted order by part; + +-- test \d+ output on a table which has both partitioned and unpartitioned +-- partitions +\d+ list_parted + +-- cleanup +drop table range_parted, list_parted; +drop table hash_parted; + +-- test that a default partition added as the first partition accepts any value +-- including null +create table list_parted (a int) partition by list (a); +create table part_default partition of list_parted default; +\d+ part_default +insert into part_default values (null); +insert into part_default values (1); +insert into part_default values (-1); +select tableoid::regclass, a from list_parted; +-- cleanup +drop table list_parted; + +-- more tests for certain multi-level partitioning scenarios +create table mlparted (a int, b int) partition by range (a, b); +create table mlparted1 (b int not null, a int not null) partition by range ((b+0)); +create table mlparted11 (like mlparted1); +alter table mlparted11 drop a; +alter table mlparted11 add a int; +alter table mlparted11 drop a; +alter table mlparted11 add a int not null; +-- attnum for key attribute 'a' is different in mlparted, mlparted1, and mlparted11 +select attrelid::regclass, attname, attnum +from pg_attribute +where attname = 'a' + and (attrelid = 'mlparted'::regclass + or attrelid = 'mlparted1'::regclass + or attrelid = 'mlparted11'::regclass) +order by attrelid::regclass::text; + +alter table mlparted1 attach partition mlparted11 for values from (2) to (5); +alter table mlparted attach partition mlparted1 for values from (1, 2) to (1, 10); + +-- check that "(1, 2)" is correctly routed to mlparted11. +insert into mlparted values (1, 2); +select tableoid::regclass, * from mlparted; + +-- check that proper message is shown after failure to route through mlparted1 +insert into mlparted (a, b) values (1, 5); + +truncate mlparted; +alter table mlparted add constraint check_b check (b = 3); + +-- have a BR trigger modify the row such that the check_b is violated +create function mlparted11_trig_fn() +returns trigger AS +$$ +begin + NEW.b := 4; + return NEW; +end; +$$ +language plpgsql; +create trigger mlparted11_trig before insert ON mlparted11 + for each row execute procedure mlparted11_trig_fn(); + +-- check that the correct row is shown when constraint check_b fails after +-- "(1, 2)" is routed to mlparted11 (actually "(1, 4)" would be shown due +-- to the BR trigger mlparted11_trig_fn) +insert into mlparted values (1, 2); +drop trigger mlparted11_trig on mlparted11; +drop function mlparted11_trig_fn(); + +-- check that inserting into an internal partition successfully results in +-- checking its partition constraint before inserting into the leaf partition +-- selected by tuple-routing +insert into mlparted1 (a, b) values (2, 3); + +-- check routing error through a list partitioned table when the key is null +create table lparted_nonullpart (a int, b char) partition by list (b); +create table lparted_nonullpart_a partition of lparted_nonullpart for values in ('a'); +insert into lparted_nonullpart values (1); +drop table lparted_nonullpart; + +-- check that RETURNING works correctly with tuple-routing +alter table mlparted drop constraint check_b; +create table mlparted12 partition of mlparted1 for values from (5) to (10); +create table mlparted2 (b int not null, a int not null); +alter table mlparted attach partition mlparted2 for values from (1, 10) to (1, 20); +create table mlparted3 partition of mlparted for values from (1, 20) to (1, 30); +create table mlparted4 (like mlparted); +alter table mlparted4 drop a; +alter table mlparted4 add a int not null; +alter table mlparted attach partition mlparted4 for values from (1, 30) to (1, 40); +with ins (a, b, c) as + (insert into mlparted (b, a) select s.a, 1 from generate_series(2, 39) s(a) returning tableoid::regclass, *) + select a, b, min(c), max(c) from ins group by a, b order by 1; + +alter table mlparted add c text; +create table mlparted5 (c text, a int not null, b int not null) partition by list (c); +create table mlparted5a (a int not null, c text, b int not null); +alter table mlparted5 attach partition mlparted5a for values in ('a'); +alter table mlparted attach partition mlparted5 for values from (1, 40) to (1, 50); +alter table mlparted add constraint check_b check (a = 1 and b < 45); +insert into mlparted values (1, 45, 'a'); +create function mlparted5abrtrig_func() returns trigger as $$ begin new.c = 'b'; return new; end; $$ language plpgsql; +create trigger mlparted5abrtrig before insert on mlparted5a for each row execute procedure mlparted5abrtrig_func(); +insert into mlparted5 (a, b, c) values (1, 40, 'a'); +drop table mlparted5; +alter table mlparted drop constraint check_b; + +-- Check multi-level default partition +create table mlparted_def partition of mlparted default partition by range(a); +create table mlparted_def1 partition of mlparted_def for values from (40) to (50); +create table mlparted_def2 partition of mlparted_def for values from (50) to (60); +insert into mlparted values (40, 100); +insert into mlparted_def1 values (42, 100); +insert into mlparted_def2 values (54, 50); +-- fail +insert into mlparted values (70, 100); +insert into mlparted_def1 values (52, 50); +insert into mlparted_def2 values (34, 50); +-- ok +create table mlparted_defd partition of mlparted_def default; +insert into mlparted values (70, 100); + +select tableoid::regclass, * from mlparted_def; + +-- Check multi-level tuple routing with attributes dropped from the +-- top-most parent. First remove the last attribute. +alter table mlparted add d int, add e int; +alter table mlparted drop e; +create table mlparted5 partition of mlparted + for values from (1, 40) to (1, 50) partition by range (c); +create table mlparted5_ab partition of mlparted5 + for values from ('a') to ('c') partition by list (c); +-- This partitioned table should remain with no partitions. +create table mlparted5_cd partition of mlparted5 + for values from ('c') to ('e') partition by list (c); +create table mlparted5_a partition of mlparted5_ab for values in ('a'); +create table mlparted5_b (d int, b int, c text, a int); +alter table mlparted5_ab attach partition mlparted5_b for values in ('b'); +truncate mlparted; +insert into mlparted values (1, 2, 'a', 1); +insert into mlparted values (1, 40, 'a', 1); -- goes to mlparted5_a +insert into mlparted values (1, 45, 'b', 1); -- goes to mlparted5_b +insert into mlparted values (1, 45, 'c', 1); -- goes to mlparted5_cd, fails +insert into mlparted values (1, 45, 'f', 1); -- goes to mlparted5, fails +select tableoid::regclass, * from mlparted order by a, b, c, d; +alter table mlparted drop d; +truncate mlparted; +-- Remove the before last attribute. +alter table mlparted add e int, add d int; +alter table mlparted drop e; +insert into mlparted values (1, 2, 'a', 1); +insert into mlparted values (1, 40, 'a', 1); -- goes to mlparted5_a +insert into mlparted values (1, 45, 'b', 1); -- goes to mlparted5_b +insert into mlparted values (1, 45, 'c', 1); -- goes to mlparted5_cd, fails +insert into mlparted values (1, 45, 'f', 1); -- goes to mlparted5, fails +select tableoid::regclass, * from mlparted order by a, b, c, d; +alter table mlparted drop d; +drop table mlparted5; + +-- check that message shown after failure to find a partition shows the +-- appropriate key description (or none) in various situations +create table key_desc (a int, b int) partition by list ((a+0)); +create table key_desc_1 partition of key_desc for values in (1) partition by range (b); + +create user regress_insert_other_user; +grant select (a) on key_desc_1 to regress_insert_other_user; +grant insert on key_desc to regress_insert_other_user; + +set role regress_insert_other_user; +-- no key description is shown +insert into key_desc values (1, 1); + +reset role; +grant select (b) on key_desc_1 to regress_insert_other_user; +set role regress_insert_other_user; +-- key description (b)=(1) is now shown +insert into key_desc values (1, 1); + +-- key description is not shown if key contains expression +insert into key_desc values (2, 1); +reset role; +revoke all on key_desc from regress_insert_other_user; +revoke all on key_desc_1 from regress_insert_other_user; +drop role regress_insert_other_user; +drop table key_desc, key_desc_1; + +-- test minvalue/maxvalue restrictions +create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c); +create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, maxvalue, maxvalue); +create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, minvalue); +create table mcrparted4 partition of mcrparted for values from (21, minvalue, 0) to (30, 20, minvalue); + +-- check multi-column range partitioning expression enforces the same +-- constraint as what tuple-routing would determine it to be +create table mcrparted0 partition of mcrparted for values from (minvalue, minvalue, minvalue) to (1, maxvalue, maxvalue); +create table mcrparted1 partition of mcrparted for values from (2, 1, minvalue) to (10, 5, 10); +create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, maxvalue); +create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10); +create table mcrparted4 partition of mcrparted for values from (21, minvalue, minvalue) to (30, 20, maxvalue); +create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (maxvalue, maxvalue, maxvalue); + +-- null not allowed in range partition +insert into mcrparted values (null, null, null); + +-- routed to mcrparted0 +insert into mcrparted values (0, 1, 1); +insert into mcrparted0 values (0, 1, 1); + +-- routed to mcparted1 +insert into mcrparted values (9, 1000, 1); +insert into mcrparted1 values (9, 1000, 1); +insert into mcrparted values (10, 5, -1); +insert into mcrparted1 values (10, 5, -1); +insert into mcrparted values (2, 1, 0); +insert into mcrparted1 values (2, 1, 0); + +-- routed to mcparted2 +insert into mcrparted values (10, 6, 1000); +insert into mcrparted2 values (10, 6, 1000); +insert into mcrparted values (10, 1000, 1000); +insert into mcrparted2 values (10, 1000, 1000); + +-- no partition exists, nor does mcrparted3 accept it +insert into mcrparted values (11, 1, -1); +insert into mcrparted3 values (11, 1, -1); + +-- routed to mcrparted5 +insert into mcrparted values (30, 21, 20); +insert into mcrparted5 values (30, 21, 20); +insert into mcrparted4 values (30, 21, 20); -- error + +-- check rows +select tableoid::regclass::text, * from mcrparted order by 1; + +-- cleanup +drop table mcrparted; + +-- check that a BR constraint can't make partition contain violating rows +create table brtrigpartcon (a int, b text) partition by list (a); +create table brtrigpartcon1 partition of brtrigpartcon for values in (1); +create or replace function brtrigpartcon1trigf() returns trigger as $$begin new.a := 2; return new; end$$ language plpgsql; +create trigger brtrigpartcon1trig before insert on brtrigpartcon1 for each row execute procedure brtrigpartcon1trigf(); +insert into brtrigpartcon values (1, 'hi there'); +insert into brtrigpartcon1 values (1, 'hi there'); + +-- check that the message shows the appropriate column description in a +-- situation where the partitioned table is not the primary ModifyTable node +create table inserttest3 (f1 text default 'foo', f2 text default 'bar', f3 int); +create role regress_coldesc_role; +grant insert on inserttest3 to regress_coldesc_role; +grant insert on brtrigpartcon to regress_coldesc_role; +revoke select on brtrigpartcon from regress_coldesc_role; +set role regress_coldesc_role; +with result as (insert into brtrigpartcon values (1, 'hi there') returning 1) + insert into inserttest3 (f3) select * from result; +reset role; + +-- cleanup +revoke all on inserttest3 from regress_coldesc_role; +revoke all on brtrigpartcon from regress_coldesc_role; +drop role regress_coldesc_role; +drop table inserttest3; +drop table brtrigpartcon; +drop function brtrigpartcon1trigf(); + +-- check that "do nothing" BR triggers work with tuple-routing (this checks +-- that estate->es_result_relation_info is appropriately set/reset for each +-- routed tuple) +create table donothingbrtrig_test (a int, b text) partition by list (a); +create table donothingbrtrig_test1 (b text, a int); +create table donothingbrtrig_test2 (c text, b text, a int); +alter table donothingbrtrig_test2 drop column c; +create or replace function donothingbrtrig_func() returns trigger as $$begin raise notice 'b: %', new.b; return NULL; end$$ language plpgsql; +create trigger donothingbrtrig1 before insert on donothingbrtrig_test1 for each row execute procedure donothingbrtrig_func(); +create trigger donothingbrtrig2 before insert on donothingbrtrig_test2 for each row execute procedure donothingbrtrig_func(); +alter table donothingbrtrig_test attach partition donothingbrtrig_test1 for values in (1); +alter table donothingbrtrig_test attach partition donothingbrtrig_test2 for values in (2); +insert into donothingbrtrig_test values (1, 'foo'), (2, 'bar'); +copy donothingbrtrig_test from stdout; +/* +1 baz +2 qux +\. +*/ +select tableoid::regclass, * from donothingbrtrig_test; + +-- cleanup +drop table donothingbrtrig_test; +drop function donothingbrtrig_func(); + +-- check multi-column range partitioning with minvalue/maxvalue constraints +create table mcrparted (a text, b int) partition by range(a, b); +create table mcrparted1_lt_b partition of mcrparted for values from (minvalue, minvalue) to ('b', minvalue); +create table mcrparted2_b partition of mcrparted for values from ('b', minvalue) to ('c', minvalue); +create table mcrparted3_c_to_common partition of mcrparted for values from ('c', minvalue) to ('common', minvalue); +create table mcrparted4_common_lt_0 partition of mcrparted for values from ('common', minvalue) to ('common', 0); +create table mcrparted5_common_0_to_10 partition of mcrparted for values from ('common', 0) to ('common', 10); +create table mcrparted6_common_ge_10 partition of mcrparted for values from ('common', 10) to ('common', maxvalue); +create table mcrparted7_gt_common_lt_d partition of mcrparted for values from ('common', maxvalue) to ('d', minvalue); +create table mcrparted8_ge_d partition of mcrparted for values from ('d', minvalue) to (maxvalue, maxvalue); + +\d+ mcrparted +\d+ mcrparted1_lt_b +\d+ mcrparted2_b +\d+ mcrparted3_c_to_common +\d+ mcrparted4_common_lt_0 +\d+ mcrparted5_common_0_to_10 +\d+ mcrparted6_common_ge_10 +\d+ mcrparted7_gt_common_lt_d +\d+ mcrparted8_ge_d + +insert into mcrparted values ('aaa', 0), ('b', 0), ('bz', 10), ('c', -10), + ('comm', -10), ('common', -10), ('common', 0), ('common', 10), + ('commons', 0), ('d', -10), ('e', 0); +select tableoid::regclass, * from mcrparted order by a, b; +drop table mcrparted; + +-- check that wholerow vars in the RETURNING list work with partitioned tables +create table returningwrtest (a int) partition by list (a); +create table returningwrtest1 partition of returningwrtest for values in (1); +insert into returningwrtest values (1) returning returningwrtest; + +-- check also that the wholerow vars in RETURNING list are converted as needed +alter table returningwrtest add b text; +create table returningwrtest2 (b text, c int, a int); +alter table returningwrtest2 drop c; +alter table returningwrtest attach partition returningwrtest2 for values in (2); +insert into returningwrtest values (2, 'foo') returning returningwrtest; +drop table returningwrtest; diff --git a/src/test/resources/postgresql-corpus/join.sql b/src/test/resources/postgresql-corpus/join.sql new file mode 100644 index 0000000..1403e0f --- /dev/null +++ b/src/test/resources/postgresql-corpus/join.sql @@ -0,0 +1,2173 @@ +-- +-- JOIN +-- Test JOIN clauses +-- + +CREATE TABLE J1_TBL ( + i integer, + j integer, + t text +); + +CREATE TABLE J2_TBL ( + i integer, + k integer +); + + +INSERT INTO J1_TBL VALUES (1, 4, 'one'); +INSERT INTO J1_TBL VALUES (2, 3, 'two'); +INSERT INTO J1_TBL VALUES (3, 2, 'three'); +INSERT INTO J1_TBL VALUES (4, 1, 'four'); +INSERT INTO J1_TBL VALUES (5, 0, 'five'); +INSERT INTO J1_TBL VALUES (6, 6, 'six'); +INSERT INTO J1_TBL VALUES (7, 7, 'seven'); +INSERT INTO J1_TBL VALUES (8, 8, 'eight'); +INSERT INTO J1_TBL VALUES (0, NULL, 'zero'); +INSERT INTO J1_TBL VALUES (NULL, NULL, 'null'); +INSERT INTO J1_TBL VALUES (NULL, 0, 'zero'); + +INSERT INTO J2_TBL VALUES (1, -1); +INSERT INTO J2_TBL VALUES (2, 2); +INSERT INTO J2_TBL VALUES (3, -3); +INSERT INTO J2_TBL VALUES (2, 4); +INSERT INTO J2_TBL VALUES (5, -5); +INSERT INTO J2_TBL VALUES (5, -5); +INSERT INTO J2_TBL VALUES (0, NULL); +INSERT INTO J2_TBL VALUES (NULL, NULL); +INSERT INTO J2_TBL VALUES (NULL, 0); + +-- useful in some tests below +create temp table onerow(); +insert into onerow default values; +analyze onerow; + + +-- +-- CORRELATION NAMES +-- Make sure that table/column aliases are supported +-- before diving into more complex join syntax. +-- + +SELECT '' AS "xxx", * + FROM J1_TBL AS tx; + +SELECT '' AS "xxx", * + FROM J1_TBL tx; + +SELECT '' AS "xxx", * + FROM J1_TBL AS t1 (a, b, c); + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c); + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c), J2_TBL t2 (d, e); + +SELECT '' AS "xxx", t1.a, t2.e + FROM J1_TBL t1 (a, b, c), J2_TBL t2 (d, e) + WHERE t1.a = t2.d; + + +-- +-- CROSS JOIN +-- Qualifications are not allowed on cross joins, +-- which degenerate into a standard unqualified inner join. +-- + +SELECT '' AS "xxx", * + FROM J1_TBL CROSS JOIN J2_TBL; + +-- ambiguous column +SELECT '' AS "xxx", i, k, t + FROM J1_TBL CROSS JOIN J2_TBL; + +-- resolve previous ambiguity by specifying the table name +SELECT '' AS "xxx", t1.i, k, t + FROM J1_TBL t1 CROSS JOIN J2_TBL t2; + +SELECT '' AS "xxx", ii, tt, kk + FROM (J1_TBL CROSS JOIN J2_TBL) + AS tx (ii, jj, tt, ii2, kk); + +SELECT '' AS "xxx", tx.ii, tx.jj, tx.kk + FROM (J1_TBL t1 (a, b, c) CROSS JOIN J2_TBL t2 (d, e)) + AS tx (ii, jj, tt, ii2, kk); + +SELECT '' AS "xxx", * + FROM J1_TBL CROSS JOIN J2_TBL a CROSS JOIN J2_TBL b; + + +-- +-- +-- Inner joins (equi-joins) +-- +-- + +-- +-- Inner joins (equi-joins) with USING clause +-- The USING syntax changes the shape of the resulting table +-- by including a column in the USING clause only once in the result. +-- + +-- Inner equi-join on specified column +SELECT '' AS "xxx", * + FROM J1_TBL INNER JOIN J2_TBL USING (i); + +-- Same as above, slightly different syntax +SELECT '' AS "xxx", * + FROM J1_TBL JOIN J2_TBL USING (i); + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c) JOIN J2_TBL t2 (a, d) USING (a) + ORDER BY a, d; + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c) JOIN J2_TBL t2 (a, b) USING (b) + ORDER BY b, t1.a; + + +-- +-- NATURAL JOIN +-- Inner equi-join on all columns with the same name +-- + +SELECT '' AS "xxx", * + FROM J1_TBL NATURAL JOIN J2_TBL; + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c) NATURAL JOIN J2_TBL t2 (a, d); + +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b, c) NATURAL JOIN J2_TBL t2 (d, a); + +-- mismatch number of columns +-- currently, Postgres will fill in with underlying names +SELECT '' AS "xxx", * + FROM J1_TBL t1 (a, b) NATURAL JOIN J2_TBL t2 (a); + + +-- +-- Inner joins (equi-joins) +-- + +SELECT '' AS "xxx", * + FROM J1_TBL JOIN J2_TBL ON (J1_TBL.i = J2_TBL.i); + +SELECT '' AS "xxx", * + FROM J1_TBL JOIN J2_TBL ON (J1_TBL.i = J2_TBL.k); + + +-- +-- Non-equi-joins +-- + +SELECT '' AS "xxx", * + FROM J1_TBL JOIN J2_TBL ON (J1_TBL.i <= J2_TBL.k); + + +-- +-- Outer joins +-- Note that OUTER is a noise word +-- + +SELECT '' AS "xxx", * + FROM J1_TBL LEFT OUTER JOIN J2_TBL USING (i) + ORDER BY i, k, t; + +SELECT '' AS "xxx", * + FROM J1_TBL LEFT JOIN J2_TBL USING (i) + ORDER BY i, k, t; + +SELECT '' AS "xxx", * + FROM J1_TBL RIGHT OUTER JOIN J2_TBL USING (i); + +SELECT '' AS "xxx", * + FROM J1_TBL RIGHT JOIN J2_TBL USING (i); + +SELECT '' AS "xxx", * + FROM J1_TBL FULL OUTER JOIN J2_TBL USING (i) + ORDER BY i, k, t; + +SELECT '' AS "xxx", * + FROM J1_TBL FULL JOIN J2_TBL USING (i) + ORDER BY i, k, t; + +SELECT '' AS "xxx", * + FROM J1_TBL LEFT JOIN J2_TBL USING (i) WHERE (k = 1); + +SELECT '' AS "xxx", * + FROM J1_TBL LEFT JOIN J2_TBL USING (i) WHERE (i = 1); + +-- +-- semijoin selectivity for <> +-- +explain (costs off) +select * from int4_tbl i4, tenk1 a +where exists(select * from tenk1 b + where a.twothousand = b.twothousand and a.fivethous <> b.fivethous) + and i4.f1 = a.tenthous; + + +-- +-- More complicated constructs +-- + +-- +-- Multiway full join +-- + +CREATE TABLE t1 (name TEXT, n INTEGER); +CREATE TABLE t2 (name TEXT, n INTEGER); +CREATE TABLE t3 (name TEXT, n INTEGER); + +INSERT INTO t1 VALUES ( 'bb', 11 ); +INSERT INTO t2 VALUES ( 'bb', 12 ); +INSERT INTO t2 VALUES ( 'cc', 22 ); +INSERT INTO t2 VALUES ( 'ee', 42 ); +INSERT INTO t3 VALUES ( 'bb', 13 ); +INSERT INTO t3 VALUES ( 'cc', 23 ); +INSERT INTO t3 VALUES ( 'dd', 33 ); + +SELECT * FROM t1 FULL JOIN t2 USING (name) FULL JOIN t3 USING (name); + +-- +-- Test interactions of join syntax and subqueries +-- + +-- Basic cases (we expect planner to pull up the subquery here) +SELECT * FROM +(SELECT * FROM t2) as s2 +INNER JOIN +(SELECT * FROM t3) s3 +USING (name); + +SELECT * FROM +(SELECT * FROM t2) as s2 +LEFT JOIN +(SELECT * FROM t3) s3 +USING (name); + +SELECT * FROM +(SELECT * FROM t2) as s2 +FULL JOIN +(SELECT * FROM t3) s3 +USING (name); + +-- Cases with non-nullable expressions in subquery results; +-- make sure these go to null as expected +SELECT * FROM +(SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 +NATURAL INNER JOIN +(SELECT name, n as s3_n, 3 as s3_2 FROM t3) s3; + +SELECT * FROM +(SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 +NATURAL LEFT JOIN +(SELECT name, n as s3_n, 3 as s3_2 FROM t3) s3; + +SELECT * FROM +(SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 +NATURAL FULL JOIN +(SELECT name, n as s3_n, 3 as s3_2 FROM t3) s3; + +SELECT * FROM +(SELECT name, n as s1_n, 1 as s1_1 FROM t1) as s1 +NATURAL INNER JOIN +(SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 +NATURAL INNER JOIN +(SELECT name, n as s3_n, 3 as s3_2 FROM t3) s3; + +SELECT * FROM +(SELECT name, n as s1_n, 1 as s1_1 FROM t1) as s1 +NATURAL FULL JOIN +(SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 +NATURAL FULL JOIN +(SELECT name, n as s3_n, 3 as s3_2 FROM t3) s3; + +SELECT * FROM +(SELECT name, n as s1_n FROM t1) as s1 +NATURAL FULL JOIN + (SELECT * FROM + (SELECT name, n as s2_n FROM t2) as s2 + NATURAL FULL JOIN + (SELECT name, n as s3_n FROM t3) as s3 + ) ss2; + +SELECT * FROM +(SELECT name, n as s1_n FROM t1) as s1 +NATURAL FULL JOIN + (SELECT * FROM + (SELECT name, n as s2_n, 2 as s2_2 FROM t2) as s2 + NATURAL FULL JOIN + (SELECT name, n as s3_n FROM t3) as s3 + ) ss2; + +-- Constants as join keys can also be problematic +SELECT * FROM + (SELECT name, n as s1_n FROM t1) as s1 +FULL JOIN + (SELECT name, 2 as s2_n FROM t2) as s2 +ON (s1_n = s2_n); + + +-- Test for propagation of nullability constraints into sub-joins + +create temp table x (x1 int, x2 int); +insert into x values (1,11); +insert into x values (2,22); +insert into x values (3,null); +insert into x values (4,44); +insert into x values (5,null); + +create temp table y (y1 int, y2 int); +insert into y values (1,111); +insert into y values (2,222); +insert into y values (3,333); +insert into y values (4,null); + +select * from x; +select * from y; + +select * from x left join y on (x1 = y1 and x2 is not null); +select * from x left join y on (x1 = y1 and y2 is not null); + +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1); +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1 and x2 is not null); +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1 and y2 is not null); +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1 and xx2 is not null); +-- these should NOT give the same answers as above +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1) where (x2 is not null); +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1) where (y2 is not null); +select * from (x left join y on (x1 = y1)) left join x xx(xx1,xx2) +on (x1 = xx1) where (xx2 is not null); + +-- +-- regression test: check for bug with propagation of implied equality +-- to outside an IN +-- +select count(*) from tenk1 a where unique1 in + (select unique1 from tenk1 b join tenk1 c using (unique1) + where b.unique2 = 42); + +-- +-- regression test: check for failure to generate a plan with multiple +-- degenerate IN clauses +-- +select count(*) from tenk1 x where + x.unique1 in (select a.f1 from int4_tbl a,float8_tbl b where a.f1=b.f1) and + x.unique1 = 0 and + x.unique1 in (select aa.f1 from int4_tbl aa,float8_tbl bb where aa.f1=bb.f1); + +-- try that with GEQO too +begin; +set geqo = on; +set geqo_threshold = 2; +select count(*) from tenk1 x where + x.unique1 in (select a.f1 from int4_tbl a,float8_tbl b where a.f1=b.f1) and + x.unique1 = 0 and + x.unique1 in (select aa.f1 from int4_tbl aa,float8_tbl bb where aa.f1=bb.f1); +rollback; + +-- +-- regression test: be sure we cope with proven-dummy append rels +-- +explain (costs off) +select aa, bb, unique1, unique1 + from tenk1 right join b on aa = unique1 + where bb < bb and bb is null; + +select aa, bb, unique1, unique1 + from tenk1 right join b on aa = unique1 + where bb < bb and bb is null; + +-- +-- regression test: check handling of empty-FROM subquery underneath outer join +-- +explain (costs off) +select * from int8_tbl i1 left join (int8_tbl i2 join + (select 123 as x) ss on i2.q1 = x) on i1.q2 = i2.q2 +order by 1, 2; + +select * from int8_tbl i1 left join (int8_tbl i2 join + (select 123 as x) ss on i2.q1 = x) on i1.q2 = i2.q2 +order by 1, 2; + +-- +-- regression test: check a case where join_clause_is_movable_into() gives +-- an imprecise result, causing an assertion failure +-- +select count(*) +from + (select t3.tenthous as x1, coalesce(t1.stringu1, t2.stringu1) as x2 + from tenk1 t1 + left join tenk1 t2 on t1.unique1 = t2.unique1 + join tenk1 t3 on t1.unique2 = t3.unique2) ss, + tenk1 t4, + tenk1 t5 +where t4.thousand = t5.unique1 and ss.x1 = t4.tenthous and ss.x2 = t5.stringu1; + +-- +-- regression test: check a case where we formerly missed including an EC +-- enforcement clause because it was expected to be handled at scan level +-- +explain (costs off) +select a.f1, b.f1, t.thousand, t.tenthous from + tenk1 t, + (select sum(f1)+1 as f1 from int4_tbl i4a) a, + (select sum(f1) as f1 from int4_tbl i4b) b +where b.f1 = t.thousand and a.f1 = b.f1 and (a.f1+b.f1+999) = t.tenthous; + +select a.f1, b.f1, t.thousand, t.tenthous from + tenk1 t, + (select sum(f1)+1 as f1 from int4_tbl i4a) a, + (select sum(f1) as f1 from int4_tbl i4b) b +where b.f1 = t.thousand and a.f1 = b.f1 and (a.f1+b.f1+999) = t.tenthous; + +-- +-- check a case where we formerly got confused by conflicting sort orders +-- in redundant merge join path keys +-- +explain (costs off) +select * from + j1_tbl full join + (select * from j2_tbl order by j2_tbl.i desc, j2_tbl.k asc) j2_tbl + on j1_tbl.i = j2_tbl.i and j1_tbl.i = j2_tbl.k; + +select * from + j1_tbl full join + (select * from j2_tbl order by j2_tbl.i desc, j2_tbl.k asc) j2_tbl + on j1_tbl.i = j2_tbl.i and j1_tbl.i = j2_tbl.k; + +-- +-- a different check for handling of redundant sort keys in merge joins +-- +explain (costs off) +select count(*) from + (select * from tenk1 x order by x.thousand, x.twothousand, x.fivethous) x + left join + (select * from tenk1 y order by y.unique2) y + on x.thousand = y.unique2 and x.twothousand = y.hundred and x.fivethous = y.unique2; + +select count(*) from + (select * from tenk1 x order by x.thousand, x.twothousand, x.fivethous) x + left join + (select * from tenk1 y order by y.unique2) y + on x.thousand = y.unique2 and x.twothousand = y.hundred and x.fivethous = y.unique2; + + +-- +-- Clean up +-- + +DROP TABLE t1; +DROP TABLE t2; +DROP TABLE t3; + +DROP TABLE J1_TBL; +DROP TABLE J2_TBL; + +-- Both DELETE and UPDATE allow the specification of additional tables +-- to "join" against to determine which rows should be modified. + +CREATE TEMP TABLE t1 (a int, b int); +CREATE TEMP TABLE t2 (a int, b int); +CREATE TEMP TABLE t3 (x int, y int); + +INSERT INTO t1 VALUES (5, 10); +INSERT INTO t1 VALUES (15, 20); +INSERT INTO t1 VALUES (100, 100); +INSERT INTO t1 VALUES (200, 1000); +INSERT INTO t2 VALUES (200, 2000); +INSERT INTO t3 VALUES (5, 20); +INSERT INTO t3 VALUES (6, 7); +INSERT INTO t3 VALUES (7, 8); +INSERT INTO t3 VALUES (500, 100); + +DELETE FROM t3 USING t1 table1 WHERE t3.x = table1.a; +SELECT * FROM t3; +DELETE FROM t3 USING t1 JOIN t2 USING (a) WHERE t3.x > t1.a; +SELECT * FROM t3; +DELETE FROM t3 USING t3 t3_other WHERE t3.x = t3_other.x AND t3.y = t3_other.y; +SELECT * FROM t3; + +-- Test join against inheritance tree + +create temp table t2a () inherits (t2); + +insert into t2a values (200, 2001); + +select * from t1 left join t2 on (t1.a = t2.a); + +-- Test matching of column name with wrong alias + +select t1.x from t1 join t3 on (t1.a = t3.x); + +-- +-- regression test for 8.1 merge right join bug +-- + +CREATE TEMP TABLE tt1 ( tt1_id int4, joincol int4 ); +INSERT INTO tt1 VALUES (1, 11); +INSERT INTO tt1 VALUES (2, NULL); + +CREATE TEMP TABLE tt2 ( tt2_id int4, joincol int4 ); +INSERT INTO tt2 VALUES (21, 11); +INSERT INTO tt2 VALUES (22, 11); + +set enable_hashjoin to off; +set enable_nestloop to off; + +-- these should give the same results + +select tt1.*, tt2.* from tt1 left join tt2 on tt1.joincol = tt2.joincol; + +select tt1.*, tt2.* from tt2 right join tt1 on tt1.joincol = tt2.joincol; + +reset enable_hashjoin; +reset enable_nestloop; + +-- +-- regression test for bug #13908 (hash join with skew tuples & nbatch increase) +-- + +set work_mem to '64kB'; +set enable_mergejoin to off; + +explain (costs off) +select count(*) from tenk1 a, tenk1 b + where a.hundred = b.thousand and (b.fivethous % 10) < 10; +select count(*) from tenk1 a, tenk1 b + where a.hundred = b.thousand and (b.fivethous % 10) < 10; + +reset work_mem; +reset enable_mergejoin; + +-- +-- regression test for 8.2 bug with improper re-ordering of left joins +-- + +create temp table tt3(f1 int, f2 text); +insert into tt3 select x, repeat('xyzzy', 100) from generate_series(1,10000) x; +create index tt3i on tt3(f1); +analyze tt3; + +create temp table tt4(f1 int); +insert into tt4 values (0),(1),(9999); +analyze tt4; + +SELECT a.f1 +FROM tt4 a +LEFT JOIN ( + SELECT b.f1 + FROM tt3 b LEFT JOIN tt3 c ON (b.f1 = c.f1) + WHERE c.f1 IS NULL +) AS d ON (a.f1 = d.f1) +WHERE d.f1 IS NULL; + +-- +-- regression test for proper handling of outer joins within antijoins +-- + +create temp table tt4x(c1 int, c2 int, c3 int); + +explain (costs off) +select * from tt4x t1 +where not exists ( + select 1 from tt4x t2 + left join tt4x t3 on t2.c3 = t3.c1 + left join ( select t5.c1 as c1 + from tt4x t4 left join tt4x t5 on t4.c2 = t5.c1 + ) a1 on t3.c2 = a1.c1 + where t1.c1 = t2.c2 +); + +-- +-- regression test for problems of the sort depicted in bug #3494 +-- + +create temp table tt5(f1 int, f2 int); +create temp table tt6(f1 int, f2 int); + +insert into tt5 values(1, 10); +insert into tt5 values(1, 11); + +insert into tt6 values(1, 9); +insert into tt6 values(1, 2); +insert into tt6 values(2, 9); + +select * from tt5,tt6 where tt5.f1 = tt6.f1 and tt5.f1 = tt5.f2 - tt6.f2; + +-- +-- regression test for problems of the sort depicted in bug #3588 +-- + +create temp table xx (pkxx int); +create temp table yy (pkyy int, pkxx int); + +insert into xx values (1); +insert into xx values (2); +insert into xx values (3); + +insert into yy values (101, 1); +insert into yy values (201, 2); +insert into yy values (301, NULL); + +select yy.pkyy as yy_pkyy, yy.pkxx as yy_pkxx, yya.pkyy as yya_pkyy, + xxa.pkxx as xxa_pkxx, xxb.pkxx as xxb_pkxx +from yy + left join (SELECT * FROM yy where pkyy = 101) as yya ON yy.pkyy = yya.pkyy + left join xx xxa on yya.pkxx = xxa.pkxx + left join xx xxb on coalesce (xxa.pkxx, 1) = xxb.pkxx; + +-- +-- regression test for improper pushing of constants across outer-join clauses +-- (as seen in early 8.2.x releases) +-- + +create temp table zt1 (f1 int primary key); +create temp table zt2 (f2 int primary key); +create temp table zt3 (f3 int primary key); +insert into zt1 values(53); +insert into zt2 values(53); + +select * from + zt2 left join zt3 on (f2 = f3) + left join zt1 on (f3 = f1) +where f2 = 53; + +create temp view zv1 as select *,'dummy'::text AS junk from zt1; + +select * from + zt2 left join zt3 on (f2 = f3) + left join zv1 on (f3 = f1) +where f2 = 53; + +-- +-- regression test for improper extraction of OR indexqual conditions +-- (as seen in early 8.3.x releases) +-- + +select a.unique2, a.ten, b.tenthous, b.unique2, b.hundred +from tenk1 a left join tenk1 b on a.unique2 = b.tenthous +where a.unique1 = 42 and + ((b.unique2 is null and a.ten = 2) or b.hundred = 3); + +-- +-- test proper positioning of one-time quals in EXISTS (8.4devel bug) +-- +prepare foo(bool) as + select count(*) from tenk1 a left join tenk1 b + on (a.unique2 = b.unique1 and exists + (select 1 from tenk1 c where c.thousand = b.unique2 and $1)); +execute foo(true); +execute foo(false); + +-- +-- test for sane behavior with noncanonical merge clauses, per bug #4926 +-- + +begin; + +set enable_mergejoin = 1; +set enable_hashjoin = 0; +set enable_nestloop = 0; + +create temp table a (i integer); +create temp table b (x integer, y integer); + +select * from a left join b on i = x and i = y and x = i; + +rollback; + +-- +-- test handling of merge clauses using record_ops +-- +begin; + +create type mycomptype as (id int, v bigint); + +create temp table tidv (idv mycomptype); +create index on tidv (idv); + +explain (costs off) +select a.idv, b.idv from tidv a, tidv b where a.idv = b.idv; + +set enable_mergejoin = 0; + +explain (costs off) +select a.idv, b.idv from tidv a, tidv b where a.idv = b.idv; + +rollback; + +-- +-- test NULL behavior of whole-row Vars, per bug #5025 +-- +select t1.q2, count(t2.*) +from int8_tbl t1 left join int8_tbl t2 on (t1.q2 = t2.q1) +group by t1.q2 order by 1; + +select t1.q2, count(t2.*) +from int8_tbl t1 left join (select * from int8_tbl) t2 on (t1.q2 = t2.q1) +group by t1.q2 order by 1; + +select t1.q2, count(t2.*) +from int8_tbl t1 left join (select * from int8_tbl offset 0) t2 on (t1.q2 = t2.q1) +group by t1.q2 order by 1; + +select t1.q2, count(t2.*) +from int8_tbl t1 left join + (select q1, case when q2=1 then 1 else q2 end as q2 from int8_tbl) t2 + on (t1.q2 = t2.q1) +group by t1.q2 order by 1; + +-- +-- test incorrect failure to NULL pulled-up subexpressions +-- +begin; + +create temp table a ( + code char not null, + constraint a_pk primary key (code) +); +create temp table b ( + a char not null, + num integer not null, + constraint b_pk primary key (a, num) +); +create temp table c ( + name char not null, + a char, + constraint c_pk primary key (name) +); + +insert into a (code) values ('p'); +insert into a (code) values ('q'); +insert into b (a, num) values ('p', 1); +insert into b (a, num) values ('p', 2); +insert into c (name, a) values ('A', 'p'); +insert into c (name, a) values ('B', 'q'); +insert into c (name, a) values ('C', null); + +select c.name, ss.code, ss.b_cnt, ss.const +from c left join + (select a.code, coalesce(b_grp.cnt, 0) as b_cnt, -1 as const + from a left join + (select count(1) as cnt, b.a from b group by b.a) as b_grp + on a.code = b_grp.a + ) as ss + on (c.a = ss.code) +order by c.name; + +rollback; + +-- +-- test incorrect handling of placeholders that only appear in targetlists, +-- per bug #6154 +-- +SELECT * FROM +( SELECT 1 as key1 ) sub1 +LEFT JOIN +( SELECT sub3.key3, sub4.value2, COALESCE(sub4.value2, 66) as value3 FROM + ( SELECT 1 as key3 ) sub3 + LEFT JOIN + ( SELECT sub5.key5, COALESCE(sub6.value1, 1) as value2 FROM + ( SELECT 1 as key5 ) sub5 + LEFT JOIN + ( SELECT 2 as key6, 42 as value1 ) sub6 + ON sub5.key5 = sub6.key6 + ) sub4 + ON sub4.key5 = sub3.key3 +) sub2 +ON sub1.key1 = sub2.key3; + +-- test the path using join aliases, too +SELECT * FROM +( SELECT 1 as key1 ) sub1 +LEFT JOIN +( SELECT sub3.key3, value2, COALESCE(value2, 66) as value3 FROM + ( SELECT 1 as key3 ) sub3 + LEFT JOIN + ( SELECT sub5.key5, COALESCE(sub6.value1, 1) as value2 FROM + ( SELECT 1 as key5 ) sub5 + LEFT JOIN + ( SELECT 2 as key6, 42 as value1 ) sub6 + ON sub5.key5 = sub6.key6 + ) sub4 + ON sub4.key5 = sub3.key3 +) sub2 +ON sub1.key1 = sub2.key3; + +-- +-- test case where a PlaceHolderVar is used as a nestloop parameter +-- + +EXPLAIN (COSTS OFF) +SELECT qq, unique1 + FROM + ( SELECT COALESCE(q1, 0) AS qq FROM int8_tbl a ) AS ss1 + FULL OUTER JOIN + ( SELECT COALESCE(q2, -1) AS qq FROM int8_tbl b ) AS ss2 + USING (qq) + INNER JOIN tenk1 c ON qq = unique2; + +SELECT qq, unique1 + FROM + ( SELECT COALESCE(q1, 0) AS qq FROM int8_tbl a ) AS ss1 + FULL OUTER JOIN + ( SELECT COALESCE(q2, -1) AS qq FROM int8_tbl b ) AS ss2 + USING (qq) + INNER JOIN tenk1 c ON qq = unique2; + +-- +-- nested nestloops can require nested PlaceHolderVars +-- + +create temp table nt1 ( + id int primary key, + a1 boolean, + a2 boolean +); +create temp table nt2 ( + id int primary key, + nt1_id int, + b1 boolean, + b2 boolean, + foreign key (nt1_id) references nt1(id) +); +create temp table nt3 ( + id int primary key, + nt2_id int, + c1 boolean, + foreign key (nt2_id) references nt2(id) +); + +insert into nt1 values (1,true,true); +insert into nt1 values (2,true,false); +insert into nt1 values (3,false,false); +insert into nt2 values (1,1,true,true); +insert into nt2 values (2,2,true,false); +insert into nt2 values (3,3,false,false); +insert into nt3 values (1,1,true); +insert into nt3 values (2,2,false); +insert into nt3 values (3,3,true); + +explain (costs off) +select nt3.id +from nt3 as nt3 + left join + (select nt2.*, (nt2.b1 and ss1.a3) AS b3 + from nt2 as nt2 + left join + (select nt1.*, (nt1.id is not null) as a3 from nt1) as ss1 + on ss1.id = nt2.nt1_id + ) as ss2 + on ss2.id = nt3.nt2_id +where nt3.id = 1 and ss2.b3; + +select nt3.id +from nt3 as nt3 + left join + (select nt2.*, (nt2.b1 and ss1.a3) AS b3 + from nt2 as nt2 + left join + (select nt1.*, (nt1.id is not null) as a3 from nt1) as ss1 + on ss1.id = nt2.nt1_id + ) as ss2 + on ss2.id = nt3.nt2_id +where nt3.id = 1 and ss2.b3; + +-- +-- test case where a PlaceHolderVar is propagated into a subquery +-- + +explain (costs off) +select * from + int8_tbl t1 left join + (select q1 as x, 42 as y from int8_tbl t2) ss + on t1.q2 = ss.x +where + 1 = (select 1 from int8_tbl t3 where ss.y is not null limit 1) +order by 1,2; + +select * from + int8_tbl t1 left join + (select q1 as x, 42 as y from int8_tbl t2) ss + on t1.q2 = ss.x +where + 1 = (select 1 from int8_tbl t3 where ss.y is not null limit 1) +order by 1,2; + +-- +-- test the corner cases FULL JOIN ON TRUE and FULL JOIN ON FALSE +-- +select * from int4_tbl a full join int4_tbl b on true; +select * from int4_tbl a full join int4_tbl b on false; + +-- +-- test for ability to use a cartesian join when necessary +-- + +create temp table q1 as select 1 as q1; +create temp table q2 as select 0 as q2; +analyze q1; +analyze q2; + +explain (costs off) +select * from + tenk1 join int4_tbl on f1 = twothousand, + q1, q2 +where q1 = thousand or q2 = thousand; + +explain (costs off) +select * from + tenk1 join int4_tbl on f1 = twothousand, + q1, q2 +where thousand = (q1 + q2); + +-- +-- test ability to generate a suitable plan for a star-schema query +-- + +explain (costs off) +select * from + tenk1, int8_tbl a, int8_tbl b +where thousand = a.q1 and tenthous = b.q1 and a.q2 = 1 and b.q2 = 2; + +-- +-- test a corner case in which we shouldn't apply the star-schema optimization +-- + +explain (costs off) +select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from + tenk1 t1 + inner join int4_tbl i1 + left join (select v1.x2, v2.y1, 11 AS d1 + from (select 1,0 from onerow) v1(x1,x2) + left join (select 3,1 from onerow) v2(y1,y2) + on v1.x1 = v2.y2) subq1 + on (i1.f1 = subq1.x2) + on (t1.unique2 = subq1.d1) + left join tenk1 t2 + on (subq1.y1 = t2.unique1) +where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; + +select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from + tenk1 t1 + inner join int4_tbl i1 + left join (select v1.x2, v2.y1, 11 AS d1 + from (select 1,0 from onerow) v1(x1,x2) + left join (select 3,1 from onerow) v2(y1,y2) + on v1.x1 = v2.y2) subq1 + on (i1.f1 = subq1.x2) + on (t1.unique2 = subq1.d1) + left join tenk1 t2 + on (subq1.y1 = t2.unique1) +where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; + +-- variant that isn't quite a star-schema case + +select ss1.d1 from + tenk1 as t1 + inner join tenk1 as t2 + on t1.tenthous = t2.ten + inner join + int8_tbl as i8 + left join int4_tbl as i4 + inner join (select 64::information_schema.cardinal_number as d1 + from tenk1 t3, + lateral (select abs(t3.unique1) + random()) ss0(x) + where t3.fivethous < 0) as ss1 + on i4.f1 = ss1.d1 + on i8.q1 = i4.f1 + on t1.tenthous = ss1.d1 +where t1.unique1 < i4.f1; + +-- this variant is foldable by the remove-useless-RESULT-RTEs code + +explain (costs off) +select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from + tenk1 t1 + inner join int4_tbl i1 + left join (select v1.x2, v2.y1, 11 AS d1 + from (values(1,0)) v1(x1,x2) + left join (values(3,1)) v2(y1,y2) + on v1.x1 = v2.y2) subq1 + on (i1.f1 = subq1.x2) + on (t1.unique2 = subq1.d1) + left join tenk1 t2 + on (subq1.y1 = t2.unique1) +where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; + +select t1.unique2, t1.stringu1, t2.unique1, t2.stringu2 from + tenk1 t1 + inner join int4_tbl i1 + left join (select v1.x2, v2.y1, 11 AS d1 + from (values(1,0)) v1(x1,x2) + left join (values(3,1)) v2(y1,y2) + on v1.x1 = v2.y2) subq1 + on (i1.f1 = subq1.x2) + on (t1.unique2 = subq1.d1) + left join tenk1 t2 + on (subq1.y1 = t2.unique1) +where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; + +-- Here's a variant that we can't fold too aggressively, though, +-- or we end up with noplace to evaluate the lateral PHV +explain (verbose, costs off) +select * from + (select 1 as x) ss1 left join (select 2 as y) ss2 on (true), + lateral (select ss2.y as z limit 1) ss3; +select * from + (select 1 as x) ss1 left join (select 2 as y) ss2 on (true), + lateral (select ss2.y as z limit 1) ss3; + +-- +-- test inlining of immutable functions +-- +create function f_immutable_int4(i integer) returns integer as +$$ begin return i; end; $$ language plpgsql immutable; + +-- check optimization of function scan with join +explain (costs off) +select unique1 from tenk1, (select * from f_immutable_int4(1) x) x +where x = unique1; + +explain (verbose, costs off) +select unique1, x.* +from tenk1, (select *, random() from f_immutable_int4(1) x) x +where x = unique1; + +explain (costs off) +select unique1 from tenk1, f_immutable_int4(1) x where x = unique1; + +explain (costs off) +select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1; + +explain (costs off) +select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x; + +explain (costs off) +select unique1, x from tenk1 left join f_immutable_int4(1) x on unique1 = x; + +explain (costs off) +select unique1, x from tenk1 right join f_immutable_int4(1) x on unique1 = x; + +explain (costs off) +select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x; + +-- check that pullup of a const function allows further const-folding +explain (costs off) +select unique1 from tenk1, f_immutable_int4(1) x where x = 42; + +-- test inlining of immutable functions with PlaceHolderVars +explain (costs off) +select nt3.id +from nt3 as nt3 + left join + (select nt2.*, (nt2.b1 or i4 = 42) AS b3 + from nt2 as nt2 + left join + f_immutable_int4(0) i4 + on i4 = nt2.nt1_id + ) as ss2 + on ss2.id = nt3.nt2_id +where nt3.id = 1 and ss2.b3; + +drop function f_immutable_int4(int); + +-- test inlining when function returns composite + +create function mki8(bigint, bigint) returns int8_tbl as +$$select row($1,$2)::int8_tbl$$ language sql; + +create function mki4(int) returns int4_tbl as +$$select row($1)::int4_tbl$$ language sql; + +explain (verbose, costs off) +select * from mki8(1,2); +select * from mki8(1,2); + +explain (verbose, costs off) +select * from mki4(42); +select * from mki4(42); + +drop function mki8(bigint, bigint); +drop function mki4(int); + +-- +-- test extraction of restriction OR clauses from join OR clause +-- (we used to only do this for indexable clauses) +-- + +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 = 1 and b.unique1 = 2) or (a.unique2 = 3 and b.hundred = 4); +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 = 1 and b.unique1 = 2) or (a.unique2 = 3 and b.ten = 4); +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 = 1 and b.unique1 = 2) or + ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); + +-- +-- test placement of movable quals in a parameterized join tree +-- + +explain (costs off) +select * from tenk1 t1 left join + (tenk1 t2 join tenk1 t3 on t2.thousand = t3.unique2) + on t1.hundred = t2.hundred and t1.ten = t3.ten +where t1.unique1 = 1; + +explain (costs off) +select * from tenk1 t1 left join + (tenk1 t2 join tenk1 t3 on t2.thousand = t3.unique2) + on t1.hundred = t2.hundred and t1.ten + t2.ten = t3.ten +where t1.unique1 = 1; + +explain (costs off) +select count(*) from + tenk1 a join tenk1 b on a.unique1 = b.unique2 + left join tenk1 c on a.unique2 = b.unique1 and c.thousand = a.thousand + join int4_tbl on b.thousand = f1; + +select count(*) from + tenk1 a join tenk1 b on a.unique1 = b.unique2 + left join tenk1 c on a.unique2 = b.unique1 and c.thousand = a.thousand + join int4_tbl on b.thousand = f1; + +explain (costs off) +select b.unique1 from + tenk1 a join tenk1 b on a.unique1 = b.unique2 + left join tenk1 c on b.unique1 = 42 and c.thousand = a.thousand + join int4_tbl i1 on b.thousand = f1 + right join int4_tbl i2 on i2.f1 = b.tenthous + order by 1; + +select b.unique1 from + tenk1 a join tenk1 b on a.unique1 = b.unique2 + left join tenk1 c on b.unique1 = 42 and c.thousand = a.thousand + join int4_tbl i1 on b.thousand = f1 + right join int4_tbl i2 on i2.f1 = b.tenthous + order by 1; + +explain (costs off) +select * from +( + select unique1, q1, coalesce(unique1, -1) + q1 as fault + from int8_tbl left join tenk1 on (q2 = unique2) +) ss +where fault = 122 +order by fault; + +select * from +( + select unique1, q1, coalesce(unique1, -1) + q1 as fault + from int8_tbl left join tenk1 on (q2 = unique2) +) ss +where fault = 122 +order by fault; + +explain (costs off) +select * from +(values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) +left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join unnest(v1ys) as u1(u1y) on u1y = v2y; + +select * from +(values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) +left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join unnest(v1ys) as u1(u1y) on u1y = v2y; + +-- +-- test handling of potential equivalence clauses above outer joins +-- + +explain (costs off) +select q1, unique2, thousand, hundred + from int8_tbl a left join tenk1 b on q1 = unique2 + where coalesce(thousand,123) = q1 and q1 = coalesce(hundred,123); + +select q1, unique2, thousand, hundred + from int8_tbl a left join tenk1 b on q1 = unique2 + where coalesce(thousand,123) = q1 and q1 = coalesce(hundred,123); + +explain (costs off) +select f1, unique2, case when unique2 is null then f1 else 0 end + from int4_tbl a left join tenk1 b on f1 = unique2 + where (case when unique2 is null then f1 else 0 end) = 0; + +select f1, unique2, case when unique2 is null then f1 else 0 end + from int4_tbl a left join tenk1 b on f1 = unique2 + where (case when unique2 is null then f1 else 0 end) = 0; + +-- +-- another case with equivalence clauses above outer joins (bug #8591) +-- + +explain (costs off) +select a.unique1, b.unique1, c.unique1, coalesce(b.twothousand, a.twothousand) + from tenk1 a left join tenk1 b on b.thousand = a.unique1 left join tenk1 c on c.unique2 = coalesce(b.twothousand, a.twothousand) + where a.unique2 < 10 and coalesce(b.twothousand, a.twothousand) = 44; + +select a.unique1, b.unique1, c.unique1, coalesce(b.twothousand, a.twothousand) + from tenk1 a left join tenk1 b on b.thousand = a.unique1 left join tenk1 c on c.unique2 = coalesce(b.twothousand, a.twothousand) + where a.unique2 < 10 and coalesce(b.twothousand, a.twothousand) = 44; + +-- +-- check handling of join aliases when flattening multiple levels of subquery +-- + +explain (verbose, costs off) +select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from + (values (0),(1)) foo1(join_key) +left join + (select join_key, bug_field from + (select ss1.join_key, ss1.bug_field from + (select f1 as join_key, 666 as bug_field from int4_tbl i1) ss1 + ) foo2 + left join + (select unique2 as join_key from tenk1 i2) ss2 + using (join_key) + ) foo3 +using (join_key); + +select foo1.join_key as foo1_id, foo3.join_key AS foo3_id, bug_field from + (values (0),(1)) foo1(join_key) +left join + (select join_key, bug_field from + (select ss1.join_key, ss1.bug_field from + (select f1 as join_key, 666 as bug_field from int4_tbl i1) ss1 + ) foo2 + left join + (select unique2 as join_key from tenk1 i2) ss2 + using (join_key) + ) foo3 +using (join_key); + +-- +-- test successful handling of nested outer joins with degenerate join quals +-- + +explain (verbose, costs off) +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +explain (verbose, costs off) +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2, int4_tbl i4b2) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2, int4_tbl i4b2) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +explain (verbose, costs off) +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2, int4_tbl i4b2 + where q1 = f1) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +select t1.* from + text_tbl t1 + left join (select *, '***'::text as d1 from int8_tbl i8b1) b1 + left join int8_tbl i8 + left join (select *, null::int as d2 from int8_tbl i8b2, int4_tbl i4b2 + where q1 = f1) b2 + on (i8.q1 = b2.q1) + on (b2.d2 = b1.q2) + on (t1.f1 = b1.d1) + left join int4_tbl i4 + on (i8.q2 = i4.f1); + +explain (verbose, costs off) +select * from + text_tbl t1 + inner join int8_tbl i8 + on i8.q2 = 456 + right join text_tbl t2 + on t1.f1 = 'doh!' + left join int4_tbl i4 + on i8.q1 = i4.f1; + +select * from + text_tbl t1 + inner join int8_tbl i8 + on i8.q2 = 456 + right join text_tbl t2 + on t1.f1 = 'doh!' + left join int4_tbl i4 + on i8.q1 = i4.f1; + +-- +-- test for appropriate join order in the presence of lateral references +-- + +explain (verbose, costs off) +select * from + text_tbl t1 + left join int8_tbl i8 + on i8.q2 = 123, + lateral (select i8.q1, t2.f1 from text_tbl t2 limit 1) as ss +where t1.f1 = ss.f1; + +select * from + text_tbl t1 + left join int8_tbl i8 + on i8.q2 = 123, + lateral (select i8.q1, t2.f1 from text_tbl t2 limit 1) as ss +where t1.f1 = ss.f1; + +explain (verbose, costs off) +select * from + text_tbl t1 + left join int8_tbl i8 + on i8.q2 = 123, + lateral (select i8.q1, t2.f1 from text_tbl t2 limit 1) as ss1, + lateral (select ss1.* from text_tbl t3 limit 1) as ss2 +where t1.f1 = ss2.f1; + +select * from + text_tbl t1 + left join int8_tbl i8 + on i8.q2 = 123, + lateral (select i8.q1, t2.f1 from text_tbl t2 limit 1) as ss1, + lateral (select ss1.* from text_tbl t3 limit 1) as ss2 +where t1.f1 = ss2.f1; + +explain (verbose, costs off) +select 1 from + text_tbl as tt1 + inner join text_tbl as tt2 on (tt1.f1 = 'foo') + left join text_tbl as tt3 on (tt3.f1 = 'foo') + left join text_tbl as tt4 on (tt3.f1 = tt4.f1), + lateral (select tt4.f1 as c0 from text_tbl as tt5 limit 1) as ss1 +where tt1.f1 = ss1.c0; + +select 1 from + text_tbl as tt1 + inner join text_tbl as tt2 on (tt1.f1 = 'foo') + left join text_tbl as tt3 on (tt3.f1 = 'foo') + left join text_tbl as tt4 on (tt3.f1 = tt4.f1), + lateral (select tt4.f1 as c0 from text_tbl as tt5 limit 1) as ss1 +where tt1.f1 = ss1.c0; + +-- +-- check a case in which a PlaceHolderVar forces join order +-- + +explain (verbose, costs off) +select ss2.* from + int4_tbl i41 + left join int8_tbl i8 + join (select i42.f1 as c1, i43.f1 as c2, 42 as c3 + from int4_tbl i42, int4_tbl i43) ss1 + on i8.q1 = ss1.c2 + on i41.f1 = ss1.c1, + lateral (select i41.*, i8.*, ss1.* from text_tbl limit 1) ss2 +where ss1.c2 = 0; + +select ss2.* from + int4_tbl i41 + left join int8_tbl i8 + join (select i42.f1 as c1, i43.f1 as c2, 42 as c3 + from int4_tbl i42, int4_tbl i43) ss1 + on i8.q1 = ss1.c2 + on i41.f1 = ss1.c1, + lateral (select i41.*, i8.*, ss1.* from text_tbl limit 1) ss2 +where ss1.c2 = 0; + +-- +-- test successful handling of full join underneath left join (bug #14105) +-- + +explain (costs off) +select * from + (select 1 as id) as xx + left join + (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) + on (xx.id = coalesce(yy.id)); + +select * from + (select 1 as id) as xx + left join + (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) + on (xx.id = coalesce(yy.id)); + +-- +-- test ability to push constants through outer join clauses +-- + +explain (costs off) + select * from int4_tbl a left join tenk1 b on f1 = unique2 where f1 = 0; + +explain (costs off) + select * from tenk1 a full join tenk1 b using(unique2) where unique2 = 42; + +-- +-- test that quals attached to an outer join have correct semantics, +-- specifically that they don't re-use expressions computed below the join; +-- we force a mergejoin so that coalesce(b.q1, 1) appears as a join input +-- + +set enable_hashjoin to off; +set enable_nestloop to off; + +explain (verbose, costs off) + select a.q2, b.q1 + from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1) + where coalesce(b.q1, 1) > 0; +select a.q2, b.q1 + from int8_tbl a left join int8_tbl b on a.q2 = coalesce(b.q1, 1) + where coalesce(b.q1, 1) > 0; + +reset enable_hashjoin; +reset enable_nestloop; + +-- +-- test join removal +-- + +begin; + +CREATE TEMP TABLE a (id int PRIMARY KEY, b_id int); +CREATE TEMP TABLE b (id int PRIMARY KEY, c_id int); +CREATE TEMP TABLE c (id int PRIMARY KEY); +CREATE TEMP TABLE d (a int, b int); +INSERT INTO a VALUES (0, 0), (1, NULL); +INSERT INTO b VALUES (0, 0), (1, NULL); +INSERT INTO c VALUES (0), (1); +INSERT INTO d VALUES (1,3), (2,2), (3,1); + +-- all three cases should be optimizable into a simple seqscan +explain (costs off) SELECT a.* FROM a LEFT JOIN b ON a.b_id = b.id; +explain (costs off) SELECT b.* FROM b LEFT JOIN c ON b.c_id = c.id; +explain (costs off) + SELECT a.* FROM a LEFT JOIN (b left join c on b.c_id = c.id) + ON (a.b_id = b.id); + +-- check optimization of outer join within another special join +explain (costs off) +select id from a where id in ( + select b.id from b left join c on b.id = c.id +); + +-- check that join removal works for a left join when joining a subquery +-- that is guaranteed to be unique by its GROUP BY clause +explain (costs off) +select d.* from d left join (select * from b group by b.id, b.c_id) s + on d.a = s.id and d.b = s.c_id; + +-- similarly, but keying off a DISTINCT clause +explain (costs off) +select d.* from d left join (select distinct * from b) s + on d.a = s.id and d.b = s.c_id; + +-- join removal is not possible when the GROUP BY contains a column that is +-- not in the join condition. (Note: as of 9.6, we notice that b.id is a +-- primary key and so drop b.c_id from the GROUP BY of the resulting plan; +-- but this happens too late for join removal in the outer plan level.) +explain (costs off) +select d.* from d left join (select * from b group by b.id, b.c_id) s + on d.a = s.id; + +-- similarly, but keying off a DISTINCT clause +explain (costs off) +select d.* from d left join (select distinct * from b) s + on d.a = s.id; + +-- check join removal works when uniqueness of the join condition is enforced +-- by a UNION +explain (costs off) +select d.* from d left join (select id from a union select id from b) s + on d.a = s.id; + +-- check join removal with a cross-type comparison operator +explain (costs off) +select i8.* from int8_tbl i8 left join (select f1 from int4_tbl group by f1) i4 + on i8.q1 = i4.f1; + +-- check join removal with lateral references +explain (costs off) +select 1 from (select a.id FROM a left join b on a.b_id = b.id) q, + lateral generate_series(1, q.id) gs(i) where q.id = gs.i; + +rollback; + +create temp table parent (k int primary key, pd int); +create temp table child (k int unique, cd int); +insert into parent values (1, 10), (2, 20), (3, 30); +insert into child values (1, 100), (4, 400); + +-- this case is optimizable +select p.* from parent p left join child c on (p.k = c.k); +explain (costs off) + select p.* from parent p left join child c on (p.k = c.k); + +-- this case is not +select p.*, linked from parent p + left join (select c.*, true as linked from child c) as ss + on (p.k = ss.k); +explain (costs off) + select p.*, linked from parent p + left join (select c.*, true as linked from child c) as ss + on (p.k = ss.k); + +-- check for a 9.0rc1 bug: join removal breaks pseudoconstant qual handling +select p.* from + parent p left join child c on (p.k = c.k) + where p.k = 1 and p.k = 2; +explain (costs off) +select p.* from + parent p left join child c on (p.k = c.k) + where p.k = 1 and p.k = 2; + +select p.* from + (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k + where p.k = 1 and p.k = 2; +explain (costs off) +select p.* from + (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k + where p.k = 1 and p.k = 2; + +-- bug 5255: this is not optimizable by join removal +begin; + +CREATE TEMP TABLE a (id int PRIMARY KEY); +CREATE TEMP TABLE b (id int PRIMARY KEY, a_id int); +INSERT INTO a VALUES (0), (1); +INSERT INTO b VALUES (0, 0), (1, NULL); + +SELECT * FROM b LEFT JOIN a ON (b.a_id = a.id) WHERE (a.id IS NULL OR a.id > 0); +SELECT b.* FROM b LEFT JOIN a ON (b.a_id = a.id) WHERE (a.id IS NULL OR a.id > 0); + +rollback; + +-- another join removal bug: this is not optimizable, either +begin; + +create temp table innertab (id int8 primary key, dat1 int8); +insert into innertab values(123, 42); + +SELECT * FROM + (SELECT 1 AS x) ss1 + LEFT JOIN + (SELECT q1, q2, COALESCE(dat1, q1) AS y + FROM int8_tbl LEFT JOIN innertab ON q2 = id) ss2 + ON true; + +rollback; + +-- another join removal bug: we must clean up correctly when removing a PHV +begin; + +create temp table uniquetbl (f1 text unique); + +explain (costs off) +select t1.* from + uniquetbl as t1 + left join (select *, '***'::text as d1 from uniquetbl) t2 + on t1.f1 = t2.f1 + left join uniquetbl t3 + on t2.d1 = t3.f1; + +explain (costs off) +select t0.* +from + text_tbl t0 + left join + (select case t1.ten when 0 then 'doh!'::text else null::text end as case1, + t1.stringu2 + from tenk1 t1 + join int4_tbl i4 ON i4.f1 = t1.unique2 + left join uniquetbl u1 ON u1.f1 = t1.string4) ss + on t0.f1 = ss.case1 +where ss.stringu2 !~* ss.case1; + +select t0.* +from + text_tbl t0 + left join + (select case t1.ten when 0 then 'doh!'::text else null::text end as case1, + t1.stringu2 + from tenk1 t1 + join int4_tbl i4 ON i4.f1 = t1.unique2 + left join uniquetbl u1 ON u1.f1 = t1.string4) ss + on t0.f1 = ss.case1 +where ss.stringu2 !~* ss.case1; + +rollback; + +-- bug #8444: we've historically allowed duplicate aliases within aliased JOINs + +select * from + int8_tbl x join (int4_tbl x cross join int4_tbl y) j on q1 = f1; -- error +select * from + int8_tbl x join (int4_tbl x cross join int4_tbl y) j on q1 = y.f1; -- error +select * from + int8_tbl x join (int4_tbl x cross join int4_tbl y(ff)) j on q1 = f1; -- ok + +-- +-- Test hints given on incorrect column references are useful +-- + +select t1.uunique1 from + tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, prefer "t1" suggestion +select t2.uunique1 from + tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, prefer "t2" suggestion +select uunique1 from + tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once + +-- +-- Take care to reference the correct RTE +-- + +select atts.relid::regclass, s.* from pg_stats s join + pg_attribute a on s.attname = a.attname and s.tablename = + a.attrelid::regclass::text join (select unnest(indkey) attnum, + indexrelid from pg_index i) atts on atts.attnum = a.attnum where + schemaname != 'pg_catalog'; + +-- +-- Test LATERAL +-- + +select unique2, x.* +from tenk1 a, lateral (select * from int4_tbl b where f1 = a.unique1) x; +explain (costs off) + select unique2, x.* + from tenk1 a, lateral (select * from int4_tbl b where f1 = a.unique1) x; +select unique2, x.* +from int4_tbl x, lateral (select unique2 from tenk1 where f1 = unique1) ss; +explain (costs off) + select unique2, x.* + from int4_tbl x, lateral (select unique2 from tenk1 where f1 = unique1) ss; +explain (costs off) + select unique2, x.* + from int4_tbl x cross join lateral (select unique2 from tenk1 where f1 = unique1) ss; +select unique2, x.* +from int4_tbl x left join lateral (select unique1, unique2 from tenk1 where f1 = unique1) ss on true; +explain (costs off) + select unique2, x.* + from int4_tbl x left join lateral (select unique1, unique2 from tenk1 where f1 = unique1) ss on true; + +-- check scoping of lateral versus parent references +-- the first of these should return int8_tbl.q2, the second int8_tbl.q1 +select *, (select r from (select q1 as q2) x, (select q2 as r) y) from int8_tbl; +select *, (select r from (select q1 as q2) x, lateral (select q2 as r) y) from int8_tbl; + +-- lateral with function in FROM +select count(*) from tenk1 a, lateral generate_series(1,two) g; +explain (costs off) + select count(*) from tenk1 a, lateral generate_series(1,two) g; +explain (costs off) + select count(*) from tenk1 a cross join lateral generate_series(1,two) g; +-- don't need the explicit LATERAL keyword for functions +explain (costs off) + select count(*) from tenk1 a, generate_series(1,two) g; + +-- lateral with UNION ALL subselect +explain (costs off) + select * from generate_series(100,200) g, + lateral (select * from int8_tbl a where g = q1 union all + select * from int8_tbl b where g = q2) ss; +select * from generate_series(100,200) g, + lateral (select * from int8_tbl a where g = q1 union all + select * from int8_tbl b where g = q2) ss; + +-- lateral with VALUES +explain (costs off) + select count(*) from tenk1 a, + tenk1 b join lateral (values(a.unique1)) ss(x) on b.unique2 = ss.x; +select count(*) from tenk1 a, + tenk1 b join lateral (values(a.unique1)) ss(x) on b.unique2 = ss.x; + +-- lateral with VALUES, no flattening possible +explain (costs off) + select count(*) from tenk1 a, + tenk1 b join lateral (values(a.unique1),(-1)) ss(x) on b.unique2 = ss.x; +select count(*) from tenk1 a, + tenk1 b join lateral (values(a.unique1),(-1)) ss(x) on b.unique2 = ss.x; + +-- lateral injecting a strange outer join condition +explain (costs off) + select * from int8_tbl a, + int8_tbl x left join lateral (select a.q1 from int4_tbl y) ss(z) + on x.q2 = ss.z + order by a.q1, a.q2, x.q1, x.q2, ss.z; +select * from int8_tbl a, + int8_tbl x left join lateral (select a.q1 from int4_tbl y) ss(z) + on x.q2 = ss.z + order by a.q1, a.q2, x.q1, x.q2, ss.z; + +-- lateral reference to a join alias variable +select * from (select f1/2 as x from int4_tbl) ss1 join int4_tbl i4 on x = f1, + lateral (select x) ss2(y); +select * from (select f1 as x from int4_tbl) ss1 join int4_tbl i4 on x = f1, + lateral (values(x)) ss2(y); +select * from ((select f1/2 as x from int4_tbl) ss1 join int4_tbl i4 on x = f1) j, + lateral (select x) ss2(y); + +-- lateral references requiring pullup +select * from (values(1)) x(lb), + lateral generate_series(lb,4) x4; +select * from (select f1/1000000000 from int4_tbl) x(lb), + lateral generate_series(lb,4) x4; +select * from (values(1)) x(lb), + lateral (values(lb)) y(lbcopy); +select * from (values(1)) x(lb), + lateral (select lb from int4_tbl) y(lbcopy); +select * from + int8_tbl x left join (select q1,coalesce(q2,0) q2 from int8_tbl) y on x.q2 = y.q1, + lateral (values(x.q1,y.q1,y.q2)) v(xq1,yq1,yq2); +select * from + int8_tbl x left join (select q1,coalesce(q2,0) q2 from int8_tbl) y on x.q2 = y.q1, + lateral (select x.q1,y.q1,y.q2) v(xq1,yq1,yq2); +select x.* from + int8_tbl x left join (select q1,coalesce(q2,0) q2 from int8_tbl) y on x.q2 = y.q1, + lateral (select x.q1,y.q1,y.q2) v(xq1,yq1,yq2); +select v.* from + (int8_tbl x left join (select q1,coalesce(q2,0) q2 from int8_tbl) y on x.q2 = y.q1) + left join int4_tbl z on z.f1 = x.q2, + lateral (select x.q1,y.q1 union all select x.q2,y.q2) v(vx,vy); +select v.* from + (int8_tbl x left join (select q1,(select coalesce(q2,0)) q2 from int8_tbl) y on x.q2 = y.q1) + left join int4_tbl z on z.f1 = x.q2, + lateral (select x.q1,y.q1 union all select x.q2,y.q2) v(vx,vy); +select v.* from + (int8_tbl x left join (select q1,(select coalesce(q2,0)) q2 from int8_tbl) y on x.q2 = y.q1) + left join int4_tbl z on z.f1 = x.q2, + lateral (select x.q1,y.q1 from onerow union all select x.q2,y.q2 from onerow) v(vx,vy); + +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; +select * from + int8_tbl a left join + lateral (select *, a.q2 as x from int8_tbl b) ss on a.q2 = ss.q1; +explain (verbose, costs off) +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; +select * from + int8_tbl a left join + lateral (select *, coalesce(a.q2, 42) as x from int8_tbl b) ss on a.q2 = ss.q1; + +-- lateral can result in join conditions appearing below their +-- real semantic level +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; +select * from int4_tbl i left join + lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; +explain (verbose, costs off) +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; +select * from int4_tbl i left join + lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; +explain (verbose, costs off) +select * from int4_tbl a, + lateral ( + select * from int4_tbl b left join int8_tbl c on (b.f1 = q1 and a.f1 = q2) + ) ss; +select * from int4_tbl a, + lateral ( + select * from int4_tbl b left join int8_tbl c on (b.f1 = q1 and a.f1 = q2) + ) ss; + +-- lateral reference in a PlaceHolderVar evaluated at join level +explain (verbose, costs off) +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; +select * from + int8_tbl a left join lateral + (select b.q1 as bq1, c.q1 as cq1, least(a.q1,b.q1,c.q1) from + int8_tbl b cross join int8_tbl c) ss + on a.q2 = ss.bq1; + +-- case requiring nested PlaceHolderVars +explain (verbose, costs off) +select * from + int8_tbl c left join ( + int8_tbl a left join (select q1, coalesce(q2,42) as x from int8_tbl b) ss1 + on a.q2 = ss1.q1 + cross join + lateral (select q1, coalesce(ss1.x,q2) as y from int8_tbl d) ss2 + ) on c.q2 = ss2.q1, + lateral (select ss2.y offset 0) ss3; + +-- case that breaks the old ph_may_need optimization +explain (verbose, costs off) +select c.*,a.*,ss1.q1,ss2.q1,ss3.* from + int8_tbl c left join ( + int8_tbl a left join + (select q1, coalesce(q2,f1) as x from int8_tbl b, int4_tbl b2 + where q1 < f1) ss1 + on a.q2 = ss1.q1 + cross join + lateral (select q1, coalesce(ss1.x,q2) as y from int8_tbl d) ss2 + ) on c.q2 = ss2.q1, + lateral (select * from int4_tbl i where ss2.y > f1) ss3; + +-- check processing of postponed quals (bug #9041) +explain (verbose, costs off) +select * from + (select 1 as x offset 0) x cross join (select 2 as y offset 0) y + left join lateral ( + select * from (select 3 as z offset 0) z where z.z = x.x + ) zz on zz.z = y.y; + +-- check dummy rels with lateral references (bug #15694) +explain (verbose, costs off) +select * from int8_tbl i8 left join lateral + (select *, i8.q2 from int4_tbl where false) ss on true; +explain (verbose, costs off) +select * from int8_tbl i8 left join lateral + (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true; + +-- check handling of nested appendrels inside LATERAL +select * from + ((select 2 as v) union all (select 3 as v)) as q1 + cross join lateral + ((select * from + ((select 4 as v) union all (select 5 as v)) as q3) + union all + (select q1.v) + ) as q2; + +-- check we don't try to do a unique-ified semijoin with LATERAL +explain (verbose, costs off) +select * from + (values (0,9998), (1,1000)) v(id,x), + lateral (select f1 from int4_tbl + where f1 = any (select unique1 from tenk1 + where unique2 = v.x offset 0)) ss; +select * from + (values (0,9998), (1,1000)) v(id,x), + lateral (select f1 from int4_tbl + where f1 = any (select unique1 from tenk1 + where unique2 = v.x offset 0)) ss; + +-- check proper extParam/allParam handling (this isn't exactly a LATERAL issue, +-- but we can make the test case much more compact with LATERAL) +explain (verbose, costs off) +select * from (values (0), (1)) v(id), +lateral (select * from int8_tbl t1, + lateral (select * from + (select * from int8_tbl t2 + where q1 = any (select q2 from int8_tbl t3 + where q2 = (select greatest(t1.q1,t2.q2)) + and (select v.id=0)) offset 0) ss2) ss + where t1.q1 = ss.q2) ss0; + +select * from (values (0), (1)) v(id), +lateral (select * from int8_tbl t1, + lateral (select * from + (select * from int8_tbl t2 + where q1 = any (select q2 from int8_tbl t3 + where q2 = (select greatest(t1.q1,t2.q2)) + and (select v.id=0)) offset 0) ss2) ss + where t1.q1 = ss.q2) ss0; + +-- test some error cases where LATERAL should have been used but wasn't +select f1,g from int4_tbl a, (select f1 as g) ss; +select f1,g from int4_tbl a, (select a.f1 as g) ss; +select f1,g from int4_tbl a cross join (select f1 as g) ss; +select f1,g from int4_tbl a cross join (select a.f1 as g) ss; +-- SQL:2008 says the left table is in scope but illegal to access here +select f1,g from int4_tbl a right join lateral generate_series(0, a.f1) g on true; +select f1,g from int4_tbl a full join lateral generate_series(0, a.f1) g on true; +-- check we complain about ambiguous table references +select * from + int8_tbl x cross join (int4_tbl x cross join lateral (select x.f1) ss); +-- LATERAL can be used to put an aggregate into the FROM clause of its query +select 1 from tenk1 a, lateral (select max(a.unique1) from int4_tbl b) ss; + +-- check behavior of LATERAL in UPDATE/DELETE + +create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl; + +-- error, can't do this: +update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; +update xx1 set x2 = f1 from (select * from int4_tbl where f1 = xx1.x1) ss; +-- can't do it even with LATERAL: +update xx1 set x2 = f1 from lateral (select * from int4_tbl where f1 = x1) ss; +-- we might in future allow something like this, but for now it's an error: +update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1) ss; + +-- also errors: +delete from xx1 using (select * from int4_tbl where f1 = x1) ss; +delete from xx1 using (select * from int4_tbl where f1 = xx1.x1) ss; +delete from xx1 using lateral (select * from int4_tbl where f1 = x1) ss; + +-- +-- test LATERAL reference propagation down a multi-level inheritance hierarchy +-- produced for a multi-level partitioned table hierarchy. +-- +create table join_pt1 (a int, b int, c varchar) partition by range(a); +create table join_pt1p1 partition of join_pt1 for values from (0) to (100) partition by range(b); +create table join_pt1p2 partition of join_pt1 for values from (100) to (200); +create table join_pt1p1p1 partition of join_pt1p1 for values from (0) to (100); +insert into join_pt1 values (1, 1, 'x'), (101, 101, 'y'); +create table join_ut1 (a int, b int, c varchar); +insert into join_ut1 values (101, 101, 'y'), (2, 2, 'z'); +explain (verbose, costs off) +select t1.b, ss.phv from join_ut1 t1 left join lateral + (select t2.a as t2a, t3.a t3a, least(t1.a, t2.a, t3.a) phv + from join_pt1 t2 join join_ut1 t3 on t2.a = t3.b) ss + on t1.a = ss.t2a order by t1.a; +select t1.b, ss.phv from join_ut1 t1 left join lateral + (select t2.a as t2a, t3.a t3a, least(t1.a, t2.a, t3.a) phv + from join_pt1 t2 join join_ut1 t3 on t2.a = t3.b) ss + on t1.a = ss.t2a order by t1.a; + +drop table join_pt1; +drop table join_ut1; +-- +-- test that foreign key join estimation performs sanely for outer joins +-- + +begin; + +create table fkest (a int, b int, c int unique, primary key(a,b)); +create table fkest1 (a int, b int, primary key(a,b)); + +insert into fkest select x/10, x%10, x from generate_series(1,1000) x; +insert into fkest1 select x/10, x%10 from generate_series(1,1000) x; + +alter table fkest1 + add constraint fkest1_a_b_fkey foreign key (a,b) references fkest; + +analyze fkest; +analyze fkest1; + +explain (costs off) +select * +from fkest f + left join fkest1 f1 on f.a = f1.a and f.b = f1.b + left join fkest1 f2 on f.a = f2.a and f.b = f2.b + left join fkest1 f3 on f.a = f3.a and f.b = f3.b +where f.c = 1; + +rollback; + +-- +-- test planner's ability to mark joins as unique +-- + +create table j1 (id int primary key); +create table j2 (id int primary key); +create table j3 (id int); + +insert into j1 values(1),(2),(3); +insert into j2 values(1),(2),(3); +insert into j3 values(1),(1); + +analyze j1; +analyze j2; +analyze j3; + +-- ensure join is properly marked as unique +explain (verbose, costs off) +select * from j1 inner join j2 on j1.id = j2.id; + +-- ensure join is not unique when not an equi-join +explain (verbose, costs off) +select * from j1 inner join j2 on j1.id > j2.id; + +-- ensure non-unique rel is not chosen as inner +explain (verbose, costs off) +select * from j1 inner join j3 on j1.id = j3.id; + +-- ensure left join is marked as unique +explain (verbose, costs off) +select * from j1 left join j2 on j1.id = j2.id; + +-- ensure right join is marked as unique +explain (verbose, costs off) +select * from j1 right join j2 on j1.id = j2.id; + +-- ensure full join is marked as unique +explain (verbose, costs off) +select * from j1 full join j2 on j1.id = j2.id; + +-- a clauseless (cross) join can't be unique +explain (verbose, costs off) +select * from j1 cross join j2; + +-- ensure a natural join is marked as unique +explain (verbose, costs off) +select * from j1 natural join j2; + +-- ensure a distinct clause allows the inner to become unique +explain (verbose, costs off) +select * from j1 +inner join (select distinct id from j3) j3 on j1.id = j3.id; + +-- ensure group by clause allows the inner to become unique +explain (verbose, costs off) +select * from j1 +inner join (select id from j3 group by id) j3 on j1.id = j3.id; + +drop table j1; +drop table j2; +drop table j3; + +-- test more complex permutations of unique joins + +create table j1 (id1 int, id2 int, primary key(id1,id2)); +create table j2 (id1 int, id2 int, primary key(id1,id2)); +create table j3 (id1 int, id2 int, primary key(id1,id2)); + +insert into j1 values(1,1),(1,2); +insert into j2 values(1,1); +insert into j3 values(1,1); + +analyze j1; +analyze j2; +analyze j3; + +-- ensure there's no unique join when not all columns which are part of the +-- unique index are seen in the join clause +explain (verbose, costs off) +select * from j1 +inner join j2 on j1.id1 = j2.id1; + +-- ensure proper unique detection with multiple join quals +explain (verbose, costs off) +select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2; + +-- ensure we don't detect the join to be unique when quals are not part of the +-- join condition +explain (verbose, costs off) +select * from j1 +inner join j2 on j1.id1 = j2.id1 where j1.id2 = 1; + +-- as above, but for left joins. +explain (verbose, costs off) +select * from j1 +left join j2 on j1.id1 = j2.id1 where j1.id2 = 1; + +-- validate logic in merge joins which skips mark and restore. +-- it should only do this if all quals which were used to detect the unique +-- are present as join quals, and not plain quals. +set enable_nestloop to 0; +set enable_hashjoin to 0; +set enable_sort to 0; + +-- create indexes that will be preferred over the PKs to perform the join +create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1; +create index j2_id1_idx on j2 (id1) where id1 % 1000 = 1; + +-- need an additional row in j2, if we want j2_id1_idx to be preferred +insert into j2 values(1,2); +analyze j2; + +explain (costs off) select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1; + +select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1; + +-- Exercise array keys mark/restore B-Tree code +explain (costs off) select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 = any (array[1]); + +select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 = any (array[1]); + +-- Exercise array keys "find extreme element" B-Tree code +explain (costs off) select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]); + +select * from j1 +inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 +where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1 and j2.id1 >= any (array[1,5]); + +reset enable_nestloop; +reset enable_hashjoin; +reset enable_sort; + +drop table j1; +drop table j2; +drop table j3; + +-- check that semijoin inner is not seen as unique for a portion of the outerrel +explain (verbose, costs off) +select t1.unique1, t2.hundred +from onek t1, tenk1 t2 +where exists (select 1 from tenk1 t3 + where t3.thousand = t1.unique1 and t3.tenthous = t2.hundred) + and t1.unique1 < 1; + +-- ... unless it actually is unique +create table j3 as select unique1, tenthous from onek; +vacuum analyze j3; +create unique index on j3(unique1, tenthous); + +explain (verbose, costs off) +select t1.unique1, t2.hundred +from onek t1, tenk1 t2 +where exists (select 1 from j3 + where j3.unique1 = t1.unique1 and j3.tenthous = t2.hundred) + and t1.unique1 < 1; + +drop table j3; diff --git a/src/test/resources/postgresql-corpus/limit.sql b/src/test/resources/postgresql-corpus/limit.sql new file mode 100644 index 0000000..81618bc --- /dev/null +++ b/src/test/resources/postgresql-corpus/limit.sql @@ -0,0 +1,198 @@ +-- +-- LIMIT +-- Check the LIMIT/OFFSET feature of SELECT +-- + +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 50 + ORDER BY unique1 LIMIT 2; +SELECT ''::text AS five, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 60 + ORDER BY unique1 LIMIT 5; +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 60 AND unique1 < 63 + ORDER BY unique1 LIMIT 5; +SELECT ''::text AS three, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 100 + ORDER BY unique1 LIMIT 3 OFFSET 20; +SELECT ''::text AS zero, unique1, unique2, stringu1 + FROM onek WHERE unique1 < 50 + ORDER BY unique1 DESC LIMIT 8 OFFSET 99; +SELECT ''::text AS eleven, unique1, unique2, stringu1 + FROM onek WHERE unique1 < 50 + ORDER BY unique1 DESC LIMIT 20 OFFSET 39; +SELECT ''::text AS ten, unique1, unique2, stringu1 + FROM onek + ORDER BY unique1 OFFSET 990; +SELECT ''::text AS five, unique1, unique2, stringu1 + FROM onek + ORDER BY unique1 OFFSET 990 LIMIT 5; +SELECT ''::text AS five, unique1, unique2, stringu1 + FROM onek + ORDER BY unique1 LIMIT 5 OFFSET 900; + +-- Test null limit and offset. The planner would discard a simple null +-- constant, so to ensure executor is exercised, do this: +select * from int8_tbl limit (case when random() < 0.5 then null::bigint end); +select * from int8_tbl offset (case when random() < 0.5 then null::bigint end); + +-- Test assorted cases involving backwards fetch from a LIMIT plan node +begin; + +declare c1 cursor for select * from int8_tbl limit 10; +fetch all in c1; +fetch 1 in c1; +fetch backward 1 in c1; +fetch backward all in c1; +fetch backward 1 in c1; +fetch all in c1; + +declare c2 cursor for select * from int8_tbl limit 3; +fetch all in c2; +fetch 1 in c2; +fetch backward 1 in c2; +fetch backward all in c2; +fetch backward 1 in c2; +fetch all in c2; + +declare c3 cursor for select * from int8_tbl offset 3; +fetch all in c3; +fetch 1 in c3; +fetch backward 1 in c3; +fetch backward all in c3; +fetch backward 1 in c3; +fetch all in c3; + +declare c4 cursor for select * from int8_tbl offset 10; +fetch all in c4; +fetch 1 in c4; +fetch backward 1 in c4; +fetch backward all in c4; +fetch backward 1 in c4; +fetch all in c4; + +declare c5 cursor for select * from int8_tbl order by q1 fetch first 2 rows with ties; +fetch all in c5; +fetch 1 in c5; +fetch backward 1 in c5; +fetch backward 1 in c5; +fetch all in c5; +fetch backward all in c5; +fetch all in c5; +fetch backward all in c5; + +rollback; + +-- Stress test for variable LIMIT in conjunction with bounded-heap sorting + +SELECT + (SELECT n + FROM (VALUES (1)) AS x, + (SELECT n FROM generate_series(1,10) AS n + ORDER BY n LIMIT 1 OFFSET s-1) AS y) AS z + FROM generate_series(1,10) AS s; + +-- +-- Test behavior of volatile and set-returning functions in conjunction +-- with ORDER BY and LIMIT. +-- + +create temp sequence testseq; + +explain (verbose, costs off) +select unique1, unique2, nextval('testseq') + from tenk1 order by unique2 limit 10; + +select unique1, unique2, nextval('testseq') + from tenk1 order by unique2 limit 10; + +select currval('testseq'); + +explain (verbose, costs off) +select unique1, unique2, nextval('testseq') + from tenk1 order by tenthous limit 10; + +select unique1, unique2, nextval('testseq') + from tenk1 order by tenthous limit 10; + +select currval('testseq'); + +explain (verbose, costs off) +select unique1, unique2, generate_series(1,10) + from tenk1 order by unique2 limit 7; + +select unique1, unique2, generate_series(1,10) + from tenk1 order by unique2 limit 7; + +explain (verbose, costs off) +select unique1, unique2, generate_series(1,10) + from tenk1 order by tenthous limit 7; + +select unique1, unique2, generate_series(1,10) + from tenk1 order by tenthous limit 7; + + + +-- test for failure to set all aggregates' aggtranstype +explain (verbose, costs off) +select sum(tenthous) as s1, sum(tenthous) + random()*0 as s2 + from tenk1 group by thousand order by thousand limit 3; + +select sum(tenthous) as s1, sum(tenthous) + random()*0 as s2 + from tenk1 group by thousand order by thousand limit 3; + +-- +-- FETCH FIRST +-- Check the WITH TIES clause +-- + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 ROW WITH TIES; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST ROWS WITH TIES; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 1 ROW WITH TIES; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 ROW ONLY; + +-- should fail +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 50 + FETCH FIRST 2 ROW WITH TIES; + +-- test ruleutils +CREATE VIEW limit_thousand_v_1 AS SELECT thousand FROM onek WHERE thousand < 995 + ORDER BY thousand FETCH FIRST 5 ROWS WITH TIES OFFSET 10; +\d+ limit_thousand_v_1 +CREATE VIEW limit_thousand_v_2 AS SELECT thousand FROM onek WHERE thousand < 995 + ORDER BY thousand OFFSET 10 FETCH FIRST 5 ROWS ONLY; +\d+ limit_thousand_v_2 +CREATE VIEW limit_thousand_v_3 AS SELECT thousand FROM onek WHERE thousand < 995 + ORDER BY thousand FETCH FIRST NULL ROWS WITH TIES; -- fails +CREATE VIEW limit_thousand_v_3 AS SELECT thousand FROM onek WHERE thousand < 995 + ORDER BY thousand FETCH FIRST (NULL+1) ROWS WITH TIES; +\d+ limit_thousand_v_3 +CREATE VIEW limit_thousand_v_4 AS SELECT thousand FROM onek WHERE thousand < 995 + ORDER BY thousand FETCH FIRST NULL ROWS ONLY; +\d+ limit_thousand_v_4 +-- leave these views +-- use of random() is to keep planner from folding the expressions together +explain (verbose, costs off) +select generate_series(0,2) as s1, generate_series((random()*.1)::int,2) as s2; + +select generate_series(0,2) as s1, generate_series((random()*.1)::int,2) as s2; + +explain (verbose, costs off) +select generate_series(0,2) as s1, generate_series((random()*.1)::int,2) as s2 +order by s2 desc; + +select generate_series(0,2) as s1, generate_series((random()*.1)::int,2) as s2 +order by s2 desc; + diff --git a/src/test/resources/postgresql-corpus/select.sql b/src/test/resources/postgresql-corpus/select.sql new file mode 100644 index 0000000..b5929b2 --- /dev/null +++ b/src/test/resources/postgresql-corpus/select.sql @@ -0,0 +1,264 @@ +-- +-- SELECT +-- + +-- btree index +-- awk '{if($1<10){print;}else{next;}}' onek.data | sort +0n -1 +-- +SELECT * FROM onek + WHERE onek.unique1 < 10 + ORDER BY onek.unique1; + +-- +-- awk '{if($1<20){print $1,$14;}else{next;}}' onek.data | sort +0nr -1 +-- +SELECT onek.unique1, onek.stringu1 FROM onek + WHERE onek.unique1 < 20 + ORDER BY unique1 using >; + +-- +-- awk '{if($1>980){print $1,$14;}else{next;}}' onek.data | sort +1d -2 +-- +SELECT onek.unique1, onek.stringu1 FROM onek + WHERE onek.unique1 > 980 + ORDER BY stringu1 using <; + +-- +-- awk '{if($1>980){print $1,$16;}else{next;}}' onek.data | +-- sort +1d -2 +0nr -1 +-- +SELECT onek.unique1, onek.string4 FROM onek + WHERE onek.unique1 > 980 + ORDER BY string4 using <, unique1 using >; + +-- +-- awk '{if($1>980){print $1,$16;}else{next;}}' onek.data | +-- sort +1dr -2 +0n -1 +-- +SELECT onek.unique1, onek.string4 FROM onek + WHERE onek.unique1 > 980 + ORDER BY string4 using >, unique1 using <; + +-- +-- awk '{if($1<20){print $1,$16;}else{next;}}' onek.data | +-- sort +0nr -1 +1d -2 +-- +SELECT onek.unique1, onek.string4 FROM onek + WHERE onek.unique1 < 20 + ORDER BY unique1 using >, string4 using <; + +-- +-- awk '{if($1<20){print $1,$16;}else{next;}}' onek.data | +-- sort +0n -1 +1dr -2 +-- +SELECT onek.unique1, onek.string4 FROM onek + WHERE onek.unique1 < 20 + ORDER BY unique1 using <, string4 using >; + +-- +-- test partial btree indexes +-- +-- As of 7.2, planner probably won't pick an indexscan without stats, +-- so ANALYZE first. Also, we want to prevent it from picking a bitmapscan +-- followed by sort, because that could hide index ordering problems. +-- +ANALYZE onek2; + +SET enable_seqscan TO off; +SET enable_bitmapscan TO off; +SET enable_sort TO off; + +-- +-- awk '{if($1<10){print $0;}else{next;}}' onek.data | sort +0n -1 +-- +SELECT onek2.* FROM onek2 WHERE onek2.unique1 < 10; + +-- +-- awk '{if($1<20){print $1,$14;}else{next;}}' onek.data | sort +0nr -1 +-- +SELECT onek2.unique1, onek2.stringu1 FROM onek2 + WHERE onek2.unique1 < 20 + ORDER BY unique1 using >; + +-- +-- awk '{if($1>980){print $1,$14;}else{next;}}' onek.data | sort +1d -2 +-- +SELECT onek2.unique1, onek2.stringu1 FROM onek2 + WHERE onek2.unique1 > 980; + +RESET enable_seqscan; +RESET enable_bitmapscan; +RESET enable_sort; + + +SELECT two, stringu1, ten, string4 + INTO TABLE tmp + FROM onek; + +-- +-- awk '{print $1,$2;}' person.data | +-- awk '{if(NF!=2){print $3,$2;}else{print;}}' - emp.data | +-- awk '{if(NF!=2){print $3,$2;}else{print;}}' - student.data | +-- awk 'BEGIN{FS=" ";}{if(NF!=2){print $4,$5;}else{print;}}' - stud_emp.data +-- +-- SELECT name, age FROM person*; ??? check if different +SELECT p.name, p.age FROM person* p; + +-- +-- awk '{print $1,$2;}' person.data | +-- awk '{if(NF!=2){print $3,$2;}else{print;}}' - emp.data | +-- awk '{if(NF!=2){print $3,$2;}else{print;}}' - student.data | +-- awk 'BEGIN{FS=" ";}{if(NF!=1){print $4,$5;}else{print;}}' - stud_emp.data | +-- sort +1nr -2 +-- +SELECT p.name, p.age FROM person* p ORDER BY age using >, name; + +-- +-- Test some cases involving whole-row Var referencing a subquery +-- +select foo from (select 1 offset 0) as foo; +select foo from (select null offset 0) as foo; +select foo from (select 'xyzzy',1,null offset 0) as foo; + +-- +-- Test VALUES lists +-- +select * from onek, (values(147, 'RFAAAA'), (931, 'VJAAAA')) as v (i, j) + WHERE onek.unique1 = v.i and onek.stringu1 = v.j; + +-- a more complex case +-- looks like we're coding lisp :-) +select * from onek, + (values ((select i from + (values(10000), (2), (389), (1000), (2000), ((select 10029))) as foo(i) + order by i asc limit 1))) bar (i) + where onek.unique1 = bar.i; + +-- try VALUES in a subquery +select * from onek + where (unique1,ten) in (values (1,1), (20,0), (99,9), (17,99)) + order by unique1; + +-- VALUES is also legal as a standalone query or a set-operation member +VALUES (1,2), (3,4+4), (7,77.7); + +VALUES (1,2), (3,4+4), (7,77.7) +UNION ALL +SELECT 2+2, 57 +UNION ALL +TABLE int8_tbl; + +-- +-- Test ORDER BY options +-- + +CREATE TEMP TABLE foo (f1 int); + +INSERT INTO foo VALUES (42),(3),(10),(7),(null),(null),(1); + +SELECT * FROM foo ORDER BY f1; +SELECT * FROM foo ORDER BY f1 ASC; -- same thing +SELECT * FROM foo ORDER BY f1 NULLS FIRST; +SELECT * FROM foo ORDER BY f1 DESC; +SELECT * FROM foo ORDER BY f1 DESC NULLS LAST; + +-- check if indexscans do the right things +CREATE INDEX fooi ON foo (f1); +SET enable_sort = false; + +SELECT * FROM foo ORDER BY f1; +SELECT * FROM foo ORDER BY f1 NULLS FIRST; +SELECT * FROM foo ORDER BY f1 DESC; +SELECT * FROM foo ORDER BY f1 DESC NULLS LAST; + +DROP INDEX fooi; +CREATE INDEX fooi ON foo (f1 DESC); + +SELECT * FROM foo ORDER BY f1; +SELECT * FROM foo ORDER BY f1 NULLS FIRST; +SELECT * FROM foo ORDER BY f1 DESC; +SELECT * FROM foo ORDER BY f1 DESC NULLS LAST; + +DROP INDEX fooi; +CREATE INDEX fooi ON foo (f1 DESC NULLS LAST); + +SELECT * FROM foo ORDER BY f1; +SELECT * FROM foo ORDER BY f1 NULLS FIRST; +SELECT * FROM foo ORDER BY f1 DESC; +SELECT * FROM foo ORDER BY f1 DESC NULLS LAST; + +-- +-- Test planning of some cases with partial indexes +-- + +-- partial index is usable +explain (costs off) +select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA'; +select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA'; +-- actually run the query with an analyze to use the partial index +explain (costs off, analyze on, timing off, summary off) +select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA'; +explain (costs off) +select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA'; +select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA'; +-- partial index predicate implies clause, so no need for retest +explain (costs off) +select * from onek2 where unique2 = 11 and stringu1 < 'B'; +select * from onek2 where unique2 = 11 and stringu1 < 'B'; +explain (costs off) +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; +-- but if it's an update target, must retest anyway +explain (costs off) +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B' for update; +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B' for update; +-- partial index is not applicable +explain (costs off) +select unique2 from onek2 where unique2 = 11 and stringu1 < 'C'; +select unique2 from onek2 where unique2 = 11 and stringu1 < 'C'; +-- partial index implies clause, but bitmap scan must recheck predicate anyway +SET enable_indexscan TO off; +explain (costs off) +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; +select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; +RESET enable_indexscan; +-- check multi-index cases too +explain (costs off) +select unique1, unique2 from onek2 + where (unique2 = 11 or unique1 = 0) and stringu1 < 'B'; +select unique1, unique2 from onek2 + where (unique2 = 11 or unique1 = 0) and stringu1 < 'B'; +explain (costs off) +select unique1, unique2 from onek2 + where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; +select unique1, unique2 from onek2 + where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; + +-- +-- Test some corner cases that have been known to confuse the planner +-- + +-- ORDER BY on a constant doesn't really need any sorting +SELECT 1 AS x ORDER BY x; + +-- But ORDER BY on a set-valued expression does +create function sillysrf(int) returns setof int as + 'values (1),(10),(2),($1)' language sql immutable; + +select sillysrf(42); +select sillysrf(-1) order by 1; + +drop function sillysrf(int); + +-- X = X isn't a no-op, it's effectively X IS NOT NULL assuming = is strict +-- (see bug #5084) +select * from (values (2),(null),(1)) v(k) where k = k order by k; +select * from (values (2),(null),(1)) v(k) where k = k; + +-- Test partitioned tables with no partitions, which should be handled the +-- same as the non-inheritance case when expanding its RTE. +create table list_parted_tbl (a int,b int) partition by list (a); +create table list_parted_tbl1 partition of list_parted_tbl + for values in (1) partition by list(b); +explain (costs off) select * from list_parted_tbl; +drop table list_parted_tbl; diff --git a/src/test/resources/postgresql-corpus/select_distinct.sql b/src/test/resources/postgresql-corpus/select_distinct.sql new file mode 100644 index 0000000..3310274 --- /dev/null +++ b/src/test/resources/postgresql-corpus/select_distinct.sql @@ -0,0 +1,137 @@ +-- +-- SELECT_DISTINCT +-- + +-- +-- awk '{print $3;}' onek.data | sort -n | uniq +-- +SELECT DISTINCT two FROM tmp ORDER BY 1; + +-- +-- awk '{print $5;}' onek.data | sort -n | uniq +-- +SELECT DISTINCT ten FROM tmp ORDER BY 1; + +-- +-- awk '{print $16;}' onek.data | sort -d | uniq +-- +SELECT DISTINCT string4 FROM tmp ORDER BY 1; + +-- +-- awk '{print $3,$16,$5;}' onek.data | sort -d | uniq | +-- sort +0n -1 +1d -2 +2n -3 +-- +SELECT DISTINCT two, string4, ten + FROM tmp + ORDER BY two using <, string4 using <, ten using <; + +-- +-- awk '{print $2;}' person.data | +-- awk '{if(NF!=1){print $2;}else{print;}}' - emp.data | +-- awk '{if(NF!=1){print $2;}else{print;}}' - student.data | +-- awk 'BEGIN{FS=" ";}{if(NF!=1){print $5;}else{print;}}' - stud_emp.data | +-- sort -n -r | uniq +-- +SELECT DISTINCT p.age FROM person* p ORDER BY age using >; + +-- +-- Check mentioning same column more than once +-- + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT count(*) FROM + (SELECT DISTINCT two, four, two FROM tenk1) ss; + +SELECT count(*) FROM + (SELECT DISTINCT two, four, two FROM tenk1) ss; + +-- +-- Compare results between plans using sorting and plans using hash +-- aggregation. Force spilling in both cases by setting work_mem low. +-- + +SET work_mem='64kB'; + +-- Produce results with sorting. + +SET enable_hashagg=FALSE; + +SET jit_above_cost=0; + +EXPLAIN (costs off) +SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; + +CREATE TABLE distinct_group_1 AS +SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; + +SET jit_above_cost TO DEFAULT; + +CREATE TABLE distinct_group_2 AS +SELECT DISTINCT (g%1000)::text FROM generate_series(0,9999) g; + +SET enable_hashagg=TRUE; + +-- Produce results with hash aggregation. + +SET enable_sort=FALSE; + +SET jit_above_cost=0; + +EXPLAIN (costs off) +SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; + +CREATE TABLE distinct_hash_1 AS +SELECT DISTINCT g%1000 FROM generate_series(0,9999) g; + +SET jit_above_cost TO DEFAULT; + +CREATE TABLE distinct_hash_2 AS +SELECT DISTINCT (g%1000)::text FROM generate_series(0,9999) g; + +SET enable_sort=TRUE; + +SET work_mem TO DEFAULT; + +-- Compare results + +(SELECT * FROM distinct_hash_1 EXCEPT SELECT * FROM distinct_group_1) + UNION ALL +(SELECT * FROM distinct_group_1 EXCEPT SELECT * FROM distinct_hash_1); + +(SELECT * FROM distinct_hash_1 EXCEPT SELECT * FROM distinct_group_1) + UNION ALL +(SELECT * FROM distinct_group_1 EXCEPT SELECT * FROM distinct_hash_1); + +DROP TABLE distinct_hash_1; +DROP TABLE distinct_hash_2; +DROP TABLE distinct_group_1; +DROP TABLE distinct_group_2; + +-- +-- Also, some tests of IS DISTINCT FROM, which doesn't quite deserve its +-- very own regression file. +-- + +CREATE TEMP TABLE disttable (f1 integer); +INSERT INTO DISTTABLE VALUES(1); +INSERT INTO DISTTABLE VALUES(2); +INSERT INTO DISTTABLE VALUES(3); +INSERT INTO DISTTABLE VALUES(NULL); + +-- basic cases +SELECT f1, f1 IS DISTINCT FROM 2 as "not 2" FROM disttable; +SELECT f1, f1 IS DISTINCT FROM NULL as "not null" FROM disttable; +SELECT f1, f1 IS DISTINCT FROM f1 as "false" FROM disttable; +SELECT f1, f1 IS DISTINCT FROM f1+1 as "not null" FROM disttable; + +-- check that optimizer constant-folds it properly +SELECT 1 IS DISTINCT FROM 2 as "yes"; +SELECT 2 IS DISTINCT FROM 2 as "no"; +SELECT 2 IS DISTINCT FROM null as "yes"; +SELECT null IS DISTINCT FROM null as "no"; + +-- negated form +SELECT 1 IS NOT DISTINCT FROM 2 as "no"; +SELECT 2 IS NOT DISTINCT FROM 2 as "yes"; +SELECT 2 IS NOT DISTINCT FROM null as "no"; +SELECT null IS NOT DISTINCT FROM null as "yes"; diff --git a/src/test/resources/postgresql-corpus/select_having.sql b/src/test/resources/postgresql-corpus/select_having.sql new file mode 100644 index 0000000..bc0cdc0 --- /dev/null +++ b/src/test/resources/postgresql-corpus/select_having.sql @@ -0,0 +1,50 @@ +-- +-- SELECT_HAVING +-- + +-- load test data +CREATE TABLE test_having (a int, b int, c char(8), d char); +INSERT INTO test_having VALUES (0, 1, 'XXXX', 'A'); +INSERT INTO test_having VALUES (1, 2, 'AAAA', 'b'); +INSERT INTO test_having VALUES (2, 2, 'AAAA', 'c'); +INSERT INTO test_having VALUES (3, 3, 'BBBB', 'D'); +INSERT INTO test_having VALUES (4, 3, 'BBBB', 'e'); +INSERT INTO test_having VALUES (5, 3, 'bbbb', 'F'); +INSERT INTO test_having VALUES (6, 4, 'cccc', 'g'); +INSERT INTO test_having VALUES (7, 4, 'cccc', 'h'); +INSERT INTO test_having VALUES (8, 4, 'CCCC', 'I'); +INSERT INTO test_having VALUES (9, 4, 'CCCC', 'j'); + +SELECT b, c FROM test_having + GROUP BY b, c HAVING count(*) = 1 ORDER BY b, c; + +-- HAVING is effectively equivalent to WHERE in this case +SELECT b, c FROM test_having + GROUP BY b, c HAVING b = 3 ORDER BY b, c; + +SELECT lower(c), count(c) FROM test_having + GROUP BY lower(c) HAVING count(*) > 2 OR min(a) = max(a) + ORDER BY lower(c); + +SELECT c, max(a) FROM test_having + GROUP BY c HAVING count(*) > 2 OR min(a) = max(a) + ORDER BY c; + +-- test degenerate cases involving HAVING without GROUP BY +-- Per SQL spec, these should generate 0 or 1 row, even without aggregates + +SELECT min(a), max(a) FROM test_having HAVING min(a) = max(a); +SELECT min(a), max(a) FROM test_having HAVING min(a) < max(a); + +-- errors: ungrouped column references +SELECT a FROM test_having HAVING min(a) < max(a); +SELECT 1 AS one FROM test_having HAVING a > 1; + +-- the really degenerate case: need not scan table at all +SELECT 1 AS one FROM test_having HAVING 1 > 2; +SELECT 1 AS one FROM test_having HAVING 1 < 2; + +-- and just to prove that we aren't scanning the table: +SELECT 1 AS one FROM test_having WHERE 1/a = 1 HAVING 1 < 2; + +DROP TABLE test_having; diff --git a/src/test/resources/postgresql-corpus/subselect.sql b/src/test/resources/postgresql-corpus/subselect.sql new file mode 100644 index 0000000..a56057b --- /dev/null +++ b/src/test/resources/postgresql-corpus/subselect.sql @@ -0,0 +1,859 @@ +-- +-- SUBSELECT +-- + +SELECT 1 AS one WHERE 1 IN (SELECT 1); + +SELECT 1 AS zero WHERE 1 NOT IN (SELECT 1); + +SELECT 1 AS zero WHERE 1 IN (SELECT 2); + +-- Check grammar's handling of extra parens in assorted contexts + +SELECT * FROM (SELECT 1 AS x) ss; +SELECT * FROM ((SELECT 1 AS x)) ss; + +(SELECT 2) UNION SELECT 2; +((SELECT 2)) UNION SELECT 2; + +SELECT ((SELECT 2) UNION SELECT 2); +SELECT (((SELECT 2)) UNION SELECT 2); + +SELECT (SELECT ARRAY[1,2,3])[1]; +SELECT ((SELECT ARRAY[1,2,3]))[2]; +SELECT (((SELECT ARRAY[1,2,3])))[3]; + +-- Set up some simple test tables + +CREATE TABLE SUBSELECT_TBL ( + f1 integer, + f2 integer, + f3 float +); + +INSERT INTO SUBSELECT_TBL VALUES (1, 2, 3); +INSERT INTO SUBSELECT_TBL VALUES (2, 3, 4); +INSERT INTO SUBSELECT_TBL VALUES (3, 4, 5); +INSERT INTO SUBSELECT_TBL VALUES (1, 1, 1); +INSERT INTO SUBSELECT_TBL VALUES (2, 2, 2); +INSERT INTO SUBSELECT_TBL VALUES (3, 3, 3); +INSERT INTO SUBSELECT_TBL VALUES (6, 7, 8); +INSERT INTO SUBSELECT_TBL VALUES (8, 9, NULL); + +SELECT '' AS eight, * FROM SUBSELECT_TBL; + +-- Uncorrelated subselects + +SELECT '' AS two, f1 AS "Constant Select" FROM SUBSELECT_TBL + WHERE f1 IN (SELECT 1); + +SELECT '' AS six, f1 AS "Uncorrelated Field" FROM SUBSELECT_TBL + WHERE f1 IN (SELECT f2 FROM SUBSELECT_TBL); + +SELECT '' AS six, f1 AS "Uncorrelated Field" FROM SUBSELECT_TBL + WHERE f1 IN (SELECT f2 FROM SUBSELECT_TBL WHERE + f2 IN (SELECT f1 FROM SUBSELECT_TBL)); + +SELECT '' AS three, f1, f2 + FROM SUBSELECT_TBL + WHERE (f1, f2) NOT IN (SELECT f2, CAST(f3 AS int4) FROM SUBSELECT_TBL + WHERE f3 IS NOT NULL); + +-- Correlated subselects + +SELECT '' AS six, f1 AS "Correlated Field", f2 AS "Second Field" + FROM SUBSELECT_TBL upper + WHERE f1 IN (SELECT f2 FROM SUBSELECT_TBL WHERE f1 = upper.f1); + +SELECT '' AS six, f1 AS "Correlated Field", f3 AS "Second Field" + FROM SUBSELECT_TBL upper + WHERE f1 IN + (SELECT f2 FROM SUBSELECT_TBL WHERE CAST(upper.f2 AS float) = f3); + +SELECT '' AS six, f1 AS "Correlated Field", f3 AS "Second Field" + FROM SUBSELECT_TBL upper + WHERE f3 IN (SELECT upper.f1 + f2 FROM SUBSELECT_TBL + WHERE f2 = CAST(f3 AS integer)); + +SELECT '' AS five, f1 AS "Correlated Field" + FROM SUBSELECT_TBL + WHERE (f1, f2) IN (SELECT f2, CAST(f3 AS int4) FROM SUBSELECT_TBL + WHERE f3 IS NOT NULL); + +-- +-- Use some existing tables in the regression test +-- + +SELECT '' AS eight, ss.f1 AS "Correlated Field", ss.f3 AS "Second Field" + FROM SUBSELECT_TBL ss + WHERE f1 NOT IN (SELECT f1+1 FROM INT4_TBL + WHERE f1 != ss.f1 AND f1 < 2147483647); + +select q1, float8(count(*)) / (select count(*) from int8_tbl) +from int8_tbl group by q1 order by q1; + +-- Unspecified-type literals in output columns should resolve as text + +SELECT *, pg_typeof(f1) FROM + (SELECT 'foo' AS f1 FROM generate_series(1,3)) ss ORDER BY 1; + +-- ... unless there's context to suggest differently + +explain (verbose, costs off) select '42' union all select '43'; +explain (verbose, costs off) select '42' union all select 43; + +-- check materialization of an initplan reference (bug #14524) +explain (verbose, costs off) +select 1 = all (select (select 1)); +select 1 = all (select (select 1)); + +-- +-- Check EXISTS simplification with LIMIT +-- +explain (costs off) +select * from int4_tbl o where exists + (select 1 from int4_tbl i where i.f1=o.f1 limit null); +explain (costs off) +select * from int4_tbl o where not exists + (select 1 from int4_tbl i where i.f1=o.f1 limit 1); +explain (costs off) +select * from int4_tbl o where exists + (select 1 from int4_tbl i where i.f1=o.f1 limit 0); + +-- +-- Test cases to catch unpleasant interactions between IN-join processing +-- and subquery pullup. +-- + +select count(*) from + (select 1 from tenk1 a + where unique1 IN (select hundred from tenk1 b)) ss; +select count(distinct ss.ten) from + (select ten from tenk1 a + where unique1 IN (select hundred from tenk1 b)) ss; +select count(*) from + (select 1 from tenk1 a + where unique1 IN (select distinct hundred from tenk1 b)) ss; +select count(distinct ss.ten) from + (select ten from tenk1 a + where unique1 IN (select distinct hundred from tenk1 b)) ss; + +-- +-- Test cases to check for overenthusiastic optimization of +-- "IN (SELECT DISTINCT ...)" and related cases. Per example from +-- Luca Pireddu and Michael Fuhr. +-- + +CREATE TEMP TABLE foo (id integer); +CREATE TEMP TABLE bar (id1 integer, id2 integer); + +INSERT INTO foo VALUES (1); + +INSERT INTO bar VALUES (1, 1); +INSERT INTO bar VALUES (2, 2); +INSERT INTO bar VALUES (3, 1); + +-- These cases require an extra level of distinct-ing above subquery s +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT DISTINCT id1, id2 FROM bar) AS s); +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT id1,id2 FROM bar GROUP BY id1,id2) AS s); +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT id1, id2 FROM bar UNION + SELECT id1, id2 FROM bar) AS s); + +-- These cases do not +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT DISTINCT ON (id2) id1, id2 FROM bar) AS s); +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT id2 FROM bar GROUP BY id2) AS s); +SELECT * FROM foo WHERE id IN + (SELECT id2 FROM (SELECT id2 FROM bar UNION + SELECT id2 FROM bar) AS s); + +-- +-- Test case to catch problems with multiply nested sub-SELECTs not getting +-- recalculated properly. Per bug report from Didier Moens. +-- + +CREATE TABLE orderstest ( + approver_ref integer, + po_ref integer, + ordercanceled boolean +); + +INSERT INTO orderstest VALUES (1, 1, false); +INSERT INTO orderstest VALUES (66, 5, false); +INSERT INTO orderstest VALUES (66, 6, false); +INSERT INTO orderstest VALUES (66, 7, false); +INSERT INTO orderstest VALUES (66, 1, true); +INSERT INTO orderstest VALUES (66, 8, false); +INSERT INTO orderstest VALUES (66, 1, false); +INSERT INTO orderstest VALUES (77, 1, false); +INSERT INTO orderstest VALUES (1, 1, false); +INSERT INTO orderstest VALUES (66, 1, false); +INSERT INTO orderstest VALUES (1, 1, false); + +CREATE VIEW orders_view AS +SELECT *, +(SELECT CASE + WHEN ord.approver_ref=1 THEN '---' ELSE 'Approved' + END) AS "Approved", +(SELECT CASE + WHEN ord.ordercanceled + THEN 'Canceled' + ELSE + (SELECT CASE + WHEN ord.po_ref=1 + THEN + (SELECT CASE + WHEN ord.approver_ref=1 + THEN '---' + ELSE 'Approved' + END) + ELSE 'PO' + END) +END) AS "Status", +(CASE + WHEN ord.ordercanceled + THEN 'Canceled' + ELSE + (CASE + WHEN ord.po_ref=1 + THEN + (CASE + WHEN ord.approver_ref=1 + THEN '---' + ELSE 'Approved' + END) + ELSE 'PO' + END) +END) AS "Status_OK" +FROM orderstest ord; + +SELECT * FROM orders_view; + +DROP TABLE orderstest cascade; + +-- +-- Test cases to catch situations where rule rewriter fails to propagate +-- hasSubLinks flag correctly. Per example from Kyle Bateman. +-- + +create temp table parts ( + partnum text, + cost float8 +); + +create temp table shipped ( + ttype char(2), + ordnum int4, + partnum text, + value float8 +); + +create temp view shipped_view as + select * from shipped where ttype = 'wt'; + +create rule shipped_view_insert as on insert to shipped_view do instead + insert into shipped values('wt', new.ordnum, new.partnum, new.value); + +insert into parts (partnum, cost) values (1, 1234.56); + +insert into shipped_view (ordnum, partnum, value) + values (0, 1, (select cost from parts where partnum = '1')); + +select * from shipped_view; + +create rule shipped_view_update as on update to shipped_view do instead + update shipped set partnum = new.partnum, value = new.value + where ttype = new.ttype and ordnum = new.ordnum; + +update shipped_view set value = 11 + from int4_tbl a join int4_tbl b + on (a.f1 = (select f1 from int4_tbl c where c.f1=b.f1)) + where ordnum = a.f1; + +select * from shipped_view; + +select f1, ss1 as relabel from + (select *, (select sum(f1) from int4_tbl b where f1 >= a.f1) as ss1 + from int4_tbl a) ss; + +-- +-- Test cases involving PARAM_EXEC parameters and min/max index optimizations. +-- Per bug report from David Sanchez i Gregori. +-- + +select * from ( + select max(unique1) from tenk1 as a + where exists (select 1 from tenk1 as b where b.thousand = a.unique2) +) ss; + +select * from ( + select min(unique1) from tenk1 as a + where not exists (select 1 from tenk1 as b where b.unique2 = 10000) +) ss; + +-- +-- Test that an IN implemented using a UniquePath does unique-ification +-- with the right semantics, as per bug #4113. (Unfortunately we have +-- no simple way to ensure that this test case actually chooses that type +-- of plan, but it does in releases 7.4-8.3. Note that an ordering difference +-- here might mean that some other plan type is being used, rendering the test +-- pointless.) +-- + +create temp table numeric_table (num_col numeric); +insert into numeric_table values (1), (1.000000000000000000001), (2), (3); + +create temp table float_table (float_col float8); +insert into float_table values (1), (2), (3); + +select * from float_table + where float_col in (select num_col from numeric_table); + +select * from numeric_table + where num_col in (select float_col from float_table); + +-- +-- Test case for bug #4290: bogus calculation of subplan param sets +-- + +create temp table ta (id int primary key, val int); + +insert into ta values(1,1); +insert into ta values(2,2); + +create temp table tb (id int primary key, aval int); + +insert into tb values(1,1); +insert into tb values(2,1); +insert into tb values(3,2); +insert into tb values(4,2); + +create temp table tc (id int primary key, aid int); + +insert into tc values(1,1); +insert into tc values(2,2); + +select + ( select min(tb.id) from tb + where tb.aval = (select ta.val from ta where ta.id = tc.aid) ) as min_tb_id +from tc; + +-- +-- Test case for 8.3 "failed to locate grouping columns" bug +-- + +create temp table t1 (f1 numeric(14,0), f2 varchar(30)); + +select * from + (select distinct f1, f2, (select f2 from t1 x where x.f1 = up.f1) as fs + from t1 up) ss +group by f1,f2,fs; + +-- +-- Test case for bug #5514 (mishandling of whole-row Vars in subselects) +-- + +create temp table table_a(id integer); +insert into table_a values (42); + +create temp view view_a as select * from table_a; + +select view_a from view_a; +select (select view_a) from view_a; +select (select (select view_a)) from view_a; +select (select (a.*)::text) from view_a a; + +-- +-- Check that whole-row Vars reading the result of a subselect don't include +-- any junk columns therein +-- + +select q from (select max(f1) from int4_tbl group by f1 order by f1) q; +with q as (select max(f1) from int4_tbl group by f1 order by f1) + select q from q; + +-- +-- Test case for sublinks pulled up into joinaliasvars lists in an +-- inherited update/delete query +-- + +begin; -- this shouldn't delete anything, but be safe + +delete from road +where exists ( + select 1 + from + int4_tbl cross join + ( select f1, array(select q1 from int8_tbl) as arr + from text_tbl ) ss + where road.name = ss.f1 ); + +rollback; + +-- +-- Test case for sublinks pushed down into subselects via join alias expansion +-- + +select + (select sq1) as qq1 +from + (select exists(select 1 from int4_tbl where f1 = q2) as sq1, 42 as dummy + from int8_tbl) sq0 + join + int4_tbl i4 on dummy = i4.f1; + +-- +-- Test case for subselect within UPDATE of INSERT...ON CONFLICT DO UPDATE +-- +create temp table upsert(key int4 primary key, val text); +insert into upsert values(1, 'val') on conflict (key) do update set val = 'not seen'; +insert into upsert values(1, 'val') on conflict (key) do update set val = 'seen with subselect ' || (select f1 from int4_tbl where f1 != 0 limit 1)::text; + +select * from upsert; + +with aa as (select 'int4_tbl' u from int4_tbl limit 1) +insert into upsert values (1, 'x'), (999, 'y') +on conflict (key) do update set val = (select u from aa) +returning *; + +-- +-- Test case for cross-type partial matching in hashed subplan (bug #7597) +-- + +create temp table outer_7597 (f1 int4, f2 int4); +insert into outer_7597 values (0, 0); +insert into outer_7597 values (1, 0); +insert into outer_7597 values (0, null); +insert into outer_7597 values (1, null); + +create temp table inner_7597(c1 int8, c2 int8); +insert into inner_7597 values(0, null); + +select * from outer_7597 where (f1, f2) not in (select * from inner_7597); + +-- +-- Similar test case using text that verifies that collation +-- information is passed through by execTuplesEqual() in nodeSubplan.c +-- (otherwise it would error in texteq()) +-- + +create temp table outer_text (f1 text, f2 text); +insert into outer_text values ('a', 'a'); +insert into outer_text values ('b', 'a'); +insert into outer_text values ('a', null); +insert into outer_text values ('b', null); + +create temp table inner_text (c1 text, c2 text); +insert into inner_text values ('a', null); + +select * from outer_text where (f1, f2) not in (select * from inner_text); + +-- +-- Another test case for cross-type hashed subplans: comparison of +-- inner-side values must be done with appropriate operator +-- + +explain (verbose, costs off) +select 'foo'::text in (select 'bar'::name union all select 'bar'::name); + +select 'foo'::text in (select 'bar'::name union all select 'bar'::name); + +-- +-- Test case for premature memory release during hashing of subplan output +-- + +select '1'::text in (select '1'::name union all select '1'::name); + +-- +-- Test case for planner bug with nested EXISTS handling +-- +select a.thousand from tenk1 a, tenk1 b +where a.thousand = b.thousand + and exists ( select 1 from tenk1 c where b.hundred = c.hundred + and not exists ( select 1 from tenk1 d + where a.thousand = d.thousand ) ); + +-- +-- Check that nested sub-selects are not pulled up if they contain volatiles +-- +explain (verbose, costs off) + select x, x from + (select (select now()) as x from (values(1),(2)) v(y)) ss; +explain (verbose, costs off) + select x, x from + (select (select random()) as x from (values(1),(2)) v(y)) ss; +explain (verbose, costs off) + select x, x from + (select (select now() where y=y) as x from (values(1),(2)) v(y)) ss; +explain (verbose, costs off) + select x, x from + (select (select random() where y=y) as x from (values(1),(2)) v(y)) ss; + +-- +-- Test rescan of a hashed subplan (the use of random() is to prevent the +-- sub-select from being pulled up, which would result in not hashing) +-- +explain (verbose, costs off) +select sum(ss.tst::int) from + onek o cross join lateral ( + select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst, + random() as r + from onek i where i.unique1 = o.unique1 ) ss +where o.ten = 0; + +select sum(ss.tst::int) from + onek o cross join lateral ( + select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst, + random() as r + from onek i where i.unique1 = o.unique1 ) ss +where o.ten = 0; + +-- +-- Test rescan of a SetOp node +-- +explain (costs off) +select count(*) from + onek o cross join lateral ( + select * from onek i1 where i1.unique1 = o.unique1 + except + select * from onek i2 where i2.unique1 = o.unique2 + ) ss +where o.ten = 1; + +select count(*) from + onek o cross join lateral ( + select * from onek i1 where i1.unique1 = o.unique1 + except + select * from onek i2 where i2.unique1 = o.unique2 + ) ss +where o.ten = 1; + +-- +-- Test rescan of a RecursiveUnion node +-- +explain (costs off) +select sum(o.four), sum(ss.a) from + onek o cross join lateral ( + with recursive x(a) as + (select o.four as a + union + select a + 1 from x + where a < 10) + select * from x + ) ss +where o.ten = 1; + +select sum(o.four), sum(ss.a) from + onek o cross join lateral ( + with recursive x(a) as + (select o.four as a + union + select a + 1 from x + where a < 10) + select * from x + ) ss +where o.ten = 1; + +-- +-- Check we don't misoptimize a NOT IN where the subquery returns no rows. +-- +create temp table notinouter (a int); +create temp table notininner (b int not null); +insert into notinouter values (null), (1); + +select * from notinouter where a not in (select b from notininner); + +-- +-- Check we behave sanely in corner case of empty SELECT list (bug #8648) +-- +create temp table nocolumns(); +select exists(select * from nocolumns); + +-- +-- Check behavior with a SubPlan in VALUES (bug #14924) +-- +select val.x + from generate_series(1,10) as s(i), + lateral ( + values ((select s.i + 1)), (s.i + 101) + ) as val(x) +where s.i < 10 and (select val.x) < 110; + +-- another variant of that (bug #16213) +explain (verbose, costs off) +select * from +(values + (3 not in (select * from (values (1), (2)) ss1)), + (false) +) ss; + +select * from +(values + (3 not in (select * from (values (1), (2)) ss1)), + (false) +) ss; + +-- +-- Check sane behavior with nested IN SubLinks +-- +explain (verbose, costs off) +select * from int4_tbl where + (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in + (select ten from tenk1 b); +select * from int4_tbl where + (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in + (select ten from tenk1 b); + +-- +-- Check for incorrect optimization when IN subquery contains a SRF +-- +explain (verbose, costs off) +select * from int4_tbl o where (f1, f1) in + (select f1, generate_series(1,50) / 10 g from int4_tbl i group by f1); +select * from int4_tbl o where (f1, f1) in + (select f1, generate_series(1,50) / 10 g from int4_tbl i group by f1); + +-- +-- check for over-optimization of whole-row Var referencing an Append plan +-- +select (select q from + (select 1,2,3 where f1 > 0 + union all + select 4,5,6.0 where f1 <= 0 + ) q ) +from int4_tbl; + +-- +-- Check for sane handling of a lateral reference in a subquery's quals +-- (most of the complication here is to prevent the test case from being +-- flattened too much) +-- +explain (verbose, costs off) +select * from + int4_tbl i4, + lateral ( + select i4.f1 > 1 as b, 1 as id + from (select random() order by 1) as t1 + union all + select true as b, 2 as id + ) as t2 +where b and f1 >= 0; + +select * from + int4_tbl i4, + lateral ( + select i4.f1 > 1 as b, 1 as id + from (select random() order by 1) as t1 + union all + select true as b, 2 as id + ) as t2 +where b and f1 >= 0; + +-- +-- Check that volatile quals aren't pushed down past a DISTINCT: +-- nextval() should not be called more than the nominal number of times +-- +create temp sequence ts1; + +select * from + (select distinct ten from tenk1) ss + where ten < 10 + nextval('ts1') + order by 1; + +select nextval('ts1'); + +-- +-- Check that volatile quals aren't pushed down past a set-returning function; +-- while a nonvolatile qual can be, if it doesn't reference the SRF. +-- +create function tattle(x int, y int) returns bool +volatile language plpgsql as $$ +begin + raise notice 'x = %, y = %', x, y; + return x > y; +end$$; + +explain (verbose, costs off) +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, 8); + +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, 8); + +-- if we pretend it's stable, we get different results: +alter function tattle(x int, y int) stable; + +explain (verbose, costs off) +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, 8); + +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, 8); + +-- although even a stable qual should not be pushed down if it references SRF +explain (verbose, costs off) +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, u); + +select * from + (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss + where tattle(x, u); + +drop function tattle(x int, y int); + +-- +-- Test that LIMIT can be pushed to SORT through a subquery that just projects +-- columns. We check for that having happened by looking to see if EXPLAIN +-- ANALYZE shows that a top-N sort was used. We must suppress or filter away +-- all the non-invariant parts of the EXPLAIN ANALYZE output. +-- +create table sq_limit (pk int primary key, c1 int, c2 int); +insert into sq_limit values + (1, 1, 1), + (2, 2, 2), + (3, 3, 3), + (4, 4, 4), + (5, 1, 1), + (6, 2, 2), + (7, 3, 3), + (8, 4, 4); + +create function explain_sq_limit() returns setof text language plpgsql as +$$ +declare ln text; +begin + for ln in + explain (analyze, summary off, timing off, costs off) + select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3 + loop + ln := regexp_replace(ln, 'Memory: \S*', 'Memory: xxx'); + return next ln; + end loop; +end; +$$; + +select * from explain_sq_limit(); + +select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3; + +drop function explain_sq_limit(); + +drop table sq_limit; + +-- +-- Ensure that backward scan direction isn't propagated into +-- expression subqueries (bug #15336) +-- + +begin; + +declare c1 scroll cursor for + select * from generate_series(1,4) i + where i <> all (values (2),(3)); + +move forward all in c1; +fetch backward all in c1; + +commit; + +-- +-- Tests for CTE inlining behavior +-- + +-- Basic subquery that can be inlined +explain (verbose, costs off) +with x as (select * from (select f1 from subselect_tbl) ss) +select * from x where f1 = 1; + +-- Explicitly request materialization +explain (verbose, costs off) +with x as materialized (select * from (select f1 from subselect_tbl) ss) +select * from x where f1 = 1; + +-- Stable functions are safe to inline +explain (verbose, costs off) +with x as (select * from (select f1, now() from subselect_tbl) ss) +select * from x where f1 = 1; + +-- Volatile functions prevent inlining +explain (verbose, costs off) +with x as (select * from (select f1, random() from subselect_tbl) ss) +select * from x where f1 = 1; + +-- SELECT FOR UPDATE cannot be inlined +explain (verbose, costs off) +with x as (select * from (select f1 from subselect_tbl for update) ss) +select * from x where f1 = 1; + +-- Multiply-referenced CTEs are inlined only when requested +explain (verbose, costs off) +with x as (select * from (select f1, now() as n from subselect_tbl) ss) +select * from x, x x2 where x.n = x2.n; + +explain (verbose, costs off) +with x as not materialized (select * from (select f1, now() as n from subselect_tbl) ss) +select * from x, x x2 where x.n = x2.n; + +-- Multiply-referenced CTEs can't be inlined if they contain outer self-refs +explain (verbose, costs off) +with recursive x(a) as + ((values ('a'), ('b')) + union all + (with z as not materialized (select * from x) + select z.a || z1.a as a from z cross join z as z1 + where length(z.a || z1.a) < 5)) +select * from x; + +with recursive x(a) as + ((values ('a'), ('b')) + union all + (with z as not materialized (select * from x) + select z.a || z1.a as a from z cross join z as z1 + where length(z.a || z1.a) < 5)) +select * from x; + +explain (verbose, costs off) +with recursive x(a) as + ((values ('a'), ('b')) + union all + (with z as not materialized (select * from x) + select z.a || z.a as a from z + where length(z.a || z.a) < 5)) +select * from x; + +with recursive x(a) as + ((values ('a'), ('b')) + union all + (with z as not materialized (select * from x) + select z.a || z.a as a from z + where length(z.a || z.a) < 5)) +select * from x; + +-- Check handling of outer references +explain (verbose, costs off) +with x as (select * from int4_tbl) +select * from (with y as (select * from x) select * from y) ss; + +explain (verbose, costs off) +with x as materialized (select * from int4_tbl) +select * from (with y as (select * from x) select * from y) ss; + +-- Ensure that we inline the currect CTE when there are +-- multiple CTEs with the same name +explain (verbose, costs off) +with x as (select 1 as y) +select * from (with x as (select 2 as y) select * from x) ss; + +-- Row marks are not pushed into CTEs +explain (verbose, costs off) +with x as (select * from subselect_tbl) +select * from x for update; diff --git a/src/test/resources/postgresql-corpus/transactions.sql b/src/test/resources/postgresql-corpus/transactions.sql new file mode 100644 index 0000000..afd0039 --- /dev/null +++ b/src/test/resources/postgresql-corpus/transactions.sql @@ -0,0 +1,585 @@ +-- +-- TRANSACTIONS +-- + +BEGIN; + +SELECT * + INTO TABLE xacttest + FROM aggtest; + +INSERT INTO xacttest (a, b) VALUES (777, 777.777); + +END; + +-- should retrieve one value-- +SELECT a FROM xacttest WHERE a > 100; + + +BEGIN; + +CREATE TABLE disappear (a int4); + +DELETE FROM aggtest; + +-- should be empty +SELECT * FROM aggtest; + +ABORT; + +-- should not exist +SELECT oid FROM pg_class WHERE relname = 'disappear'; + +-- should have members again +SELECT * FROM aggtest; + + +-- Read-only tests + +CREATE TABLE writetest (a int); +CREATE TEMPORARY TABLE temptest (a int); + +BEGIN; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE; -- ok +SELECT * FROM writetest; -- ok +SET TRANSACTION READ WRITE; --fail +COMMIT; + +BEGIN; +SET TRANSACTION READ ONLY; -- ok +SET TRANSACTION READ WRITE; -- ok +SET TRANSACTION READ ONLY; -- ok +SELECT * FROM writetest; -- ok +SAVEPOINT x; +SET TRANSACTION READ ONLY; -- ok +SELECT * FROM writetest; -- ok +SET TRANSACTION READ ONLY; -- ok +SET TRANSACTION READ WRITE; --fail +COMMIT; + +BEGIN; +SET TRANSACTION READ WRITE; -- ok +SAVEPOINT x; +SET TRANSACTION READ WRITE; -- ok +SET TRANSACTION READ ONLY; -- ok +SELECT * FROM writetest; -- ok +SET TRANSACTION READ ONLY; -- ok +SET TRANSACTION READ WRITE; --fail +COMMIT; + +BEGIN; +SET TRANSACTION READ WRITE; -- ok +SAVEPOINT x; +SET TRANSACTION READ ONLY; -- ok +SELECT * FROM writetest; -- ok +ROLLBACK TO SAVEPOINT x; +SHOW transaction_read_only; -- off +SAVEPOINT y; +SET TRANSACTION READ ONLY; -- ok +SELECT * FROM writetest; -- ok +RELEASE SAVEPOINT y; +SHOW transaction_read_only; -- off +COMMIT; + +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY; + +DROP TABLE writetest; -- fail +INSERT INTO writetest VALUES (1); -- fail +SELECT * FROM writetest; -- ok +DELETE FROM temptest; -- ok +UPDATE temptest SET a = 0 FROM writetest WHERE temptest.a = 1 AND writetest.a = temptest.a; -- ok +PREPARE test AS UPDATE writetest SET a = 0; -- ok +EXECUTE test; -- fail +SELECT * FROM writetest, temptest; -- ok +CREATE TABLE test AS SELECT * FROM writetest; -- fail + +START TRANSACTION READ WRITE; +DROP TABLE writetest; -- ok +COMMIT; + +-- Subtransactions, basic tests +-- create & drop tables +SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE; +CREATE TABLE trans_foobar (a int); +BEGIN; + CREATE TABLE trans_foo (a int); + SAVEPOINT one; + DROP TABLE trans_foo; + CREATE TABLE trans_bar (a int); + ROLLBACK TO SAVEPOINT one; + RELEASE SAVEPOINT one; + SAVEPOINT two; + CREATE TABLE trans_baz (a int); + RELEASE SAVEPOINT two; + drop TABLE trans_foobar; + CREATE TABLE trans_barbaz (a int); +COMMIT; +-- should exist: trans_barbaz, trans_baz, trans_foo +SELECT * FROM trans_foo; -- should be empty +SELECT * FROM trans_bar; -- shouldn't exist +SELECT * FROM trans_barbaz; -- should be empty +SELECT * FROM trans_baz; -- should be empty + +-- inserts +BEGIN; + INSERT INTO trans_foo VALUES (1); + SAVEPOINT one; + INSERT into trans_bar VALUES (1); + ROLLBACK TO one; + RELEASE SAVEPOINT one; + SAVEPOINT two; + INSERT into trans_barbaz VALUES (1); + RELEASE two; + SAVEPOINT three; + SAVEPOINT four; + INSERT INTO trans_foo VALUES (2); + RELEASE SAVEPOINT four; + ROLLBACK TO SAVEPOINT three; + RELEASE SAVEPOINT three; + INSERT INTO trans_foo VALUES (3); +COMMIT; +SELECT * FROM trans_foo; -- should have 1 and 3 +SELECT * FROM trans_barbaz; -- should have 1 + +-- test whole-tree commit +BEGIN; + SAVEPOINT one; + SELECT trans_foo; + ROLLBACK TO SAVEPOINT one; + RELEASE SAVEPOINT one; + SAVEPOINT two; + CREATE TABLE savepoints (a int); + SAVEPOINT three; + INSERT INTO savepoints VALUES (1); + SAVEPOINT four; + INSERT INTO savepoints VALUES (2); + SAVEPOINT five; + INSERT INTO savepoints VALUES (3); + ROLLBACK TO SAVEPOINT five; +COMMIT; +COMMIT; -- should not be in a transaction block +SELECT * FROM savepoints; + +-- test whole-tree rollback +BEGIN; + SAVEPOINT one; + DELETE FROM savepoints WHERE a=1; + RELEASE SAVEPOINT one; + SAVEPOINT two; + DELETE FROM savepoints WHERE a=1; + SAVEPOINT three; + DELETE FROM savepoints WHERE a=2; +ROLLBACK; +COMMIT; -- should not be in a transaction block + +SELECT * FROM savepoints; + +-- test whole-tree commit on an aborted subtransaction +BEGIN; + INSERT INTO savepoints VALUES (4); + SAVEPOINT one; + INSERT INTO savepoints VALUES (5); + SELECT trans_foo; +COMMIT; +SELECT * FROM savepoints; + +BEGIN; + INSERT INTO savepoints VALUES (6); + SAVEPOINT one; + INSERT INTO savepoints VALUES (7); + RELEASE SAVEPOINT one; + INSERT INTO savepoints VALUES (8); +COMMIT; +-- rows 6 and 8 should have been created by the same xact +SELECT a.xmin = b.xmin FROM savepoints a, savepoints b WHERE a.a=6 AND b.a=8; +-- rows 6 and 7 should have been created by different xacts +SELECT a.xmin = b.xmin FROM savepoints a, savepoints b WHERE a.a=6 AND b.a=7; + +BEGIN; + INSERT INTO savepoints VALUES (9); + SAVEPOINT one; + INSERT INTO savepoints VALUES (10); + ROLLBACK TO SAVEPOINT one; + INSERT INTO savepoints VALUES (11); +COMMIT; +SELECT a FROM savepoints WHERE a in (9, 10, 11); +-- rows 9 and 11 should have been created by different xacts +SELECT a.xmin = b.xmin FROM savepoints a, savepoints b WHERE a.a=9 AND b.a=11; + +BEGIN; + INSERT INTO savepoints VALUES (12); + SAVEPOINT one; + INSERT INTO savepoints VALUES (13); + SAVEPOINT two; + INSERT INTO savepoints VALUES (14); + ROLLBACK TO SAVEPOINT one; + INSERT INTO savepoints VALUES (15); + SAVEPOINT two; + INSERT INTO savepoints VALUES (16); + SAVEPOINT three; + INSERT INTO savepoints VALUES (17); +COMMIT; +SELECT a FROM savepoints WHERE a BETWEEN 12 AND 17; + +BEGIN; + INSERT INTO savepoints VALUES (18); + SAVEPOINT one; + INSERT INTO savepoints VALUES (19); + SAVEPOINT two; + INSERT INTO savepoints VALUES (20); + ROLLBACK TO SAVEPOINT one; + INSERT INTO savepoints VALUES (21); + ROLLBACK TO SAVEPOINT one; + INSERT INTO savepoints VALUES (22); +COMMIT; +SELECT a FROM savepoints WHERE a BETWEEN 18 AND 22; + +DROP TABLE savepoints; + +-- only in a transaction block: +SAVEPOINT one; +ROLLBACK TO SAVEPOINT one; +RELEASE SAVEPOINT one; + +-- Only "rollback to" allowed in aborted state +BEGIN; + SAVEPOINT one; + SELECT 0/0; + SAVEPOINT two; -- ignored till the end of ... + RELEASE SAVEPOINT one; -- ignored till the end of ... + ROLLBACK TO SAVEPOINT one; + SELECT 1; +COMMIT; +SELECT 1; -- this should work + +-- check non-transactional behavior of cursors +BEGIN; + DECLARE c CURSOR FOR SELECT unique2 FROM tenk1 ORDER BY unique2; + SAVEPOINT one; + FETCH 10 FROM c; + ROLLBACK TO SAVEPOINT one; + FETCH 10 FROM c; + RELEASE SAVEPOINT one; + FETCH 10 FROM c; + CLOSE c; + DECLARE c CURSOR FOR SELECT unique2/0 FROM tenk1 ORDER BY unique2; + SAVEPOINT two; + FETCH 10 FROM c; + ROLLBACK TO SAVEPOINT two; + -- c is now dead to the world ... + FETCH 10 FROM c; + ROLLBACK TO SAVEPOINT two; + RELEASE SAVEPOINT two; + FETCH 10 FROM c; +COMMIT; + +-- +-- Check that "stable" functions are really stable. They should not be +-- able to see the partial results of the calling query. (Ideally we would +-- also check that they don't see commits of concurrent transactions, but +-- that's a mite hard to do within the limitations of pg_regress.) +-- +select * from xacttest; + +create or replace function max_xacttest() returns smallint language sql as +'select max(a) from xacttest' stable; + +begin; +update xacttest set a = max_xacttest() + 10 where a > 0; +select * from xacttest; +rollback; + +-- But a volatile function can see the partial results of the calling query +create or replace function max_xacttest() returns smallint language sql as +'select max(a) from xacttest' volatile; + +begin; +update xacttest set a = max_xacttest() + 10 where a > 0; +select * from xacttest; +rollback; + +-- Now the same test with plpgsql (since it depends on SPI which is different) +create or replace function max_xacttest() returns smallint language plpgsql as +'begin return max(a) from xacttest; end' stable; + +begin; +update xacttest set a = max_xacttest() + 10 where a > 0; +select * from xacttest; +rollback; + +create or replace function max_xacttest() returns smallint language plpgsql as +'begin return max(a) from xacttest; end' volatile; + +begin; +update xacttest set a = max_xacttest() + 10 where a > 0; +select * from xacttest; +rollback; + + +-- test case for problems with dropping an open relation during abort +BEGIN; + savepoint x; + CREATE TABLE koju (a INT UNIQUE); + INSERT INTO koju VALUES (1); + INSERT INTO koju VALUES (1); + rollback to x; + + CREATE TABLE koju (a INT UNIQUE); + INSERT INTO koju VALUES (1); + INSERT INTO koju VALUES (1); +ROLLBACK; + +DROP TABLE trans_foo; +DROP TABLE trans_baz; +DROP TABLE trans_barbaz; + + +-- test case for problems with revalidating an open relation during abort +create function inverse(int) returns float8 as +$$ +begin + analyze revalidate_bug; + return 1::float8/$1; +exception + when division_by_zero then return 0; +end$$ language plpgsql volatile; + +create table revalidate_bug (c float8 unique); +insert into revalidate_bug values (1); +insert into revalidate_bug values (inverse(0)); + +drop table revalidate_bug; +drop function inverse(int); + + +-- verify that cursors created during an aborted subtransaction are +-- closed, but that we do not rollback the effect of any FETCHs +-- performed in the aborted subtransaction +begin; + +savepoint x; +create table abc (a int); +insert into abc values (5); +insert into abc values (10); +declare foo cursor for select * from abc; +fetch from foo; +rollback to x; + +-- should fail +fetch from foo; +commit; + +begin; + +create table abc (a int); +insert into abc values (5); +insert into abc values (10); +insert into abc values (15); +declare foo cursor for select * from abc; + +fetch from foo; + +savepoint x; +fetch from foo; +rollback to x; + +fetch from foo; + +abort; + + +-- Test for proper cleanup after a failure in a cursor portal +-- that was created in an outer subtransaction +CREATE FUNCTION invert(x float8) RETURNS float8 LANGUAGE plpgsql AS +$$ begin return 1/x; end $$; + +CREATE FUNCTION create_temp_tab() RETURNS text +LANGUAGE plpgsql AS $$ +BEGIN + CREATE TEMP TABLE new_table (f1 float8); + -- case of interest is that we fail while holding an open + -- relcache reference to new_table + INSERT INTO new_table SELECT invert(0.0); + RETURN 'foo'; +END $$; + +BEGIN; +DECLARE ok CURSOR FOR SELECT * FROM int8_tbl; +DECLARE ctt CURSOR FOR SELECT create_temp_tab(); +FETCH ok; +SAVEPOINT s1; +FETCH ok; -- should work +FETCH ctt; -- error occurs here +ROLLBACK TO s1; +FETCH ok; -- should work +FETCH ctt; -- must be rejected +COMMIT; + +DROP FUNCTION create_temp_tab(); +DROP FUNCTION invert(x float8); + + +-- Tests for AND CHAIN + +CREATE TABLE abc (a int); + +-- set nondefault value so we have something to override below +SET default_transaction_read_only = on; + +START TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, DEFERRABLE; +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES (1); +INSERT INTO abc VALUES (2); +COMMIT AND CHAIN; -- TBLOCK_END +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES ('error'); +INSERT INTO abc VALUES (3); -- check it's really aborted +COMMIT AND CHAIN; -- TBLOCK_ABORT_END +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES (4); +COMMIT; + +START TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, DEFERRABLE; +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +SAVEPOINT x; +INSERT INTO abc VALUES ('error'); +COMMIT AND CHAIN; -- TBLOCK_ABORT_PENDING +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES (5); +COMMIT; + +-- different mix of options just for fun +START TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ WRITE, NOT DEFERRABLE; +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES (6); +ROLLBACK AND CHAIN; -- TBLOCK_ABORT_PENDING +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +INSERT INTO abc VALUES ('error'); +ROLLBACK AND CHAIN; -- TBLOCK_ABORT_END +SHOW transaction_isolation; +SHOW transaction_read_only; +SHOW transaction_deferrable; +ROLLBACK; + +-- not allowed outside a transaction block +COMMIT AND CHAIN; -- error +ROLLBACK AND CHAIN; -- error + +SELECT * FROM abc ORDER BY 1; + +RESET default_transaction_read_only; + +DROP TABLE abc; + + +-- Test assorted behaviors around the implicit transaction block created +-- when multiple SQL commands are sent in a single Query message. These +-- tests rely on the fact that psql will not break SQL commands apart at a +-- backslash-quoted semicolon, but will send them as one Query. + +create temp table i_table (f1 int); + +-- psql will show only the last result in a multi-statement Query +SELECT 1\; SELECT 2\; SELECT 3; + +select * from i_table; + +rollback; -- we are not in a transaction at this point + +-- can use regular begin/commit/rollback within a single Query +begin\; insert into i_table values(3)\; commit; +rollback; -- we are not in a transaction at this point +begin\; insert into i_table values(4)\; rollback; +rollback; -- we are not in a transaction at this point + +-- begin converts implicit transaction into a regular one that +-- can extend past the end of the Query +select 1\; begin\; insert into i_table values(5); +commit; +select 1\; begin\; insert into i_table values(6); +rollback; + +-- commit in implicit-transaction state commits but issues a warning. +insert into i_table values(7)\; commit\; insert into i_table values(8)\; select 1/0; +-- similarly, rollback aborts but issues a warning. +insert into i_table values(9)\; rollback\; select 2; + +select * from i_table; + +rollback; -- we are not in a transaction at this point + +-- implicit transaction block is still a transaction block, for e.g. VACUUM +SELECT 1\; VACUUM; +SELECT 1\; COMMIT\; VACUUM; + +-- we disallow savepoint-related commands in implicit-transaction state +SELECT 1\; SAVEPOINT sp; +SELECT 1\; COMMIT\; SAVEPOINT sp; +ROLLBACK TO SAVEPOINT sp\; SELECT 2; +SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3; + +-- but this is OK, because the BEGIN converts it to a regular xact +SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT; + + +-- Tests for AND CHAIN in implicit transaction blocks + +SET TRANSACTION READ ONLY\; COMMIT AND CHAIN; -- error +SHOW transaction_read_only; + +SET TRANSACTION READ ONLY\; ROLLBACK AND CHAIN; -- error +SHOW transaction_read_only; + +CREATE TABLE abc (a int); + +-- COMMIT/ROLLBACK + COMMIT/ROLLBACK AND CHAIN +INSERT INTO abc VALUES (7)\; COMMIT\; INSERT INTO abc VALUES (8)\; COMMIT AND CHAIN; -- 7 commit, 8 error +INSERT INTO abc VALUES (9)\; ROLLBACK\; INSERT INTO abc VALUES (10)\; ROLLBACK AND CHAIN; -- 9 rollback, 10 error + +-- COMMIT/ROLLBACK AND CHAIN + COMMIT/ROLLBACK +INSERT INTO abc VALUES (11)\; COMMIT AND CHAIN\; INSERT INTO abc VALUES (12)\; COMMIT; -- 11 error, 12 not reached +INSERT INTO abc VALUES (13)\; ROLLBACK AND CHAIN\; INSERT INTO abc VALUES (14)\; ROLLBACK; -- 13 error, 14 not reached + +-- START TRANSACTION + COMMIT/ROLLBACK AND CHAIN +START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (15)\; COMMIT AND CHAIN; -- 15 ok +SHOW transaction_isolation; -- transaction is active at this point +COMMIT; + +START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (16)\; ROLLBACK AND CHAIN; -- 16 ok +SHOW transaction_isolation; -- transaction is active at this point +ROLLBACK; + +-- START TRANSACTION + COMMIT/ROLLBACK + COMMIT/ROLLBACK AND CHAIN +START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (17)\; COMMIT\; INSERT INTO abc VALUES (18)\; COMMIT AND CHAIN; -- 17 commit, 18 error +SHOW transaction_isolation; -- out of transaction block + +START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (19)\; ROLLBACK\; INSERT INTO abc VALUES (20)\; ROLLBACK AND CHAIN; -- 19 rollback, 20 error +SHOW transaction_isolation; -- out of transaction block + +SELECT * FROM abc ORDER BY 1; + +DROP TABLE abc; + + +-- Test for successful cleanup of an aborted transaction at session exit. +-- THIS MUST BE THE LAST TEST IN THIS FILE. + +begin; +select 1/0; +rollback to X; + +-- DO NOT ADD ANYTHING HERE. diff --git a/src/test/resources/postgresql-corpus/union.sql b/src/test/resources/postgresql-corpus/union.sql new file mode 100644 index 0000000..5f4881d --- /dev/null +++ b/src/test/resources/postgresql-corpus/union.sql @@ -0,0 +1,442 @@ +-- +-- UNION (also INTERSECT, EXCEPT) +-- + +-- Simple UNION constructs + +SELECT 1 AS two UNION SELECT 2 ORDER BY 1; + +SELECT 1 AS one UNION SELECT 1 ORDER BY 1; + +SELECT 1 AS two UNION ALL SELECT 2; + +SELECT 1 AS two UNION ALL SELECT 1; + +SELECT 1 AS three UNION SELECT 2 UNION SELECT 3 ORDER BY 1; + +SELECT 1 AS two UNION SELECT 2 UNION SELECT 2 ORDER BY 1; + +SELECT 1 AS three UNION SELECT 2 UNION ALL SELECT 2 ORDER BY 1; + +SELECT 1.1 AS two UNION SELECT 2.2 ORDER BY 1; + +-- Mixed types + +SELECT 1.1 AS two UNION SELECT 2 ORDER BY 1; + +SELECT 1 AS two UNION SELECT 2.2 ORDER BY 1; + +SELECT 1 AS one UNION SELECT 1.0::float8 ORDER BY 1; + +SELECT 1.1 AS two UNION ALL SELECT 2 ORDER BY 1; + +SELECT 1.0::float8 AS two UNION ALL SELECT 1 ORDER BY 1; + +SELECT 1.1 AS three UNION SELECT 2 UNION SELECT 3 ORDER BY 1; + +SELECT 1.1::float8 AS two UNION SELECT 2 UNION SELECT 2.0::float8 ORDER BY 1; + +SELECT 1.1 AS three UNION SELECT 2 UNION ALL SELECT 2 ORDER BY 1; + +SELECT 1.1 AS two UNION (SELECT 2 UNION ALL SELECT 2) ORDER BY 1; + +-- +-- Try testing from tables... +-- + +SELECT f1 AS five FROM FLOAT8_TBL +UNION +SELECT f1 FROM FLOAT8_TBL +ORDER BY 1; + +SELECT f1 AS ten FROM FLOAT8_TBL +UNION ALL +SELECT f1 FROM FLOAT8_TBL; + +SELECT f1 AS nine FROM FLOAT8_TBL +UNION +SELECT f1 FROM INT4_TBL +ORDER BY 1; + +SELECT f1 AS ten FROM FLOAT8_TBL +UNION ALL +SELECT f1 FROM INT4_TBL; + +SELECT f1 AS five FROM FLOAT8_TBL + WHERE f1 BETWEEN -1e6 AND 1e6 +UNION +SELECT f1 FROM INT4_TBL + WHERE f1 BETWEEN 0 AND 1000000 +ORDER BY 1; + +SELECT CAST(f1 AS char(4)) AS three FROM VARCHAR_TBL +UNION +SELECT f1 FROM CHAR_TBL +ORDER BY 1; + +SELECT f1 AS three FROM VARCHAR_TBL +UNION +SELECT CAST(f1 AS varchar) FROM CHAR_TBL +ORDER BY 1; + +SELECT f1 AS eight FROM VARCHAR_TBL +UNION ALL +SELECT f1 FROM CHAR_TBL; + +SELECT f1 AS five FROM TEXT_TBL +UNION +SELECT f1 FROM VARCHAR_TBL +UNION +SELECT TRIM(TRAILING FROM f1) FROM CHAR_TBL +ORDER BY 1; + +-- +-- INTERSECT and EXCEPT +-- + +SELECT q2 FROM int8_tbl INTERSECT SELECT q1 FROM int8_tbl ORDER BY 1; + +SELECT q2 FROM int8_tbl INTERSECT ALL SELECT q1 FROM int8_tbl ORDER BY 1; + +SELECT q2 FROM int8_tbl EXCEPT SELECT q1 FROM int8_tbl ORDER BY 1; + +SELECT q2 FROM int8_tbl EXCEPT ALL SELECT q1 FROM int8_tbl ORDER BY 1; + +SELECT q2 FROM int8_tbl EXCEPT ALL SELECT DISTINCT q1 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl EXCEPT ALL SELECT q2 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl EXCEPT ALL SELECT DISTINCT q2 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl EXCEPT ALL SELECT q1 FROM int8_tbl FOR NO KEY UPDATE; + +-- nested cases +(SELECT 1,2,3 UNION SELECT 4,5,6) INTERSECT SELECT 4,5,6; +(SELECT 1,2,3 UNION SELECT 4,5,6 ORDER BY 1,2) INTERSECT SELECT 4,5,6; +(SELECT 1,2,3 UNION SELECT 4,5,6) EXCEPT SELECT 4,5,6; +(SELECT 1,2,3 UNION SELECT 4,5,6 ORDER BY 1,2) EXCEPT SELECT 4,5,6; + +-- exercise both hashed and sorted implementations of INTERSECT/EXCEPT + +set enable_hashagg to on; + +explain (costs off) +select count(*) from + ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss; +select count(*) from + ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss; + +explain (costs off) +select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; +select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; + +set enable_hashagg to off; + +explain (costs off) +select count(*) from + ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss; +select count(*) from + ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss; + +explain (costs off) +select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; +select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10; + +reset enable_hashagg; + +-- +-- Mixed types +-- + +SELECT f1 FROM float8_tbl INTERSECT SELECT f1 FROM int4_tbl ORDER BY 1; + +SELECT f1 FROM float8_tbl EXCEPT SELECT f1 FROM int4_tbl ORDER BY 1; + +-- +-- Operator precedence and (((((extra))))) parentheses +-- + +SELECT q1 FROM int8_tbl INTERSECT SELECT q2 FROM int8_tbl UNION ALL SELECT q2 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl INTERSECT (((SELECT q2 FROM int8_tbl UNION ALL SELECT q2 FROM int8_tbl))) ORDER BY 1; + +(((SELECT q1 FROM int8_tbl INTERSECT SELECT q2 FROM int8_tbl ORDER BY 1))) UNION ALL SELECT q2 FROM int8_tbl; + +SELECT q1 FROM int8_tbl UNION ALL SELECT q2 FROM int8_tbl EXCEPT SELECT q1 FROM int8_tbl ORDER BY 1; + +SELECT q1 FROM int8_tbl UNION ALL (((SELECT q2 FROM int8_tbl EXCEPT SELECT q1 FROM int8_tbl ORDER BY 1))); + +(((SELECT q1 FROM int8_tbl UNION ALL SELECT q2 FROM int8_tbl))) EXCEPT SELECT q1 FROM int8_tbl ORDER BY 1; + +-- +-- Subqueries with ORDER BY & LIMIT clauses +-- + +-- In this syntax, ORDER BY/LIMIT apply to the result of the EXCEPT +SELECT q1,q2 FROM int8_tbl EXCEPT SELECT q2,q1 FROM int8_tbl +ORDER BY q2,q1; + +-- This should fail, because q2 isn't a name of an EXCEPT output column +SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1; + +-- But this should work: +SELECT q1 FROM int8_tbl EXCEPT (((SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1))) ORDER BY 1; + +-- +-- New syntaxes (7.1) permit new tests +-- + +(((((select * from int8_tbl))))); + +-- +-- Check behavior with empty select list (allowed since 9.4) +-- + +select union select; +select intersect select; +select except select; + +-- check hashed implementation +set enable_hashagg = true; +set enable_sort = false; + +explain (costs off) +select from generate_series(1,5) union select from generate_series(1,3); +explain (costs off) +select from generate_series(1,5) intersect select from generate_series(1,3); + +select from generate_series(1,5) union select from generate_series(1,3); +select from generate_series(1,5) union all select from generate_series(1,3); +select from generate_series(1,5) intersect select from generate_series(1,3); +select from generate_series(1,5) intersect all select from generate_series(1,3); +select from generate_series(1,5) except select from generate_series(1,3); +select from generate_series(1,5) except all select from generate_series(1,3); + +-- check sorted implementation +set enable_hashagg = false; +set enable_sort = true; + +explain (costs off) +select from generate_series(1,5) union select from generate_series(1,3); +explain (costs off) +select from generate_series(1,5) intersect select from generate_series(1,3); + +select from generate_series(1,5) union select from generate_series(1,3); +select from generate_series(1,5) union all select from generate_series(1,3); +select from generate_series(1,5) intersect select from generate_series(1,3); +select from generate_series(1,5) intersect all select from generate_series(1,3); +select from generate_series(1,5) except select from generate_series(1,3); +select from generate_series(1,5) except all select from generate_series(1,3); + +reset enable_hashagg; +reset enable_sort; + +-- +-- Check handling of a case with unknown constants. We don't guarantee +-- an undecorated constant will work in all cases, but historically this +-- usage has worked, so test we don't break it. +-- + +SELECT a.f1 FROM (SELECT 'test' AS f1 FROM varchar_tbl) a +UNION +SELECT b.f1 FROM (SELECT f1 FROM varchar_tbl) b +ORDER BY 1; + +-- This should fail, but it should produce an error cursor +SELECT '3.4'::numeric UNION SELECT 'foo'; + +-- +-- Test that expression-index constraints can be pushed down through +-- UNION or UNION ALL +-- + +CREATE TEMP TABLE t1 (a text, b text); +CREATE INDEX t1_ab_idx on t1 ((a || b)); +CREATE TEMP TABLE t2 (ab text primary key); +INSERT INTO t1 VALUES ('a', 'b'), ('x', 'y'); +INSERT INTO t2 VALUES ('ab'), ('xy'); + +set enable_seqscan = off; +set enable_indexscan = on; +set enable_bitmapscan = off; + +explain (costs off) + SELECT * FROM + (SELECT a || b AS ab FROM t1 + UNION ALL + SELECT * FROM t2) t + WHERE ab = 'ab'; + +explain (costs off) + SELECT * FROM + (SELECT a || b AS ab FROM t1 + UNION + SELECT * FROM t2) t + WHERE ab = 'ab'; + +-- +-- Test that ORDER BY for UNION ALL can be pushed down to inheritance +-- children. +-- + +CREATE TEMP TABLE t1c (b text, a text); +ALTER TABLE t1c INHERIT t1; +CREATE TEMP TABLE t2c (primary key (ab)) INHERITS (t2); +INSERT INTO t1c VALUES ('v', 'w'), ('c', 'd'), ('m', 'n'), ('e', 'f'); +INSERT INTO t2c VALUES ('vw'), ('cd'), ('mn'), ('ef'); +CREATE INDEX t1c_ab_idx on t1c ((a || b)); + +set enable_seqscan = on; +set enable_indexonlyscan = off; + +explain (costs off) + SELECT * FROM + (SELECT a || b AS ab FROM t1 + UNION ALL + SELECT ab FROM t2) t + ORDER BY 1 LIMIT 8; + + SELECT * FROM + (SELECT a || b AS ab FROM t1 + UNION ALL + SELECT ab FROM t2) t + ORDER BY 1 LIMIT 8; + +reset enable_seqscan; +reset enable_indexscan; +reset enable_bitmapscan; + +-- This simpler variant of the above test has been observed to fail differently + +create table events (event_id int primary key); +create table other_events (event_id int primary key); +create table events_child () inherits (events); + +explain (costs off) +select event_id + from (select event_id from events + union all + select event_id from other_events) ss + order by event_id; + +drop table events_child, events, other_events; + +reset enable_indexonlyscan; + +-- Test constraint exclusion of UNION ALL subqueries +explain (costs off) + SELECT * FROM + (SELECT 1 AS t, * FROM tenk1 a + UNION ALL + SELECT 2 AS t, * FROM tenk1 b) c + WHERE t = 2; + +-- Test that we push quals into UNION sub-selects only when it's safe +explain (costs off) +SELECT * FROM + (SELECT 1 AS t, 2 AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x < 4 +ORDER BY x; + +SELECT * FROM + (SELECT 1 AS t, 2 AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x < 4 +ORDER BY x; + +explain (costs off) +SELECT * FROM + (SELECT 1 AS t, generate_series(1,10) AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x < 4 +ORDER BY x; + +SELECT * FROM + (SELECT 1 AS t, generate_series(1,10) AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x < 4 +ORDER BY x; + +explain (costs off) +SELECT * FROM + (SELECT 1 AS t, (random()*3)::int AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x > 3 +ORDER BY x; + +SELECT * FROM + (SELECT 1 AS t, (random()*3)::int AS x + UNION + SELECT 2 AS t, 4 AS x) ss +WHERE x > 3 +ORDER BY x; + +-- Test cases where the native ordering of a sub-select has more pathkeys +-- than the outer query cares about +explain (costs off) +select distinct q1 from + (select distinct * from int8_tbl i81 + union all + select distinct * from int8_tbl i82) ss +where q2 = q2; + +select distinct q1 from + (select distinct * from int8_tbl i81 + union all + select distinct * from int8_tbl i82) ss +where q2 = q2; + +explain (costs off) +select distinct q1 from + (select distinct * from int8_tbl i81 + union all + select distinct * from int8_tbl i82) ss +where -q1 = q2; + +select distinct q1 from + (select distinct * from int8_tbl i81 + union all + select distinct * from int8_tbl i82) ss +where -q1 = q2; + +-- Test proper handling of parameterized appendrel paths when the +-- potential join qual is expensive +create function expensivefunc(int) returns int +language plpgsql immutable strict cost 10000 +as $$begin return $1; end$$; + +create temp table t3 as select generate_series(-1000,1000) as x; +create index t3i on t3 (expensivefunc(x)); +analyze t3; + +explain (costs off) +select * from + (select * from t3 a union all select * from t3 b) ss + join int4_tbl on f1 = expensivefunc(x); +select * from + (select * from t3 a union all select * from t3 b) ss + join int4_tbl on f1 = expensivefunc(x); + +drop table t3; +drop function expensivefunc(int); + +-- Test handling of appendrel quals that const-simplify into an AND +explain (costs off) +select * from + (select *, 0 as x from int8_tbl a + union all + select *, 1 as x from int8_tbl b) ss +where (x = 0) or (q1 >= q2 and q1 <= q2); +select * from + (select *, 0 as x from int8_tbl a + union all + select *, 1 as x from int8_tbl b) ss +where (x = 0) or (q1 >= q2 and q1 <= q2); diff --git a/src/test/resources/postgresql-corpus/update.sql b/src/test/resources/postgresql-corpus/update.sql new file mode 100644 index 0000000..5953d4d --- /dev/null +++ b/src/test/resources/postgresql-corpus/update.sql @@ -0,0 +1,614 @@ +-- +-- UPDATE syntax tests +-- + +CREATE TABLE update_test ( + a INT DEFAULT 10, + b INT, + c TEXT +); + +CREATE TABLE upsert_test ( + a INT PRIMARY KEY, + b TEXT +); + +INSERT INTO update_test VALUES (5, 10, 'foo'); +INSERT INTO update_test(b, a) VALUES (15, 10); + +SELECT * FROM update_test; + +UPDATE update_test SET a = DEFAULT, b = DEFAULT; + +SELECT * FROM update_test; + +-- aliases for the UPDATE target table +UPDATE update_test AS t SET b = 10 WHERE t.a = 10; + +SELECT * FROM update_test; + +UPDATE update_test t SET b = t.b + 10 WHERE t.a = 10; + +SELECT * FROM update_test; + +-- +-- Test VALUES in FROM +-- + +UPDATE update_test SET a=v.i FROM (VALUES(100, 20)) AS v(i, j) + WHERE update_test.b = v.j; + +SELECT * FROM update_test; + +-- fail, wrong data type: +UPDATE update_test SET a = v.* FROM (VALUES(100, 20)) AS v(i, j) + WHERE update_test.b = v.j; + +-- +-- Test multiple-set-clause syntax +-- + +INSERT INTO update_test SELECT a,b+1,c FROM update_test; +SELECT * FROM update_test; + +UPDATE update_test SET (c,b,a) = ('bugle', b+11, DEFAULT) WHERE c = 'foo'; +SELECT * FROM update_test; +UPDATE update_test SET (c,b) = ('car', a+b), a = a + 1 WHERE a = 10; +SELECT * FROM update_test; +-- fail, multi assignment to same column: +UPDATE update_test SET (c,b) = ('car', a+b), b = a + 1 WHERE a = 10; + +-- uncorrelated sub-select: +UPDATE update_test + SET (b,a) = (select a,b from update_test where b = 41 and c = 'car') + WHERE a = 100 AND b = 20; +SELECT * FROM update_test; +-- correlated sub-select: +UPDATE update_test o + SET (b,a) = (select a+1,b from update_test i + where i.a=o.a and i.b=o.b and i.c is not distinct from o.c); +SELECT * FROM update_test; +-- fail, multiple rows supplied: +UPDATE update_test SET (b,a) = (select a+1,b from update_test); +-- set to null if no rows supplied: +UPDATE update_test SET (b,a) = (select a+1,b from update_test where a = 1000) + WHERE a = 11; +SELECT * FROM update_test; +-- *-expansion should work in this context: +UPDATE update_test SET (a,b) = ROW(v.*) FROM (VALUES(21, 100)) AS v(i, j) + WHERE update_test.a = v.i; +-- you might expect this to work, but syntactically it's not a RowExpr: +UPDATE update_test SET (a,b) = (v.*) FROM (VALUES(21, 101)) AS v(i, j) + WHERE update_test.a = v.i; + +-- if an alias for the target table is specified, don't allow references +-- to the original table name +UPDATE update_test AS t SET b = update_test.b + 10 WHERE t.a = 10; + +-- Make sure that we can update to a TOASTed value. +UPDATE update_test SET c = repeat('x', 10000) WHERE c = 'car'; +SELECT a, b, char_length(c) FROM update_test; + +-- Check multi-assignment with a Result node to handle a one-time filter. +EXPLAIN (VERBOSE, COSTS OFF) +UPDATE update_test t + SET (a, b) = (SELECT b, a FROM update_test s WHERE s.a = t.a) + WHERE CURRENT_USER = SESSION_USER; +UPDATE update_test t + SET (a, b) = (SELECT b, a FROM update_test s WHERE s.a = t.a) + WHERE CURRENT_USER = SESSION_USER; +SELECT a, b, char_length(c) FROM update_test; + +-- Test ON CONFLICT DO UPDATE +INSERT INTO upsert_test VALUES(1, 'Boo'); +-- uncorrelated sub-select: +WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test + VALUES (1, 'Bar') ON CONFLICT(a) + DO UPDATE SET (b, a) = (SELECT b, a FROM aaa) RETURNING *; +-- correlated sub-select: +INSERT INTO upsert_test VALUES (1, 'Baz') ON CONFLICT(a) + DO UPDATE SET (b, a) = (SELECT b || ', Correlated', a from upsert_test i WHERE i.a = upsert_test.a) + RETURNING *; +-- correlated sub-select (EXCLUDED.* alias): +INSERT INTO upsert_test VALUES (1, 'Bat') ON CONFLICT(a) + DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a) + RETURNING *; + +-- ON CONFLICT using system attributes in RETURNING, testing both the +-- inserting and updating paths. See bug report at: +-- https://www.postgresql.org/message-id/73436355-6432-49B1-92ED-1FE4F7E7E100%40finefun.com.au +INSERT INTO upsert_test VALUES (2, 'Beeble') ON CONFLICT(a) + DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a) + RETURNING tableoid::regclass, xmin = pg_current_xact_id()::xid AS xmin_correct, xmax = 0 AS xmax_correct; +-- currently xmax is set after a conflict - that's probably not good, +-- but it seems worthwhile to have to be explicit if that changes. +INSERT INTO upsert_test VALUES (2, 'Brox') ON CONFLICT(a) + DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a) + RETURNING tableoid::regclass, xmin = pg_current_xact_id()::xid AS xmin_correct, xmax = pg_current_xact_id()::xid AS xmax_correct; + + +DROP TABLE update_test; +DROP TABLE upsert_test; + + +--------------------------- +-- UPDATE with row movement +--------------------------- + +-- When a partitioned table receives an UPDATE to the partitioned key and the +-- new values no longer meet the partition's bound, the row must be moved to +-- the correct partition for the new partition key (if one exists). We must +-- also ensure that updatable views on partitioned tables properly enforce any +-- WITH CHECK OPTION that is defined. The situation with triggers in this case +-- also requires thorough testing as partition key updates causing row +-- movement convert UPDATEs into DELETE+INSERT. + +CREATE TABLE range_parted ( + a text, + b bigint, + c numeric, + d int, + e varchar +) PARTITION BY RANGE (a, b); + +-- Create partitions intentionally in descending bound order, so as to test +-- that update-row-movement works with the leaf partitions not in bound order. +CREATE TABLE part_b_20_b_30 (e varchar, c numeric, a text, b bigint, d int); +ALTER TABLE range_parted ATTACH PARTITION part_b_20_b_30 FOR VALUES FROM ('b', 20) TO ('b', 30); +CREATE TABLE part_b_10_b_20 (e varchar, c numeric, a text, b bigint, d int) PARTITION BY RANGE (c); +CREATE TABLE part_b_1_b_10 PARTITION OF range_parted FOR VALUES FROM ('b', 1) TO ('b', 10); +ALTER TABLE range_parted ATTACH PARTITION part_b_10_b_20 FOR VALUES FROM ('b', 10) TO ('b', 20); +CREATE TABLE part_a_10_a_20 PARTITION OF range_parted FOR VALUES FROM ('a', 10) TO ('a', 20); +CREATE TABLE part_a_1_a_10 PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('a', 10); + +-- Check that partition-key UPDATE works sanely on a partitioned table that +-- does not have any child partitions. +UPDATE part_b_10_b_20 set b = b - 6; + +-- Create some more partitions following the above pattern of descending bound +-- order, but let's make the situation a bit more complex by having the +-- attribute numbers of the columns vary from their parent partition. +CREATE TABLE part_c_100_200 (e varchar, c numeric, a text, b bigint, d int) PARTITION BY range (abs(d)); +ALTER TABLE part_c_100_200 DROP COLUMN e, DROP COLUMN c, DROP COLUMN a; +ALTER TABLE part_c_100_200 ADD COLUMN c numeric, ADD COLUMN e varchar, ADD COLUMN a text; +ALTER TABLE part_c_100_200 DROP COLUMN b; +ALTER TABLE part_c_100_200 ADD COLUMN b bigint; +CREATE TABLE part_d_1_15 PARTITION OF part_c_100_200 FOR VALUES FROM (1) TO (15); +CREATE TABLE part_d_15_20 PARTITION OF part_c_100_200 FOR VALUES FROM (15) TO (20); + +ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_100_200 FOR VALUES FROM (100) TO (200); + +CREATE TABLE part_c_1_100 (e varchar, d int, c numeric, b bigint, a text); +ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_1_100 FOR VALUES FROM (1) TO (100); + +\set init_range_parted 'truncate range_parted; insert into range_parted VALUES (''a'', 1, 1, 1), (''a'', 10, 200, 1), (''b'', 12, 96, 1), (''b'', 13, 97, 2), (''b'', 15, 105, 16), (''b'', 17, 105, 19)' +\set show_data 'select tableoid::regclass::text COLLATE "C" partname, * from range_parted ORDER BY 1, 2, 3, 4, 5, 6' +----:init_range_parted; +----:show_data; + +-- The order of subplans should be in bound order +EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97; + +-- fail, row movement happens only within the partition subtree. +UPDATE part_c_100_200 set c = c - 20, d = c WHERE c = 105; +-- fail, no partition key update, so no attempt to move tuple, +-- but "a = 'a'" violates partition constraint enforced by root partition) +UPDATE part_b_10_b_20 set a = 'a'; +-- ok, partition key update, no constraint violation +UPDATE range_parted set d = d - 10 WHERE d > 10; +-- ok, no partition key update, no constraint violation +UPDATE range_parted set e = d; +-- No row found +UPDATE part_c_1_100 set c = c + 20 WHERE c = 98; +-- ok, row movement +UPDATE part_b_10_b_20 set c = c + 20 returning c, b, a; +--:show_data; + +-- fail, row movement happens only within the partition subtree. +UPDATE part_b_10_b_20 set b = b - 6 WHERE c > 116 returning *; +-- ok, row movement, with subset of rows moved into different partition. +UPDATE range_parted set b = b - 6 WHERE c > 116 returning a, b + c; + +----:show_data; + +-- Common table needed for multiple test scenarios. +CREATE TABLE mintab(c1 int); +INSERT into mintab VALUES (120); + +-- update partition key using updatable view. +CREATE VIEW upview AS SELECT * FROM range_parted WHERE (select c > c1 FROM mintab) WITH CHECK OPTION; +-- ok +UPDATE upview set c = 199 WHERE b = 4; +-- fail, check option violation +UPDATE upview set c = 120 WHERE b = 4; +-- fail, row movement with check option violation +UPDATE upview set a = 'b', b = 15, c = 120 WHERE b = 4; +-- ok, row movement, check option passes +UPDATE upview set a = 'b', b = 15 WHERE b = 4; + +--:show_data; + +-- cleanup +DROP VIEW upview; + +-- RETURNING having whole-row vars. +--:init_range_parted; +UPDATE range_parted set c = 95 WHERE a = 'b' and b > 10 and c > 100 returning (range_parted), *; +--:show_data; + + +-- Transition tables with update row movement +----:init_range_parted; + +CREATE FUNCTION trans_updatetrigfunc() RETURNS trigger LANGUAGE plpgsql AS +$$ + begin + raise notice 'trigger = %, old table = %, new table = %', + TG_NAME, + (select string_agg(old_table::text, ', ' ORDER BY a) FROM old_table), + (select string_agg(new_table::text, ', ' ORDER BY a) FROM new_table); + return null; + end; +$$; + +CREATE TRIGGER trans_updatetrig + AFTER UPDATE ON range_parted REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trans_updatetrigfunc(); + +UPDATE range_parted set c = (case when c = 96 then 110 else c + 1 end ) WHERE a = 'b' and b > 10 and c >= 96; +--:show_data; +----:init_range_parted; + +-- Enabling OLD TABLE capture for both DELETE as well as UPDATE stmt triggers +-- should not cause DELETEd rows to be captured twice. Similar thing for +-- INSERT triggers and inserted rows. +CREATE TRIGGER trans_deletetrig + AFTER DELETE ON range_parted REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE trans_updatetrigfunc(); +CREATE TRIGGER trans_inserttrig + AFTER INSERT ON range_parted REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trans_updatetrigfunc(); +UPDATE range_parted set c = c + 50 WHERE a = 'b' and b > 10 and c >= 96; +--:show_data; +DROP TRIGGER trans_deletetrig ON range_parted; +DROP TRIGGER trans_inserttrig ON range_parted; +-- Don't drop trans_updatetrig yet. It is required below. + +-- Test with transition tuple conversion happening for rows moved into the +-- new partition. This requires a trigger that references transition table +-- (we already have trans_updatetrig). For inserted rows, the conversion +-- is not usually needed, because the original tuple is already compatible with +-- the desired transition tuple format. But conversion happens when there is a +-- BR trigger because the trigger can change the inserted row. So install a +-- BR triggers on those child partitions where the rows will be moved. +CREATE FUNCTION func_parted_mod_b() RETURNS trigger AS $$ +BEGIN + NEW.b = NEW.b + 1; + return NEW; +END $$ language plpgsql; +CREATE TRIGGER trig_c1_100 BEFORE UPDATE OR INSERT ON part_c_1_100 + FOR EACH ROW EXECUTE PROCEDURE func_parted_mod_b(); +CREATE TRIGGER trig_d1_15 BEFORE UPDATE OR INSERT ON part_d_1_15 + FOR EACH ROW EXECUTE PROCEDURE func_parted_mod_b(); +CREATE TRIGGER trig_d15_20 BEFORE UPDATE OR INSERT ON part_d_15_20 + FOR EACH ROW EXECUTE PROCEDURE func_parted_mod_b(); +----:init_range_parted; +UPDATE range_parted set c = (case when c = 96 then 110 else c + 1 end) WHERE a = 'b' and b > 10 and c >= 96; +--:show_data; +----:init_range_parted; +UPDATE range_parted set c = c + 50 WHERE a = 'b' and b > 10 and c >= 96; +--:show_data; + +-- Case where per-partition tuple conversion map array is allocated, but the +-- map is not required for the particular tuple that is routed, thanks to +-- matching table attributes of the partition and the target table. +----:init_range_parted; +UPDATE range_parted set b = 15 WHERE b = 1; +--:show_data; + +DROP TRIGGER trans_updatetrig ON range_parted; +DROP TRIGGER trig_c1_100 ON part_c_1_100; +DROP TRIGGER trig_d1_15 ON part_d_1_15; +DROP TRIGGER trig_d15_20 ON part_d_15_20; +DROP FUNCTION func_parted_mod_b(); + +-- RLS policies with update-row-movement +----------------------------------------- + +ALTER TABLE range_parted ENABLE ROW LEVEL SECURITY; +CREATE USER regress_range_parted_user; +GRANT ALL ON range_parted, mintab TO regress_range_parted_user; +CREATE POLICY seeall ON range_parted AS PERMISSIVE FOR SELECT USING (true); +CREATE POLICY policy_range_parted ON range_parted for UPDATE USING (true) WITH CHECK (c % 2 = 0); + +----:init_range_parted; +SET SESSION AUTHORIZATION regress_range_parted_user; +-- This should fail with RLS violation error while moving row from +-- part_a_10_a_20 to part_d_1_15, because we are setting 'c' to an odd number. +UPDATE range_parted set a = 'b', c = 151 WHERE a = 'a' and c = 200; + +RESET SESSION AUTHORIZATION; +-- Create a trigger on part_d_1_15 +CREATE FUNCTION func_d_1_15() RETURNS trigger AS $$ +BEGIN + NEW.c = NEW.c + 1; -- Make even numbers odd, or vice versa + return NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER trig_d_1_15 BEFORE INSERT ON part_d_1_15 + FOR EACH ROW EXECUTE PROCEDURE func_d_1_15(); + +----:init_range_parted; +SET SESSION AUTHORIZATION regress_range_parted_user; + +-- Here, RLS checks should succeed while moving row from part_a_10_a_20 to +-- part_d_1_15. Even though the UPDATE is setting 'c' to an odd number, the +-- trigger at the destination partition again makes it an even number. +UPDATE range_parted set a = 'b', c = 151 WHERE a = 'a' and c = 200; + +RESET SESSION AUTHORIZATION; +----:init_range_parted; +SET SESSION AUTHORIZATION regress_range_parted_user; +-- This should fail with RLS violation error. Even though the UPDATE is setting +-- 'c' to an even number, the trigger at the destination partition again makes +-- it an odd number. +UPDATE range_parted set a = 'b', c = 150 WHERE a = 'a' and c = 200; + +-- Cleanup +RESET SESSION AUTHORIZATION; +DROP TRIGGER trig_d_1_15 ON part_d_1_15; +DROP FUNCTION func_d_1_15(); + +-- Policy expression contains SubPlan +RESET SESSION AUTHORIZATION; +----:init_range_parted; +CREATE POLICY policy_range_parted_subplan on range_parted + AS RESTRICTIVE for UPDATE USING (true) + WITH CHECK ((SELECT range_parted.c <= c1 FROM mintab)); +SET SESSION AUTHORIZATION regress_range_parted_user; +-- fail, mintab has row with c1 = 120 +UPDATE range_parted set a = 'b', c = 122 WHERE a = 'a' and c = 200; +-- ok +UPDATE range_parted set a = 'b', c = 120 WHERE a = 'a' and c = 200; + +-- RLS policy expression contains whole row. + +RESET SESSION AUTHORIZATION; +----:init_range_parted; +CREATE POLICY policy_range_parted_wholerow on range_parted AS RESTRICTIVE for UPDATE USING (true) + WITH CHECK (range_parted = row('b', 10, 112, 1, NULL)::range_parted); +SET SESSION AUTHORIZATION regress_range_parted_user; +-- ok, should pass the RLS check +UPDATE range_parted set a = 'b', c = 112 WHERE a = 'a' and c = 200; +RESET SESSION AUTHORIZATION; +----:init_range_parted; +SET SESSION AUTHORIZATION regress_range_parted_user; +-- fail, the whole row RLS check should fail +UPDATE range_parted set a = 'b', c = 116 WHERE a = 'a' and c = 200; + +-- Cleanup +RESET SESSION AUTHORIZATION; +DROP POLICY policy_range_parted ON range_parted; +DROP POLICY policy_range_parted_subplan ON range_parted; +DROP POLICY policy_range_parted_wholerow ON range_parted; +REVOKE ALL ON range_parted, mintab FROM regress_range_parted_user; +DROP USER regress_range_parted_user; +DROP TABLE mintab; + + +-- statement triggers with update row movement +--------------------------------------------------- + +----:init_range_parted; + +CREATE FUNCTION trigfunc() returns trigger language plpgsql as +$$ + begin + raise notice 'trigger = % fired on table % during %', + TG_NAME, TG_TABLE_NAME, TG_OP; + return null; + end; +$$; +-- Triggers on root partition +CREATE TRIGGER parent_delete_trig + AFTER DELETE ON range_parted for each statement execute procedure trigfunc(); +CREATE TRIGGER parent_update_trig + AFTER UPDATE ON range_parted for each statement execute procedure trigfunc(); +CREATE TRIGGER parent_insert_trig + AFTER INSERT ON range_parted for each statement execute procedure trigfunc(); + +-- Triggers on leaf partition part_c_1_100 +CREATE TRIGGER c1_delete_trig + AFTER DELETE ON part_c_1_100 for each statement execute procedure trigfunc(); +CREATE TRIGGER c1_update_trig + AFTER UPDATE ON part_c_1_100 for each statement execute procedure trigfunc(); +CREATE TRIGGER c1_insert_trig + AFTER INSERT ON part_c_1_100 for each statement execute procedure trigfunc(); + +-- Triggers on leaf partition part_d_1_15 +CREATE TRIGGER d1_delete_trig + AFTER DELETE ON part_d_1_15 for each statement execute procedure trigfunc(); +CREATE TRIGGER d1_update_trig + AFTER UPDATE ON part_d_1_15 for each statement execute procedure trigfunc(); +CREATE TRIGGER d1_insert_trig + AFTER INSERT ON part_d_1_15 for each statement execute procedure trigfunc(); +-- Triggers on leaf partition part_d_15_20 +CREATE TRIGGER d15_delete_trig + AFTER DELETE ON part_d_15_20 for each statement execute procedure trigfunc(); +CREATE TRIGGER d15_update_trig + AFTER UPDATE ON part_d_15_20 for each statement execute procedure trigfunc(); +CREATE TRIGGER d15_insert_trig + AFTER INSERT ON part_d_15_20 for each statement execute procedure trigfunc(); + +-- Move all rows from part_c_100_200 to part_c_1_100. None of the delete or +-- insert statement triggers should be fired. +UPDATE range_parted set c = c - 50 WHERE c > 97; +--:show_data; + +DROP TRIGGER parent_delete_trig ON range_parted; +DROP TRIGGER parent_update_trig ON range_parted; +DROP TRIGGER parent_insert_trig ON range_parted; +DROP TRIGGER c1_delete_trig ON part_c_1_100; +DROP TRIGGER c1_update_trig ON part_c_1_100; +DROP TRIGGER c1_insert_trig ON part_c_1_100; +DROP TRIGGER d1_delete_trig ON part_d_1_15; +DROP TRIGGER d1_update_trig ON part_d_1_15; +DROP TRIGGER d1_insert_trig ON part_d_1_15; +DROP TRIGGER d15_delete_trig ON part_d_15_20; +DROP TRIGGER d15_update_trig ON part_d_15_20; +DROP TRIGGER d15_insert_trig ON part_d_15_20; + + +-- Creating default partition for range +----:init_range_parted; +create table part_def partition of range_parted default; +\d+ part_def +insert into range_parted values ('c', 9); +-- ok +update part_def set a = 'd' where a = 'c'; +-- fail +update part_def set a = 'a' where a = 'd'; + +--:show_data; + +-- Update row movement from non-default to default partition. +-- fail, default partition is not under part_a_10_a_20; +UPDATE part_a_10_a_20 set a = 'ad' WHERE a = 'a'; +-- ok +UPDATE range_parted set a = 'ad' WHERE a = 'a'; +UPDATE range_parted set a = 'bd' WHERE a = 'b'; +--:show_data; +-- Update row movement from default to non-default partitions. +-- ok +UPDATE range_parted set a = 'a' WHERE a = 'ad'; +UPDATE range_parted set a = 'b' WHERE a = 'bd'; +--:show_data; + +-- Cleanup: range_parted no longer needed. +DROP TABLE range_parted; + +CREATE TABLE list_parted ( + a text, + b int +) PARTITION BY list (a); +CREATE TABLE list_part1 PARTITION OF list_parted for VALUES in ('a', 'b'); +CREATE TABLE list_default PARTITION OF list_parted default; +INSERT into list_part1 VALUES ('a', 1); +INSERT into list_default VALUES ('d', 10); + +-- fail +UPDATE list_default set a = 'a' WHERE a = 'd'; +-- ok +UPDATE list_default set a = 'x' WHERE a = 'd'; + +DROP TABLE list_parted; + +-------------- +-- Some more update-partition-key test scenarios below. This time use list +-- partitions. +-------------- + +-- Setup for list partitions +CREATE TABLE list_parted (a numeric, b int, c int8) PARTITION BY list (a); +CREATE TABLE sub_parted PARTITION OF list_parted for VALUES in (1) PARTITION BY list (b); + +CREATE TABLE sub_part1(b int, c int8, a numeric); +ALTER TABLE sub_parted ATTACH PARTITION sub_part1 for VALUES in (1); +CREATE TABLE sub_part2(b int, c int8, a numeric); +ALTER TABLE sub_parted ATTACH PARTITION sub_part2 for VALUES in (2); + +CREATE TABLE list_part1(a numeric, b int, c int8); +ALTER TABLE list_parted ATTACH PARTITION list_part1 for VALUES in (2,3); + +INSERT into list_parted VALUES (2,5,50); +INSERT into list_parted VALUES (3,6,60); +INSERT into sub_parted VALUES (1,1,60); +INSERT into sub_parted VALUES (1,2,10); + +-- Test partition constraint violation when intermediate ancestor is used and +-- constraint is inherited from upper root. +UPDATE sub_parted set a = 2 WHERE c = 10; + +-- Test update-partition-key, where the unpruned partitions do not have their +-- partition keys updated. +SELECT tableoid::regclass::text, * FROM list_parted WHERE a = 2 ORDER BY 1; +UPDATE list_parted set b = c + a WHERE a = 2; +SELECT tableoid::regclass::text, * FROM list_parted WHERE a = 2 ORDER BY 1; + + +-- Test the case where BR UPDATE triggers change the partition key. +CREATE FUNCTION func_parted_mod_b() returns trigger as $$ +BEGIN + NEW.b = 2; -- This is changing partition key column. + return NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER parted_mod_b before update on sub_part1 + for each row execute procedure func_parted_mod_b(); + +SELECT tableoid::regclass::text, * FROM list_parted ORDER BY 1, 2, 3, 4; + +-- This should do the tuple routing even though there is no explicit +-- partition-key update, because there is a trigger on sub_part1. +UPDATE list_parted set c = 70 WHERE b = 1; +SELECT tableoid::regclass::text, * FROM list_parted ORDER BY 1, 2, 3, 4; + +DROP TRIGGER parted_mod_b ON sub_part1; + +-- If BR DELETE trigger prevented DELETE from happening, we should also skip +-- the INSERT if that delete is part of UPDATE=>DELETE+INSERT. +CREATE OR REPLACE FUNCTION func_parted_mod_b() returns trigger as $$ +BEGIN + raise notice 'Trigger: Got OLD row %, but returning NULL', OLD; + return NULL; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER trig_skip_delete before delete on sub_part2 + for each row execute procedure func_parted_mod_b(); +UPDATE list_parted set b = 1 WHERE c = 70; +SELECT tableoid::regclass::text, * FROM list_parted ORDER BY 1, 2, 3, 4; +-- Drop the trigger. Now the row should be moved. +DROP TRIGGER trig_skip_delete ON sub_part2; +UPDATE list_parted set b = 1 WHERE c = 70; +SELECT tableoid::regclass::text, * FROM list_parted ORDER BY 1, 2, 3, 4; +DROP FUNCTION func_parted_mod_b(); + +-- UPDATE partition-key with FROM clause. If join produces multiple output +-- rows for the same row to be modified, we should tuple-route the row only +-- once. There should not be any rows inserted. +CREATE TABLE non_parted (id int); +INSERT into non_parted VALUES (1), (1), (1), (2), (2), (2), (3), (3), (3); +UPDATE list_parted t1 set a = 2 FROM non_parted t2 WHERE t1.a = t2.id and a = 1; +SELECT tableoid::regclass::text, * FROM list_parted ORDER BY 1, 2, 3, 4; +DROP TABLE non_parted; + +-- Cleanup: list_parted no longer needed. +DROP TABLE list_parted; + +-- create custom operator class and hash function, for the same reason +-- explained in alter_table.sql +create or replace function dummy_hashint4(a int4, seed int8) returns int8 as +$$ begin return (a + seed); end; $$ language 'plpgsql' immutable; +create operator class custom_opclass for type int4 using hash as +operator 1 = , function 2 dummy_hashint4(int4, int8); + +create table hash_parted ( + a int, + b int +) partition by hash (a custom_opclass, b custom_opclass); +create table hpart1 partition of hash_parted for values with (modulus 2, remainder 1); +create table hpart2 partition of hash_parted for values with (modulus 4, remainder 2); +create table hpart3 partition of hash_parted for values with (modulus 8, remainder 0); +create table hpart4 partition of hash_parted for values with (modulus 8, remainder 4); +insert into hpart1 values (1, 1); +insert into hpart2 values (2, 5); +insert into hpart4 values (3, 4); + +-- fail +update hpart1 set a = 3, b=4 where a = 1; +-- ok, row movement +update hash_parted set b = b - 1 where b = 1; +-- ok +update hash_parted set b = b + 8 where b = 1; + +-- cleanup +drop table hash_parted; +drop operator class custom_opclass using hash; +drop function dummy_hashint4(a int4, seed int8); diff --git a/src/test/resources/postgresql-corpus/window.sql b/src/test/resources/postgresql-corpus/window.sql new file mode 100644 index 0000000..e0ab365 --- /dev/null +++ b/src/test/resources/postgresql-corpus/window.sql @@ -0,0 +1,1306 @@ +-- +-- WINDOW FUNCTIONS +-- + +CREATE TEMPORARY TABLE empsalary ( + depname varchar, + empno bigint, + salary int, + enroll_date date +); + +INSERT INTO empsalary VALUES +('develop', 10, 5200, '2007-08-01'), +('sales', 1, 5000, '2006-10-01'), +('personnel', 5, 3500, '2007-12-10'), +('sales', 4, 4800, '2007-08-08'), +('personnel', 2, 3900, '2006-12-23'), +('develop', 7, 4200, '2008-01-01'), +('develop', 9, 4500, '2008-01-01'), +('sales', 3, 4800, '2007-08-01'), +('develop', 8, 6000, '2006-10-01'), +('develop', 11, 5200, '2007-08-15'); + +SELECT depname, empno, salary, sum(salary) OVER (PARTITION BY depname) FROM empsalary ORDER BY depname, salary; + +SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary) FROM empsalary; + +-- with GROUP BY +SELECT four, ten, SUM(SUM(four)) OVER (PARTITION BY four), AVG(ten) FROM tenk1 +GROUP BY four, ten ORDER BY four, ten; + +SELECT depname, empno, salary, sum(salary) OVER w FROM empsalary WINDOW w AS (PARTITION BY depname); + +SELECT depname, empno, salary, rank() OVER w FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary) ORDER BY rank() OVER w; + +-- empty window specification +SELECT COUNT(*) OVER () FROM tenk1 WHERE unique2 < 10; + +SELECT COUNT(*) OVER w FROM tenk1 WHERE unique2 < 10 WINDOW w AS (); + +-- no window operation +SELECT four FROM tenk1 WHERE FALSE WINDOW w AS (PARTITION BY ten); + +-- cumulative aggregate +SELECT sum(four) OVER (PARTITION BY ten ORDER BY unique2) AS sum_1, ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT row_number() OVER (ORDER BY unique2) FROM tenk1 WHERE unique2 < 10; + +SELECT rank() OVER (PARTITION BY four ORDER BY ten) AS rank_1, ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT dense_rank() OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT percent_rank() OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT cume_dist() OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT ntile(3) OVER (ORDER BY ten, four), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT ntile(NULL) OVER (ORDER BY ten, four), ten, four FROM tenk1 LIMIT 2; + +SELECT lag(ten) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT lag(ten, four) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT lag(ten, four, 0) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT lead(ten) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT lead(ten * 2, 1) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT lead(ten * 2, 1, -1) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT first_value(ten) OVER (PARTITION BY four ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +-- last_value returns the last row of the frame, which is CURRENT ROW in ORDER BY window. +SELECT last_value(four) OVER (ORDER BY ten), ten, four FROM tenk1 WHERE unique2 < 10; + +SELECT last_value(ten) OVER (PARTITION BY four), ten, four FROM + (SELECT * FROM tenk1 WHERE unique2 < 10 ORDER BY four, ten)s + ORDER BY four, ten; + +SELECT nth_value(ten, four + 1) OVER (PARTITION BY four), ten, four + FROM (SELECT * FROM tenk1 WHERE unique2 < 10 ORDER BY four, ten)s; + +SELECT ten, two, sum(hundred) AS gsum, sum(sum(hundred)) OVER (PARTITION BY two ORDER BY ten) AS wsum +FROM tenk1 GROUP BY ten, two; + +SELECT count(*) OVER (PARTITION BY four), four FROM (SELECT * FROM tenk1 WHERE two = 1)s WHERE unique2 < 10; + +SELECT (count(*) OVER (PARTITION BY four ORDER BY ten) + + sum(hundred) OVER (PARTITION BY four ORDER BY ten))::varchar AS cntsum + FROM tenk1 WHERE unique2 < 10; + +-- opexpr with different windows evaluation. +SELECT * FROM( + SELECT count(*) OVER (PARTITION BY four ORDER BY ten) + + sum(hundred) OVER (PARTITION BY two ORDER BY ten) AS total, + count(*) OVER (PARTITION BY four ORDER BY ten) AS fourcount, + sum(hundred) OVER (PARTITION BY two ORDER BY ten) AS twosum + FROM tenk1 +)sub +WHERE total <> fourcount + twosum; + +SELECT avg(four) OVER (PARTITION BY four ORDER BY thousand / 100) FROM tenk1 WHERE unique2 < 10; + +SELECT ten, two, sum(hundred) AS gsum, sum(sum(hundred)) OVER win AS wsum +FROM tenk1 GROUP BY ten, two WINDOW win AS (PARTITION BY two ORDER BY ten); + +-- more than one window with GROUP BY +SELECT sum(salary), + row_number() OVER (ORDER BY depname), + sum(sum(salary)) OVER (ORDER BY depname DESC) +FROM empsalary GROUP BY depname; + +-- identical windows with different names +SELECT sum(salary) OVER w1, count(*) OVER w2 +FROM empsalary WINDOW w1 AS (ORDER BY salary), w2 AS (ORDER BY salary); + +-- subplan +SELECT lead(ten, (SELECT two FROM tenk1 WHERE s.unique2 = unique2)) OVER (PARTITION BY four ORDER BY ten) +FROM tenk1 s WHERE unique2 < 10; + +-- empty table +SELECT count(*) OVER (PARTITION BY four) FROM (SELECT * FROM tenk1 WHERE FALSE)s; + +-- mixture of agg/wfunc in the same window +SELECT sum(salary) OVER w, rank() OVER w FROM empsalary WINDOW w AS (PARTITION BY depname ORDER BY salary DESC); + +-- strict aggs +SELECT empno, depname, salary, bonus, depadj, MIN(bonus) OVER (ORDER BY empno), MAX(depadj) OVER () FROM( + SELECT *, + CASE WHEN enroll_date < '2008-01-01' THEN 2008 - extract(YEAR FROM enroll_date) END * 500 AS bonus, + CASE WHEN + AVG(salary) OVER (PARTITION BY depname) < salary + THEN 200 END AS depadj FROM empsalary +)s; + +-- window function over ungrouped agg over empty row set (bug before 9.1) +SELECT SUM(COUNT(f1)) OVER () FROM int4_tbl WHERE f1=42; + +-- window function with ORDER BY an expression involving aggregates (9.1 bug) +select ten, + sum(unique1) + sum(unique2) as res, + rank() over (order by sum(unique1) + sum(unique2)) as rank +from tenk1 +group by ten order by ten; + +-- window and aggregate with GROUP BY expression (9.2 bug) +explain (costs off) +select first_value(max(x)) over (), y + from (select unique1 as x, ten+four as y from tenk1) ss + group by y; + +-- test non-default frame specifications +SELECT four, ten, + sum(ten) over (partition by four order by ten), + last_value(ten) over (partition by four order by ten) +FROM (select distinct ten, four from tenk1) ss; + +SELECT four, ten, + sum(ten) over (partition by four order by ten range between unbounded preceding and current row), + last_value(ten) over (partition by four order by ten range between unbounded preceding and current row) +FROM (select distinct ten, four from tenk1) ss; + +SELECT four, ten, + sum(ten) over (partition by four order by ten range between unbounded preceding and unbounded following), + last_value(ten) over (partition by four order by ten range between unbounded preceding and unbounded following) +FROM (select distinct ten, four from tenk1) ss; + +SELECT four, ten/4 as two, + sum(ten/4) over (partition by four order by ten/4 range between unbounded preceding and current row), + last_value(ten/4) over (partition by four order by ten/4 range between unbounded preceding and current row) +FROM (select distinct ten, four from tenk1) ss; + +SELECT four, ten/4 as two, + sum(ten/4) over (partition by four order by ten/4 rows between unbounded preceding and current row), + last_value(ten/4) over (partition by four order by ten/4 rows between unbounded preceding and current row) +FROM (select distinct ten, four from tenk1) ss; + +SELECT sum(unique1) over (order by four range between current row and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between current row and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 2 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude no others), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT first_value(unique1) over (ORDER BY four rows between current row and 2 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT first_value(unique1) over (ORDER BY four rows between current row and 2 following exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT first_value(unique1) over (ORDER BY four rows between current row and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 following exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 2 preceding and 1 preceding), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 1 following and 3 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between unbounded preceding and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (w range between current row and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four); + +SELECT sum(unique1) over (w range between unbounded preceding and current row exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four); + +SELECT sum(unique1) over (w range between unbounded preceding and current row exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four); + +SELECT sum(unique1) over (w range between unbounded preceding and current row exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four); + +SELECT first_value(unique1) over w, + nth_value(unique1, 2) over w AS nth_2, + last_value(unique1) over w, unique1, four +FROM tenk1 WHERE unique1 < 10 +WINDOW w AS (order by four range between current row and unbounded following); + +SELECT sum(unique1) over + (order by unique1 + rows (SELECT unique1 FROM tenk1 ORDER BY unique1 LIMIT 1) + 1 PRECEDING), + unique1 +FROM tenk1 WHERE unique1 < 10; + +CREATE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following) as sum_rows + FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +CREATE OR REPLACE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following + exclude current row) as sum_rows FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +CREATE OR REPLACE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following + exclude group) as sum_rows FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +CREATE OR REPLACE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following + exclude ties) as sum_rows FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +CREATE OR REPLACE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following + exclude no others) as sum_rows FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +CREATE OR REPLACE TEMP VIEW v_window AS + SELECT i, sum(i) over (order by i groups between 1 preceding and 1 following) as sum_rows FROM generate_series(1, 10) i; + +SELECT * FROM v_window; + +SELECT pg_get_viewdef('v_window'); + +DROP VIEW v_window; + +CREATE TEMP VIEW v_window AS + SELECT i, min(i) over (order by i range between '1 day' preceding and '10 days' following) as min_i + FROM generate_series(now(), now()+'100 days'::interval, '1 hour') i; + +SELECT pg_get_viewdef('v_window'); + +-- RANGE offset PRECEDING/FOLLOWING tests + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four desc range between 2::int8 preceding and 1::int2 preceding), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude no others), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 6::int2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four range between 2::int8 preceding and 6::int2 following exclude group), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by four order by unique1 range between 5::int8 preceding and 6::int2 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by four order by unique1 range between 5::int8 preceding and 6::int2 following + exclude current row),unique1, four +FROM tenk1 WHERE unique1 < 10; + +select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following), + salary, enroll_date from empsalary; + +select sum(salary) over (order by enroll_date desc range between '1 year'::interval preceding and '1 year'::interval following), + salary, enroll_date from empsalary; + +select sum(salary) over (order by enroll_date desc range between '1 year'::interval following and '1 year'::interval following), + salary, enroll_date from empsalary; + +select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following + exclude current row), salary, enroll_date from empsalary; + +select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following + exclude group), salary, enroll_date from empsalary; + +select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following + exclude ties), salary, enroll_date from empsalary; + +select first_value(salary) over(order by salary range between 1000 preceding and 1000 following), + lead(salary) over(order by salary range between 1000 preceding and 1000 following), + nth_value(salary, 1) over(order by salary range between 1000 preceding and 1000 following), + salary from empsalary; + +select last_value(salary) over(order by salary range between 1000 preceding and 1000 following), + lag(salary) over(order by salary range between 1000 preceding and 1000 following), + salary from empsalary; + +select first_value(salary) over(order by salary range between 1000 following and 3000 following + exclude current row), + lead(salary) over(order by salary range between 1000 following and 3000 following exclude ties), + nth_value(salary, 1) over(order by salary range between 1000 following and 3000 following + exclude ties), + salary from empsalary; + +select last_value(salary) over(order by salary range between 1000 following and 3000 following + exclude group), + lag(salary) over(order by salary range between 1000 following and 3000 following exclude group), + salary from empsalary; + +select first_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude ties), + last_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following), + salary, enroll_date from empsalary; + +select first_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude ties), + last_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude ties), + salary, enroll_date from empsalary; + +select first_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude group), + last_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude group), + salary, enroll_date from empsalary; + +select first_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude current row), + last_value(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following + exclude current row), + salary, enroll_date from empsalary; + +-- RANGE offset PRECEDING/FOLLOWING with null values +select x, y, + first_value(y) over w, + last_value(y) over w +from + (select x, x as y from generate_series(1,5) as x + union all select null, 42 + union all select null, 43) ss +window w as + (order by x asc nulls first range between 2 preceding and 2 following); + +select x, y, + first_value(y) over w, + last_value(y) over w +from + (select x, x as y from generate_series(1,5) as x + union all select null, 42 + union all select null, 43) ss +window w as + (order by x asc nulls last range between 2 preceding and 2 following); + +select x, y, + first_value(y) over w, + last_value(y) over w +from + (select x, x as y from generate_series(1,5) as x + union all select null, 42 + union all select null, 43) ss +window w as + (order by x desc nulls first range between 2 preceding and 2 following); + +select x, y, + first_value(y) over w, + last_value(y) over w +from + (select x, x as y from generate_series(1,5) as x + union all select null, 42 + union all select null, 43) ss +window w as + (order by x desc nulls last range between 2 preceding and 2 following); + +-- Check overflow behavior for various integer sizes + +select x, last_value(x) over (order by x::smallint range between current row and 2147450884 following) +from generate_series(32764, 32766) x; + +select x, last_value(x) over (order by x::smallint desc range between current row and 2147450885 following) +from generate_series(-32766, -32764) x; + +select x, last_value(x) over (order by x range between current row and 4 following) +from generate_series(2147483644, 2147483646) x; + +select x, last_value(x) over (order by x desc range between current row and 5 following) +from generate_series(-2147483646, -2147483644) x; + +select x, last_value(x) over (order by x range between current row and 4 following) +from generate_series(9223372036854775804, 9223372036854775806) x; + +select x, last_value(x) over (order by x desc range between current row and 5 following) +from generate_series(-9223372036854775806, -9223372036854775804) x; + +-- Test in_range for other numeric datatypes + +create temp table numerics( + id int, + f_float4 float4, + f_float8 float8, + f_numeric numeric +); + +insert into numerics values +(0, '-infinity', '-infinity', '-infinity'), +(1, -3, -3, -3), +(2, -1, -1, -1), +(3, 0, 0, 0), +(4, 1.1, 1.1, 1.1), +(5, 1.12, 1.12, 1.12), +(6, 2, 2, 2), +(7, 100, 100, 100), +(8, 'infinity', 'infinity', 'infinity'), +(9, 'NaN', 'NaN', 'NaN'); + +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 1 preceding and 1 following); +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 1 preceding and 1.1::float4 following); +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 'inf' preceding and 'inf' following); +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 'inf' preceding and 'inf' preceding); +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 'inf' following and 'inf' following); +select id, f_float4, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float4 range between + 1.1 preceding and 'NaN' following); -- error, NaN disallowed + +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 1 preceding and 1 following); +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 1 preceding and 1.1::float8 following); +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 'inf' preceding and 'inf' following); +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 'inf' preceding and 'inf' preceding); +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 'inf' following and 'inf' following); +select id, f_float8, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_float8 range between + 1.1 preceding and 'NaN' following); -- error, NaN disallowed + +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 1 preceding and 1 following); +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 1 preceding and 1.1::numeric following); +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 1 preceding and 1.1::float8 following); -- currently unsupported +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 'inf' preceding and 'inf' following); +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 'inf' preceding and 'inf' preceding); +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 'inf' following and 'inf' following); +select id, f_numeric, first_value(id) over w, last_value(id) over w +from numerics +window w as (order by f_numeric range between + 1.1 preceding and 'NaN' following); -- error, NaN disallowed + +-- Test in_range for other datetime datatypes + +create temp table datetimes( + id int, + f_time time, + f_timetz timetz, + f_interval interval, + f_timestamptz timestamptz, + f_timestamp timestamp +); + +insert into datetimes values +(1, '11:00', '11:00 BST', '1 year', '2000-10-19 10:23:54+01', '2000-10-19 10:23:54'), +(2, '12:00', '12:00 BST', '2 years', '2001-10-19 10:23:54+01', '2001-10-19 10:23:54'), +(3, '13:00', '13:00 BST', '3 years', '2001-10-19 10:23:54+01', '2001-10-19 10:23:54'), +(4, '14:00', '14:00 BST', '4 years', '2002-10-19 10:23:54+01', '2002-10-19 10:23:54'), +(5, '15:00', '15:00 BST', '5 years', '2003-10-19 10:23:54+01', '2003-10-19 10:23:54'), +(6, '15:00', '15:00 BST', '5 years', '2004-10-19 10:23:54+01', '2004-10-19 10:23:54'), +(7, '17:00', '17:00 BST', '7 years', '2005-10-19 10:23:54+01', '2005-10-19 10:23:54'), +(8, '18:00', '18:00 BST', '8 years', '2006-10-19 10:23:54+01', '2006-10-19 10:23:54'), +(9, '19:00', '19:00 BST', '9 years', '2007-10-19 10:23:54+01', '2007-10-19 10:23:54'), +(10, '20:00', '20:00 BST', '10 years', '2008-10-19 10:23:54+01', '2008-10-19 10:23:54'); + +select id, f_time, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_time range between + '70 min'::interval preceding and '2 hours'::interval following); + +select id, f_time, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_time desc range between + '70 min' preceding and '2 hours' following); + +select id, f_timetz, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timetz range between + '70 min'::interval preceding and '2 hours'::interval following); + +select id, f_timetz, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timetz desc range between + '70 min' preceding and '2 hours' following); + +select id, f_interval, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_interval range between + '1 year'::interval preceding and '1 year'::interval following); + +select id, f_interval, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_interval desc range between + '1 year' preceding and '1 year' following); + +select id, f_timestamptz, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timestamptz range between + '1 year'::interval preceding and '1 year'::interval following); + +select id, f_timestamptz, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timestamptz desc range between + '1 year' preceding and '1 year' following); + +select id, f_timestamp, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timestamp range between + '1 year'::interval preceding and '1 year'::interval following); + +select id, f_timestamp, first_value(id) over w, last_value(id) over w +from datetimes +window w as (order by f_timestamp desc range between + '1 year' preceding and '1 year' following); + +-- RANGE offset PRECEDING/FOLLOWING error cases +select sum(salary) over (order by enroll_date, salary range between '1 year'::interval preceding and '2 years'::interval following + exclude ties), salary, enroll_date from empsalary; + +select sum(salary) over (range between '1 year'::interval preceding and '2 years'::interval following + exclude ties), salary, enroll_date from empsalary; + +select sum(salary) over (order by depname range between '1 year'::interval preceding and '2 years'::interval following + exclude ties), salary, enroll_date from empsalary; + +select max(enroll_date) over (order by enroll_date range between 1 preceding and 2 following + exclude ties), salary, enroll_date from empsalary; + +select max(enroll_date) over (order by salary range between -1 preceding and 2 following + exclude ties), salary, enroll_date from empsalary; + +select max(enroll_date) over (order by salary range between 1 preceding and -2 following + exclude ties), salary, enroll_date from empsalary; + +select max(enroll_date) over (order by salary range between '1 year'::interval preceding and '2 years'::interval following + exclude ties), salary, enroll_date from empsalary; + +select max(enroll_date) over (order by enroll_date range between '1 year'::interval preceding and '-2 years'::interval following + exclude ties), salary, enroll_date from empsalary; + +-- GROUPS tests + +SELECT sum(unique1) over (order by four groups between unbounded preceding and current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between unbounded preceding and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between current row and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 1 preceding and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 1 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between unbounded preceding and 2 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 2 preceding and 1 preceding), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 0 preceding and 0 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following + exclude current row), unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following + exclude group), unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following + exclude ties), unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by ten + order by four groups between 0 preceding and 0 following),unique1, four, ten +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by ten + order by four groups between 0 preceding and 0 following exclude current row), unique1, four, ten +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by ten + order by four groups between 0 preceding and 0 following exclude group), unique1, four, ten +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (partition by ten + order by four groups between 0 preceding and 0 following exclude ties), unique1, four, ten +FROM tenk1 WHERE unique1 < 10; + +select first_value(salary) over(order by enroll_date groups between 1 preceding and 1 following), + lead(salary) over(order by enroll_date groups between 1 preceding and 1 following), + nth_value(salary, 1) over(order by enroll_date groups between 1 preceding and 1 following), + salary, enroll_date from empsalary; + +select last_value(salary) over(order by enroll_date groups between 1 preceding and 1 following), + lag(salary) over(order by enroll_date groups between 1 preceding and 1 following), + salary, enroll_date from empsalary; + +select first_value(salary) over(order by enroll_date groups between 1 following and 3 following + exclude current row), + lead(salary) over(order by enroll_date groups between 1 following and 3 following exclude ties), + nth_value(salary, 1) over(order by enroll_date groups between 1 following and 3 following + exclude ties), + salary, enroll_date from empsalary; + +select last_value(salary) over(order by enroll_date groups between 1 following and 3 following + exclude group), + lag(salary) over(order by enroll_date groups between 1 following and 3 following exclude group), + salary, enroll_date from empsalary; + +-- Show differences in offset interpretation between ROWS, RANGE, and GROUPS +WITH cte (x) AS ( + SELECT * FROM generate_series(1, 35, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following); + +WITH cte (x) AS ( + SELECT * FROM generate_series(1, 35, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x range between 1 preceding and 1 following); + +WITH cte (x) AS ( + SELECT * FROM generate_series(1, 35, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following); + +WITH cte (x) AS ( + select 1 union all select 1 union all select 1 union all + SELECT * FROM generate_series(5, 49, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following); + +WITH cte (x) AS ( + select 1 union all select 1 union all select 1 union all + SELECT * FROM generate_series(5, 49, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x range between 1 preceding and 1 following); + +WITH cte (x) AS ( + select 1 union all select 1 union all select 1 union all + SELECT * FROM generate_series(5, 49, 2) +) +SELECT x, (sum(x) over w) +FROM cte +WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following); + +-- with UNION +SELECT count(*) OVER (PARTITION BY four) FROM (SELECT * FROM tenk1 UNION ALL SELECT * FROM tenk2)s LIMIT 0; + +-- check some degenerate cases +create temp table t1 (f1 int, f2 int8); +insert into t1 values (1,1),(1,2),(2,2); + +select f1, sum(f1) over (partition by f1 + range between 1 preceding and 1 following) +from t1 where f1 = f2; -- error, must have order by +explain (costs off) +select f1, sum(f1) over (partition by f1 order by f2 + range between 1 preceding and 1 following) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1 order by f2 + range between 1 preceding and 1 following) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1, f1 order by f2 + range between 2 preceding and 1 preceding) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1, f2 order by f2 + range between 1 following and 2 following) +from t1 where f1 = f2; + +select f1, sum(f1) over (partition by f1 + groups between 1 preceding and 1 following) +from t1 where f1 = f2; -- error, must have order by +explain (costs off) +select f1, sum(f1) over (partition by f1 order by f2 + groups between 1 preceding and 1 following) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1 order by f2 + groups between 1 preceding and 1 following) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1, f1 order by f2 + groups between 2 preceding and 1 preceding) +from t1 where f1 = f2; +select f1, sum(f1) over (partition by f1, f2 order by f2 + groups between 1 following and 2 following) +from t1 where f1 = f2; + +-- ordering by a non-integer constant is allowed +SELECT rank() OVER (ORDER BY length('abc')); + +-- can't order by another window function +SELECT rank() OVER (ORDER BY rank() OVER (ORDER BY random())); + +-- some other errors +SELECT * FROM empsalary WHERE row_number() OVER (ORDER BY salary) < 10; + +SELECT * FROM empsalary INNER JOIN tenk1 ON row_number() OVER (ORDER BY salary) < 10; + +SELECT rank() OVER (ORDER BY 1), count(*) FROM empsalary GROUP BY 1; + +--SELECT * FROM rank() OVER (ORDER BY random()); + +DELETE FROM empsalary WHERE (rank() OVER (ORDER BY random())) > 10; + +DELETE FROM empsalary RETURNING rank() OVER (ORDER BY random()); + +SELECT count(*) OVER w FROM tenk1 WINDOW w AS (ORDER BY unique1), w AS (ORDER BY unique1); + +--SELECT rank() OVER (PARTITION BY four, ORDER BY ten) FROM tenk1; + +SELECT count() OVER () FROM tenk1; + +SELECT generate_series(1, 100) OVER () FROM empsalary; + +SELECT ntile(0) OVER (ORDER BY ten), ten, four FROM tenk1; + +SELECT nth_value(four, 0) OVER (ORDER BY ten), ten, four FROM tenk1; + +-- filter + +SELECT sum(salary), row_number() OVER (ORDER BY depname), sum( + sum(salary) FILTER (WHERE enroll_date > '2007-01-01') +) FILTER (WHERE depname <> 'sales') OVER (ORDER BY depname DESC) AS "filtered_sum", + depname +FROM empsalary GROUP BY depname; + +-- Test pushdown of quals into a subquery containing window functions + +-- pushdown is safe because all PARTITION BY clauses include depname: +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT depname, + sum(salary) OVER (PARTITION BY depname) depsalary, + min(salary) OVER (PARTITION BY depname || 'A', depname) depminsalary + FROM empsalary) emp +WHERE depname = 'sales'; + +-- pushdown is unsafe because there's a PARTITION BY clause without depname: +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT depname, + sum(salary) OVER (PARTITION BY enroll_date) enroll_salary, + min(salary) OVER (PARTITION BY depname) depminsalary + FROM empsalary) emp +WHERE depname = 'sales'; + +-- Test Sort node collapsing +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT depname, + sum(salary) OVER (PARTITION BY depname order by empno) depsalary, + min(salary) OVER (PARTITION BY depname, empno order by enroll_date) depminsalary + FROM empsalary) emp +WHERE depname = 'sales'; + +-- Test Sort node reordering +EXPLAIN (COSTS OFF) +SELECT + lead(1) OVER (PARTITION BY depname ORDER BY salary, enroll_date), + lag(1) OVER (PARTITION BY depname ORDER BY salary,enroll_date,empno) +FROM empsalary; + +-- cleanup +DROP TABLE empsalary; + +-- test user-defined window function with named args and default args +CREATE FUNCTION nth_value_def(val anyelement, n integer = 1) RETURNS anyelement + LANGUAGE internal WINDOW IMMUTABLE STRICT AS 'window_nth_value'; + +SELECT nth_value_def(n := 2, val := ten) OVER (PARTITION BY four), ten, four + FROM (SELECT * FROM tenk1 WHERE unique2 < 10 ORDER BY four, ten) s; + +SELECT nth_value_def(ten) OVER (PARTITION BY four), ten, four + FROM (SELECT * FROM tenk1 WHERE unique2 < 10 ORDER BY four, ten) s; + +-- +-- Test the basic moving-aggregate machinery +-- + +-- create aggregates that record the series of transform calls (these are +-- intentionally not true inverses) + +CREATE FUNCTION logging_sfunc_nonstrict(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '*' || quote_nullable($2) $$ +LANGUAGE SQL IMMUTABLE; + +CREATE FUNCTION logging_msfunc_nonstrict(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '+' || quote_nullable($2) $$ +LANGUAGE SQL IMMUTABLE; + +CREATE FUNCTION logging_minvfunc_nonstrict(text, anyelement) RETURNS text AS +$$ SELECT $1 || '-' || quote_nullable($2) $$ +LANGUAGE SQL IMMUTABLE; + +CREATE AGGREGATE logging_agg_nonstrict (anyelement) +( + stype = text, + sfunc = logging_sfunc_nonstrict, + mstype = text, + msfunc = logging_msfunc_nonstrict, + minvfunc = logging_minvfunc_nonstrict +); + +CREATE AGGREGATE logging_agg_nonstrict_initcond (anyelement) +( + stype = text, + sfunc = logging_sfunc_nonstrict, + mstype = text, + msfunc = logging_msfunc_nonstrict, + minvfunc = logging_minvfunc_nonstrict, + initcond = 'I', + minitcond = 'MI' +); + +CREATE FUNCTION logging_sfunc_strict(text, anyelement) RETURNS text AS +$$ SELECT $1 || '*' || quote_nullable($2) $$ +LANGUAGE SQL STRICT IMMUTABLE; + +CREATE FUNCTION logging_msfunc_strict(text, anyelement) RETURNS text AS +$$ SELECT $1 || '+' || quote_nullable($2) $$ +LANGUAGE SQL STRICT IMMUTABLE; + +CREATE FUNCTION logging_minvfunc_strict(text, anyelement) RETURNS text AS +$$ SELECT $1 || '-' || quote_nullable($2) $$ +LANGUAGE SQL STRICT IMMUTABLE; + +CREATE AGGREGATE logging_agg_strict (text) +( + stype = text, + sfunc = logging_sfunc_strict, + mstype = text, + msfunc = logging_msfunc_strict, + minvfunc = logging_minvfunc_strict +); + +CREATE AGGREGATE logging_agg_strict_initcond (anyelement) +( + stype = text, + sfunc = logging_sfunc_strict, + mstype = text, + msfunc = logging_msfunc_strict, + minvfunc = logging_minvfunc_strict, + initcond = 'I', + minitcond = 'MI' +); + +-- test strict and non-strict cases +SELECT + p::text || ',' || i::text || ':' || COALESCE(v::text, 'NULL') AS row, + logging_agg_nonstrict(v) over wnd as nstrict, + logging_agg_nonstrict_initcond(v) over wnd as nstrict_init, + logging_agg_strict(v::text) over wnd as strict, + logging_agg_strict_initcond(v) over wnd as strict_init +FROM (VALUES + (1, 1, NULL), + (1, 2, 'a'), + (1, 3, 'b'), + (1, 4, NULL), + (1, 5, NULL), + (1, 6, 'c'), + (2, 1, NULL), + (2, 2, 'x'), + (3, 1, 'z') +) AS t(p, i, v) +WINDOW wnd AS (PARTITION BY P ORDER BY i ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) +ORDER BY p, i; + +-- and again, but with filter +SELECT + p::text || ',' || i::text || ':' || + CASE WHEN f THEN COALESCE(v::text, 'NULL') ELSE '-' END as row, + logging_agg_nonstrict(v) filter(where f) over wnd as nstrict_filt, + logging_agg_nonstrict_initcond(v) filter(where f) over wnd as nstrict_init_filt, + logging_agg_strict(v::text) filter(where f) over wnd as strict_filt, + logging_agg_strict_initcond(v) filter(where f) over wnd as strict_init_filt +FROM (VALUES + (1, 1, true, NULL), + (1, 2, false, 'a'), + (1, 3, true, 'b'), + (1, 4, false, NULL), + (1, 5, false, NULL), + (1, 6, false, 'c'), + (2, 1, false, NULL), + (2, 2, true, 'x'), + (3, 1, true, 'z') +) AS t(p, i, f, v) +WINDOW wnd AS (PARTITION BY p ORDER BY i ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) +ORDER BY p, i; + +-- test that volatile arguments disable moving-aggregate mode +SELECT + i::text || ':' || COALESCE(v::text, 'NULL') as row, + logging_agg_strict(v::text) + over wnd as inverse, + logging_agg_strict(v::text || CASE WHEN random() < 0 then '?' ELSE '' END) + over wnd as noinverse +FROM (VALUES + (1, 'a'), + (2, 'b'), + (3, 'c') +) AS t(i, v) +WINDOW wnd AS (ORDER BY i ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) +ORDER BY i; + +SELECT + i::text || ':' || COALESCE(v::text, 'NULL') as row, + logging_agg_strict(v::text) filter(where true) + over wnd as inverse, + logging_agg_strict(v::text) filter(where random() >= 0) + over wnd as noinverse +FROM (VALUES + (1, 'a'), + (2, 'b'), + (3, 'c') +) AS t(i, v) +WINDOW wnd AS (ORDER BY i ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) +ORDER BY i; + +-- test that non-overlapping windows don't use inverse transitions +SELECT + logging_agg_strict(v::text) OVER wnd +FROM (VALUES + (1, 'a'), + (2, 'b'), + (3, 'c') +) AS t(i, v) +WINDOW wnd AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND CURRENT ROW) +ORDER BY i; + +-- test that returning NULL from the inverse transition functions +-- restarts the aggregation from scratch. The second aggregate is supposed +-- to test cases where only some aggregates restart, the third one checks +-- that one aggregate restarting doesn't cause others to restart. + +CREATE FUNCTION sum_int_randrestart_minvfunc(int4, int4) RETURNS int4 AS +$$ SELECT CASE WHEN random() < 0.2 THEN NULL ELSE $1 - $2 END $$ +LANGUAGE SQL STRICT; + +CREATE AGGREGATE sum_int_randomrestart (int4) +( + stype = int4, + sfunc = int4pl, + mstype = int4, + msfunc = int4pl, + minvfunc = sum_int_randrestart_minvfunc +); + +WITH +vs AS ( + SELECT i, (random() * 100)::int4 AS v + FROM generate_series(1, 100) AS i +), +sum_following AS ( + SELECT i, SUM(v) OVER + (ORDER BY i DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS s + FROM vs +) +SELECT DISTINCT + sum_following.s = sum_int_randomrestart(v) OVER fwd AS eq1, + -sum_following.s = sum_int_randomrestart(-v) OVER fwd AS eq2, + 100*3+(vs.i-1)*3 = length(logging_agg_nonstrict(''::text) OVER fwd) AS eq3 +FROM vs +JOIN sum_following ON sum_following.i = vs.i +WINDOW fwd AS ( + ORDER BY vs.i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +); + +-- +-- Test various built-in aggregates that have moving-aggregate support +-- + +-- test inverse transition functions handle NULLs properly +SELECT i,AVG(v::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,AVG(v::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,AVG(v::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,AVG(v::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1.5),(2,2.5),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,AVG(v::interval) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,'1 sec'),(2,'2 sec'),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::money) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,'1.10'),(2,'2.20'),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::interval) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,'1 sec'),(2,'2 sec'),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1.1),(2,2.2),(3,NULL),(4,NULL)) t(i,v); + +SELECT SUM(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1.01),(2,2),(3,3)) v(i,n); + +SELECT i,COUNT(v) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,COUNT(*) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT VAR_POP(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_POP(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_POP(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_POP(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_SAMP(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_SAMP(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_SAMP(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VAR_SAMP(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VARIANCE(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VARIANCE(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VARIANCE(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT VARIANCE(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT STDDEV_POP(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_POP(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_POP(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_POP(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_SAMP(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_SAMP(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_SAMP(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV_SAMP(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(1,NULL),(2,600),(3,470),(4,170),(5,430),(6,300)) r(i,n); + +SELECT STDDEV(n::bigint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(0,NULL),(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT STDDEV(n::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(0,NULL),(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT STDDEV(n::smallint) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(0,NULL),(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +SELECT STDDEV(n::numeric) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + FROM (VALUES(0,NULL),(1,600),(2,470),(3,170),(4,430),(5,300)) r(i,n); + +-- test that inverse transition functions work with various frame options +SELECT i,SUM(v::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND CURRENT ROW) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::int) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,NULL),(4,NULL)) t(i,v); + +SELECT i,SUM(v::int) OVER (ORDER BY i ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) + FROM (VALUES(1,1),(2,2),(3,3),(4,4)) t(i,v); + +-- ensure aggregate over numeric properly recovers from NaN values +SELECT a, b, + SUM(b) OVER(ORDER BY A ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) +FROM (VALUES(1,1::numeric),(2,2),(3,'NaN'),(4,3),(5,4)) t(a,b); + +-- It might be tempting for someone to add an inverse trans function for +-- float and double precision. This should not be done as it can give incorrect +-- results. This test should fail if anyone ever does this without thinking too +-- hard about it. +SELECT to_char(SUM(n::float8) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING),'999999999999999999999D9') + FROM (VALUES(1,1e20),(2,1)) n(i,n); + +SELECT i, b, bool_and(b) OVER w, bool_or(b) OVER w + FROM (VALUES (1,true), (2,true), (3,false), (4,false), (5,true)) v(i,b) + WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING); + +-- Tests for problems with failure to walk or mutate expressions +-- within window frame clauses. + +-- test walker (fails with collation error if expressions are not walked) +SELECT array_agg(i) OVER w + FROM generate_series(1,5) i +WINDOW w AS (ORDER BY i ROWS BETWEEN (('foo' < 'foobar')::integer) PRECEDING AND CURRENT ROW); + +-- test mutator (fails when inlined if expressions are not mutated) +CREATE FUNCTION pg_temp.f(group_size BIGINT) RETURNS SETOF integer[] +AS $$ + SELECT array_agg(s) OVER w + FROM generate_series(1,5) s + WINDOW w AS (ORDER BY s ROWS BETWEEN CURRENT ROW AND GROUP_SIZE FOLLOWING) +$$ LANGUAGE SQL STABLE; + +EXPLAIN (costs off) SELECT * FROM pg_temp.f(2); +SELECT * FROM pg_temp.f(2); diff --git a/src/test/resources/postgresql-corpus/with.sql b/src/test/resources/postgresql-corpus/with.sql new file mode 100644 index 0000000..213c19e --- /dev/null +++ b/src/test/resources/postgresql-corpus/with.sql @@ -0,0 +1,1041 @@ +-- +-- Tests for common table expressions (WITH query, ... SELECT ...) +-- + +-- Basic WITH +WITH q1(x,y) AS (SELECT 1,2) +SELECT * FROM q1, q1 AS q2; + +-- Multiple uses are evaluated only once +SELECT count(*) FROM ( + WITH q1(x) AS (SELECT random() FROM generate_series(1, 5)) + SELECT * FROM q1 + UNION + SELECT * FROM q1 +) ss; + +-- WITH RECURSIVE + +-- sum of 1..100 +WITH RECURSIVE t(n) AS ( + VALUES (1) +UNION ALL + SELECT n+1 FROM t WHERE n < 100 +) +SELECT sum(n) FROM t; + +WITH RECURSIVE t(n) AS ( + SELECT (VALUES(1)) +UNION ALL + SELECT n+1 FROM t WHERE n < 5 +) +SELECT * FROM t; + +-- recursive view +CREATE RECURSIVE VIEW nums (n) AS + VALUES (1) +UNION ALL + SELECT n+1 FROM nums WHERE n < 5; + +SELECT * FROM nums; + +CREATE OR REPLACE RECURSIVE VIEW nums (n) AS + VALUES (1) +UNION ALL + SELECT n+1 FROM nums WHERE n < 6; + +SELECT * FROM nums; + +-- This is an infinite loop with UNION ALL, but not with UNION +WITH RECURSIVE t(n) AS ( + SELECT 1 +UNION + SELECT 10-n FROM t) +SELECT * FROM t; + +-- This'd be an infinite loop, but outside query reads only as much as needed +WITH RECURSIVE t(n) AS ( + VALUES (1) +UNION ALL + SELECT n+1 FROM t) +SELECT * FROM t LIMIT 10; + +-- UNION case should have same property +WITH RECURSIVE t(n) AS ( + SELECT 1 +UNION + SELECT n+1 FROM t) +SELECT * FROM t LIMIT 10; + +-- Test behavior with an unknown-type literal in the WITH +WITH q AS (SELECT 'foo' AS x) +SELECT x, x IS OF (text) AS is_text FROM q; + +WITH RECURSIVE t(n) AS ( + SELECT 'foo' +UNION ALL + SELECT n || ' bar' FROM t WHERE length(n) < 20 +) +SELECT n, n IS OF (text) AS is_text FROM t; + +-- In a perfect world, this would work and resolve the literal as int ... +-- but for now, we have to be content with resolving to text too soon. +WITH RECURSIVE t(n) AS ( + SELECT '7' +UNION ALL + SELECT n+1 FROM t WHERE n < 10 +) +SELECT n, n IS OF (int) AS is_int FROM t; + +-- +-- Some examples with a tree +-- +-- department structure represented here is as follows: +-- +-- ROOT-+->A-+->B-+->C +-- | | +-- | +->D-+->F +-- +->E-+->G + +CREATE TEMP TABLE department ( + id INTEGER PRIMARY KEY, -- department ID + parent_department INTEGER REFERENCES department, -- upper department ID + name TEXT -- department name +); + +INSERT INTO department VALUES (0, NULL, 'ROOT'); +INSERT INTO department VALUES (1, 0, 'A'); +INSERT INTO department VALUES (2, 1, 'B'); +INSERT INTO department VALUES (3, 2, 'C'); +INSERT INTO department VALUES (4, 2, 'D'); +INSERT INTO department VALUES (5, 0, 'E'); +INSERT INTO department VALUES (6, 4, 'F'); +INSERT INTO department VALUES (7, 5, 'G'); + + +-- extract all departments under 'A'. Result should be A, B, C, D and F +WITH RECURSIVE subdepartment AS +( + -- non recursive term + SELECT name as root_name, * FROM department WHERE name = 'A' + + UNION ALL + + -- recursive term + SELECT sd.root_name, d.* FROM department AS d, subdepartment AS sd + WHERE d.parent_department = sd.id +) +SELECT * FROM subdepartment ORDER BY name; + +-- extract all departments under 'A' with "level" number +WITH RECURSIVE subdepartment(level, id, parent_department, name) AS +( + -- non recursive term + SELECT 1, * FROM department WHERE name = 'A' + + UNION ALL + + -- recursive term + SELECT sd.level + 1, d.* FROM department AS d, subdepartment AS sd + WHERE d.parent_department = sd.id +) +SELECT * FROM subdepartment ORDER BY name; + +-- extract all departments under 'A' with "level" number. +-- Only shows level 2 or more +WITH RECURSIVE subdepartment(level, id, parent_department, name) AS +( + -- non recursive term + SELECT 1, * FROM department WHERE name = 'A' + + UNION ALL + + -- recursive term + SELECT sd.level + 1, d.* FROM department AS d, subdepartment AS sd + WHERE d.parent_department = sd.id +) +SELECT * FROM subdepartment WHERE level >= 2 ORDER BY name; + +-- "RECURSIVE" is ignored if the query has no self-reference +WITH RECURSIVE subdepartment AS +( + -- note lack of recursive UNION structure + SELECT * FROM department WHERE name = 'A' +) +SELECT * FROM subdepartment ORDER BY name; + +-- inside subqueries +SELECT count(*) FROM ( + WITH RECURSIVE t(n) AS ( + SELECT 1 UNION ALL SELECT n + 1 FROM t WHERE n < 500 + ) + SELECT * FROM t) AS t WHERE n < ( + SELECT count(*) FROM ( + WITH RECURSIVE t(n) AS ( + SELECT 1 UNION ALL SELECT n + 1 FROM t WHERE n < 100 + ) + SELECT * FROM t WHERE n < 50000 + ) AS t WHERE n < 100); + +-- use same CTE twice at different subquery levels +WITH q1(x,y) AS ( + SELECT hundred, sum(ten) FROM tenk1 GROUP BY hundred + ) +SELECT count(*) FROM q1 WHERE y > (SELECT sum(y)/100 FROM q1 qsub); + +-- via a VIEW +CREATE TEMPORARY VIEW vsubdepartment AS + WITH RECURSIVE subdepartment AS + ( + -- non recursive term + SELECT * FROM department WHERE name = 'A' + UNION ALL + -- recursive term + SELECT d.* FROM department AS d, subdepartment AS sd + WHERE d.parent_department = sd.id + ) + SELECT * FROM subdepartment; + +SELECT * FROM vsubdepartment ORDER BY name; + +-- Check reverse listing +SELECT pg_get_viewdef('vsubdepartment'::regclass); +SELECT pg_get_viewdef('vsubdepartment'::regclass, true); + +-- Another reverse-listing example +CREATE VIEW sums_1_100 AS +WITH RECURSIVE t(n) AS ( + VALUES (1) +UNION ALL + SELECT n+1 FROM t WHERE n < 100 +) +SELECT sum(n) FROM t; + +\d+ sums_1_100 + +-- corner case in which sub-WITH gets initialized first +with recursive q as ( + select * from department + union all + (with x as (select * from q) + select * from x) + ) +select * from q limit 24; + +with recursive q as ( + select * from department + union all + (with recursive x as ( + select * from department + union all + (select * from q union all select * from x) + ) + select * from x) + ) +select * from q limit 32; + +-- recursive term has sub-UNION +WITH RECURSIVE t(i,j) AS ( + VALUES (1,2) + UNION ALL + SELECT t2.i, t.j+1 FROM + (SELECT 2 AS i UNION ALL SELECT 3 AS i) AS t2 + JOIN t ON (t2.i = t.i+1)) + + SELECT * FROM t; + +-- +-- different tree example +-- +CREATE TEMPORARY TABLE tree( + id INTEGER PRIMARY KEY, + parent_id INTEGER REFERENCES tree(id) +); + +INSERT INTO tree +VALUES (1, NULL), (2, 1), (3,1), (4,2), (5,2), (6,2), (7,3), (8,3), + (9,4), (10,4), (11,7), (12,7), (13,7), (14, 9), (15,11), (16,11); + +-- +-- get all paths from "second level" nodes to leaf nodes +-- +WITH RECURSIVE t(id, path) AS ( + VALUES(1,ARRAY[]::integer[]) +UNION ALL + SELECT tree.id, t.path || tree.id + FROM tree JOIN t ON (tree.parent_id = t.id) +) +SELECT t1.*, t2.* FROM t AS t1 JOIN t AS t2 ON + (t1.path[1] = t2.path[1] AND + array_upper(t1.path,1) = 1 AND + array_upper(t2.path,1) > 1) + ORDER BY t1.id, t2.id; + +-- just count 'em +WITH RECURSIVE t(id, path) AS ( + VALUES(1,ARRAY[]::integer[]) +UNION ALL + SELECT tree.id, t.path || tree.id + FROM tree JOIN t ON (tree.parent_id = t.id) +) +SELECT t1.id, count(t2.*) FROM t AS t1 JOIN t AS t2 ON + (t1.path[1] = t2.path[1] AND + array_upper(t1.path,1) = 1 AND + array_upper(t2.path,1) > 1) + GROUP BY t1.id + ORDER BY t1.id; + +-- this variant tickled a whole-row-variable bug in 8.4devel +WITH RECURSIVE t(id, path) AS ( + VALUES(1,ARRAY[]::integer[]) +UNION ALL + SELECT tree.id, t.path || tree.id + FROM tree JOIN t ON (tree.parent_id = t.id) +) +SELECT t1.id, t2.path, t2 FROM t AS t1 JOIN t AS t2 ON +(t1.id=t2.id); + +-- +-- test cycle detection +-- +create temp table graph( f int, t int, label text ); + +insert into graph values + (1, 2, 'arc 1 -> 2'), + (1, 3, 'arc 1 -> 3'), + (2, 3, 'arc 2 -> 3'), + (1, 4, 'arc 1 -> 4'), + (4, 5, 'arc 4 -> 5'), + (5, 1, 'arc 5 -> 1'); + +with recursive search_graph(f, t, label, path, cycle) as ( + select *, array[row(g.f, g.t)], false from graph g + union all + select g.*, path || row(g.f, g.t), row(g.f, g.t) = any(path) + from graph g, search_graph sg + where g.f = sg.t and not cycle +) +select * from search_graph; + +-- ordering by the path column has same effect as SEARCH DEPTH FIRST +with recursive search_graph(f, t, label, path, cycle) as ( + select *, array[row(g.f, g.t)], false from graph g + union all + select g.*, path || row(g.f, g.t), row(g.f, g.t) = any(path) + from graph g, search_graph sg + where g.f = sg.t and not cycle +) +select * from search_graph order by path; + +-- +-- test multiple WITH queries +-- +WITH RECURSIVE + y (id) AS (VALUES (1)), + x (id) AS (SELECT * FROM y UNION ALL SELECT id+1 FROM x WHERE id < 5) +SELECT * FROM x; + +-- forward reference OK +WITH RECURSIVE + x(id) AS (SELECT * FROM y UNION ALL SELECT id+1 FROM x WHERE id < 5), + y(id) AS (values (1)) + SELECT * FROM x; + +WITH RECURSIVE + x(id) AS + (VALUES (1) UNION ALL SELECT id+1 FROM x WHERE id < 5), + y(id) AS + (VALUES (1) UNION ALL SELECT id+1 FROM y WHERE id < 10) + SELECT y.*, x.* FROM y LEFT JOIN x USING (id); + +WITH RECURSIVE + x(id) AS + (VALUES (1) UNION ALL SELECT id+1 FROM x WHERE id < 5), + y(id) AS + (VALUES (1) UNION ALL SELECT id+1 FROM x WHERE id < 10) + SELECT y.*, x.* FROM y LEFT JOIN x USING (id); + +WITH RECURSIVE + x(id) AS + (SELECT 1 UNION ALL SELECT id+1 FROM x WHERE id < 3 ), + y(id) AS + (SELECT * FROM x UNION ALL SELECT * FROM x), + z(id) AS + (SELECT * FROM x UNION ALL SELECT id+1 FROM z WHERE id < 10) + SELECT * FROM z; + +WITH RECURSIVE + x(id) AS + (SELECT 1 UNION ALL SELECT id+1 FROM x WHERE id < 3 ), + y(id) AS + (SELECT * FROM x UNION ALL SELECT * FROM x), + z(id) AS + (SELECT * FROM y UNION ALL SELECT id+1 FROM z WHERE id < 10) + SELECT * FROM z; + +-- +-- Test WITH attached to a data-modifying statement +-- + +CREATE TEMPORARY TABLE y (a INTEGER); +INSERT INTO y SELECT generate_series(1, 10); + +WITH t AS ( + SELECT a FROM y +) +INSERT INTO y +SELECT a+20 FROM t RETURNING *; + +SELECT * FROM y; + +WITH t AS ( + SELECT a FROM y +) +UPDATE y SET a = y.a-10 FROM t WHERE y.a > 20 AND t.a = y.a RETURNING y.a; + +SELECT * FROM y; + +WITH RECURSIVE t(a) AS ( + SELECT 11 + UNION ALL + SELECT a+1 FROM t WHERE a < 50 +) +DELETE FROM y USING t WHERE t.a = y.a RETURNING y.a; + +SELECT * FROM y; + +DROP TABLE y; + +-- +-- error cases +-- + +-- INTERSECT +WITH RECURSIVE x(n) AS (SELECT 1 INTERSECT SELECT n+1 FROM x) + SELECT * FROM x; + +WITH RECURSIVE x(n) AS (SELECT 1 INTERSECT ALL SELECT n+1 FROM x) + SELECT * FROM x; + +-- EXCEPT +WITH RECURSIVE x(n) AS (SELECT 1 EXCEPT SELECT n+1 FROM x) + SELECT * FROM x; + +WITH RECURSIVE x(n) AS (SELECT 1 EXCEPT ALL SELECT n+1 FROM x) + SELECT * FROM x; + +-- no non-recursive term +WITH RECURSIVE x(n) AS (SELECT n FROM x) + SELECT * FROM x; + +-- recursive term in the left hand side (strictly speaking, should allow this) +WITH RECURSIVE x(n) AS (SELECT n FROM x UNION ALL SELECT 1) + SELECT * FROM x; + +CREATE TEMPORARY TABLE y (a INTEGER); +INSERT INTO y SELECT generate_series(1, 10); + +-- LEFT JOIN + +WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1 + UNION ALL + SELECT x.n+1 FROM y LEFT JOIN x ON x.n = y.a WHERE n < 10) +SELECT * FROM x; + +-- RIGHT JOIN +WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1 + UNION ALL + SELECT x.n+1 FROM x RIGHT JOIN y ON x.n = y.a WHERE n < 10) +SELECT * FROM x; + +-- FULL JOIN +WITH RECURSIVE x(n) AS (SELECT a FROM y WHERE a = 1 + UNION ALL + SELECT x.n+1 FROM x FULL JOIN y ON x.n = y.a WHERE n < 10) +SELECT * FROM x; + +-- subquery +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x + WHERE n IN (SELECT * FROM x)) + SELECT * FROM x; + +-- aggregate functions +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT count(*) FROM x) + SELECT * FROM x; + +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT sum(n) FROM x) + SELECT * FROM x; + +-- ORDER BY +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x ORDER BY 1) + SELECT * FROM x; + +-- LIMIT/OFFSET +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x LIMIT 10 OFFSET 1) + SELECT * FROM x; + +-- FOR UPDATE +WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x FOR UPDATE) + SELECT * FROM x; + +-- target list has a recursive query name +WITH RECURSIVE x(id) AS (values (1) + UNION ALL + SELECT (SELECT * FROM x) FROM x WHERE id < 5 +) SELECT * FROM x; + +-- mutual recursive query (not implemented) +WITH RECURSIVE + x (id) AS (SELECT 1 UNION ALL SELECT id+1 FROM y WHERE id < 5), + y (id) AS (SELECT 1 UNION ALL SELECT id+1 FROM x WHERE id < 5) +SELECT * FROM x; + +-- non-linear recursion is not allowed +WITH RECURSIVE foo(i) AS + (values (1) + UNION ALL + (SELECT i+1 FROM foo WHERE i < 10 + UNION ALL + SELECT i+1 FROM foo WHERE i < 5) +) SELECT * FROM foo; + +WITH RECURSIVE foo(i) AS + (values (1) + UNION ALL + SELECT * FROM + (SELECT i+1 FROM foo WHERE i < 10 + UNION ALL + SELECT i+1 FROM foo WHERE i < 5) AS t +) SELECT * FROM foo; + +WITH RECURSIVE foo(i) AS + (values (1) + UNION ALL + (SELECT i+1 FROM foo WHERE i < 10 + EXCEPT + SELECT i+1 FROM foo WHERE i < 5) +) SELECT * FROM foo; + +WITH RECURSIVE foo(i) AS + (values (1) + UNION ALL + (SELECT i+1 FROM foo WHERE i < 10 + INTERSECT + SELECT i+1 FROM foo WHERE i < 5) +) SELECT * FROM foo; + +-- Wrong type induced from non-recursive term +WITH RECURSIVE foo(i) AS + (SELECT i FROM (VALUES(1),(2)) t(i) + UNION ALL + SELECT (i+1)::numeric(10,0) FROM foo WHERE i < 10) +SELECT * FROM foo; + +-- rejects different typmod, too (should we allow this?) +WITH RECURSIVE foo(i) AS + (SELECT i::numeric(3,0) FROM (VALUES(1),(2)) t(i) + UNION ALL + SELECT (i+1)::numeric(10,0) FROM foo WHERE i < 10) +SELECT * FROM foo; + +-- disallow OLD/NEW reference in CTE +CREATE TEMPORARY TABLE x (n integer); +CREATE RULE r2 AS ON UPDATE TO x DO INSTEAD + WITH t AS (SELECT OLD.*) UPDATE y SET a = t.n FROM t; + +-- +-- test for bug #4902 +-- +with cte(foo) as ( values(42) ) values((select foo from cte)); +with cte(foo) as ( select 42 ) select * from ((select foo from cte)) q; + +-- test CTE referencing an outer-level variable (to see that changed-parameter +-- signaling still works properly after fixing this bug) +select ( with cte(foo) as ( values(f1) ) + select (select foo from cte) ) +from int4_tbl; + +select ( with cte(foo) as ( values(f1) ) + values((select foo from cte)) ) +from int4_tbl; + +-- +-- test for nested-recursive-WITH bug +-- +WITH RECURSIVE t(j) AS ( + WITH RECURSIVE s(i) AS ( + VALUES (1) + UNION ALL + SELECT i+1 FROM s WHERE i < 10 + ) + SELECT i FROM s + UNION ALL + SELECT j+1 FROM t WHERE j < 10 +) +SELECT * FROM t; + +-- +-- test WITH attached to intermediate-level set operation +-- + +WITH outermost(x) AS ( + SELECT 1 + UNION (WITH innermost as (SELECT 2) + SELECT * FROM innermost + UNION SELECT 3) +) +SELECT * FROM outermost ORDER BY 1; + +WITH outermost(x) AS ( + SELECT 1 + UNION (WITH innermost as (SELECT 2) + SELECT * FROM outermost -- fail + UNION SELECT * FROM innermost) +) +SELECT * FROM outermost ORDER BY 1; + +WITH RECURSIVE outermost(x) AS ( + SELECT 1 + UNION (WITH innermost as (SELECT 2) + SELECT * FROM outermost + UNION SELECT * FROM innermost) +) +SELECT * FROM outermost ORDER BY 1; + +WITH RECURSIVE outermost(x) AS ( + WITH innermost as (SELECT 2 FROM outermost) -- fail + SELECT * FROM innermost + UNION SELECT * from outermost +) +SELECT * FROM outermost ORDER BY 1; + +-- +-- This test will fail with the old implementation of PARAM_EXEC parameter +-- assignment, because the "q1" Var passed down to A's targetlist subselect +-- looks exactly like the "A.id" Var passed down to C's subselect, causing +-- the old code to give them the same runtime PARAM_EXEC slot. But the +-- lifespans of the two parameters overlap, thanks to B also reading A. +-- + +with +A as ( select q2 as id, (select q1) as x from int8_tbl ), +B as ( select id, row_number() over (partition by id) as r from A ), +C as ( select A.id, array(select B.id from B where B.id = A.id) from A ) +select * from C; + +-- +-- Test CTEs read in non-initialization orders +-- + +WITH RECURSIVE + tab(id_key,link) AS (VALUES (1,17), (2,17), (3,17), (4,17), (6,17), (5,17)), + iter (id_key, row_type, link) AS ( + SELECT 0, 'base', 17 + UNION ALL ( + WITH remaining(id_key, row_type, link, min) AS ( + SELECT tab.id_key, 'true'::text, iter.link, MIN(tab.id_key) OVER () + FROM tab INNER JOIN iter USING (link) + WHERE tab.id_key > iter.id_key + ), + first_remaining AS ( + SELECT id_key, row_type, link + FROM remaining + WHERE id_key=min + ), + effect AS ( + SELECT tab.id_key, 'new'::text, tab.link + FROM first_remaining e INNER JOIN tab ON e.id_key=tab.id_key + WHERE e.row_type = 'false' + ) + SELECT * FROM first_remaining + UNION ALL SELECT * FROM effect + ) + ) +SELECT * FROM iter; + +WITH RECURSIVE + tab(id_key,link) AS (VALUES (1,17), (2,17), (3,17), (4,17), (6,17), (5,17)), + iter (id_key, row_type, link) AS ( + SELECT 0, 'base', 17 + UNION ( + WITH remaining(id_key, row_type, link, min) AS ( + SELECT tab.id_key, 'true'::text, iter.link, MIN(tab.id_key) OVER () + FROM tab INNER JOIN iter USING (link) + WHERE tab.id_key > iter.id_key + ), + first_remaining AS ( + SELECT id_key, row_type, link + FROM remaining + WHERE id_key=min + ), + effect AS ( + SELECT tab.id_key, 'new'::text, tab.link + FROM first_remaining e INNER JOIN tab ON e.id_key=tab.id_key + WHERE e.row_type = 'false' + ) + SELECT * FROM first_remaining + UNION ALL SELECT * FROM effect + ) + ) +SELECT * FROM iter; + +-- +-- Data-modifying statements in WITH +-- + +-- INSERT ... RETURNING +WITH t AS ( + INSERT INTO y + VALUES + (11), + (12), + (13), + (14), + (15), + (16), + (17), + (18), + (19), + (20) + RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +-- UPDATE ... RETURNING +WITH t AS ( + UPDATE y + SET a=a+1 + RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +-- DELETE ... RETURNING +WITH t AS ( + DELETE FROM y + WHERE a <= 10 + RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +-- forward reference +WITH RECURSIVE t AS ( + INSERT INTO y + SELECT a+5 FROM t2 WHERE a > 5 + RETURNING * +), t2 AS ( + UPDATE y SET a=a-11 RETURNING * +) +SELECT * FROM t +UNION ALL +SELECT * FROM t2; + +SELECT * FROM y; + +-- unconditional DO INSTEAD rule +CREATE RULE y_rule AS ON DELETE TO y DO INSTEAD + INSERT INTO y VALUES(42) RETURNING *; + +WITH t AS ( + DELETE FROM y RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +DROP RULE y_rule ON y; + +-- check merging of outer CTE with CTE in a rule action +CREATE TEMP TABLE bug6051 AS + select i from generate_series(1,3) as t(i); + +SELECT * FROM bug6051; + +WITH t1 AS ( DELETE FROM bug6051 RETURNING * ) +INSERT INTO bug6051 SELECT * FROM t1; + +SELECT * FROM bug6051; + +CREATE TEMP TABLE bug6051_2 (i int); + +CREATE RULE bug6051_ins AS ON INSERT TO bug6051 DO INSTEAD + INSERT INTO bug6051_2 + SELECT NEW.i; + +WITH t1 AS ( DELETE FROM bug6051 RETURNING * ) +INSERT INTO bug6051 SELECT * FROM t1; + +SELECT * FROM bug6051; +SELECT * FROM bug6051_2; + +-- a truly recursive CTE in the same list +WITH RECURSIVE t(a) AS ( + SELECT 0 + UNION ALL + SELECT a+1 FROM t WHERE a+1 < 5 +), t2 as ( + INSERT INTO y + SELECT * FROM t RETURNING * +) +SELECT * FROM t2 JOIN y USING (a) ORDER BY a; + +SELECT * FROM y; + +-- data-modifying WITH in a modifying statement +WITH t AS ( + DELETE FROM y + WHERE a <= 10 + RETURNING * +) +INSERT INTO y SELECT -a FROM t RETURNING *; + +SELECT * FROM y; + +-- check that WITH query is run to completion even if outer query isn't +WITH t AS ( + UPDATE y SET a = a * 100 RETURNING * +) +SELECT * FROM t LIMIT 10; + +SELECT * FROM y; + +-- data-modifying WITH containing INSERT...ON CONFLICT DO UPDATE +CREATE TABLE withz AS SELECT i AS k, (i || ' v')::text v FROM generate_series(1, 16, 3) i; +ALTER TABLE withz ADD UNIQUE (k); + +WITH t AS ( + INSERT INTO withz SELECT i, 'insert' + FROM generate_series(0, 16) i + ON CONFLICT (k) DO UPDATE SET v = withz.v || ', now update' + RETURNING * +) +SELECT * FROM t JOIN y ON t.k = y.a ORDER BY a, k; + +-- Test EXCLUDED.* reference within CTE +WITH aa AS ( + INSERT INTO withz VALUES(1, 5) ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v + WHERE withz.k != EXCLUDED.k + RETURNING * +) +SELECT * FROM aa; + +-- New query/snapshot demonstrates side-effects of previous query. +SELECT * FROM withz ORDER BY k; + +-- +-- Ensure subqueries within the update clause work, even if they +-- reference outside values +-- +WITH aa AS (SELECT 1 a, 2 b) +INSERT INTO withz VALUES(1, 'insert') +ON CONFLICT (k) DO UPDATE SET v = (SELECT b || ' update' FROM aa WHERE a = 1 LIMIT 1); +WITH aa AS (SELECT 1 a, 2 b) +INSERT INTO withz VALUES(1, 'insert') +ON CONFLICT (k) DO UPDATE SET v = ' update' WHERE withz.k = (SELECT a FROM aa); +WITH aa AS (SELECT 1 a, 2 b) +INSERT INTO withz VALUES(1, 'insert') +ON CONFLICT (k) DO UPDATE SET v = (SELECT b || ' update' FROM aa WHERE a = 1 LIMIT 1); +WITH aa AS (SELECT 'a' a, 'b' b UNION ALL SELECT 'a' a, 'b' b) +INSERT INTO withz VALUES(1, 'insert') +ON CONFLICT (k) DO UPDATE SET v = (SELECT b || ' update' FROM aa WHERE a = 'a' LIMIT 1); +WITH aa AS (SELECT 1 a, 2 b) +INSERT INTO withz VALUES(1, (SELECT b || ' insert' FROM aa WHERE a = 1 )) +ON CONFLICT (k) DO UPDATE SET v = (SELECT b || ' update' FROM aa WHERE a = 1 LIMIT 1); + +-- Update a row more than once, in different parts of a wCTE. That is +-- an allowed, presumably very rare, edge case, but since it was +-- broken in the past, having a test seems worthwhile. +WITH simpletup AS ( + SELECT 2 k, 'Green' v), +upsert_cte AS ( + INSERT INTO withz VALUES(2, 'Blue') ON CONFLICT (k) DO + UPDATE SET (k, v) = (SELECT k, v FROM simpletup WHERE simpletup.k = withz.k) + RETURNING k, v) +INSERT INTO withz VALUES(2, 'Red') ON CONFLICT (k) DO +UPDATE SET (k, v) = (SELECT k, v FROM upsert_cte WHERE upsert_cte.k = withz.k) +RETURNING k, v; + +DROP TABLE withz; + +-- check that run to completion happens in proper ordering + +TRUNCATE TABLE y; +INSERT INTO y SELECT generate_series(1, 3); +CREATE TEMPORARY TABLE yy (a INTEGER); + +WITH RECURSIVE t1 AS ( + INSERT INTO y SELECT * FROM y RETURNING * +), t2 AS ( + INSERT INTO yy SELECT * FROM t1 RETURNING * +) +SELECT 1; + +SELECT * FROM y; +SELECT * FROM yy; + +WITH RECURSIVE t1 AS ( + INSERT INTO yy SELECT * FROM t2 RETURNING * +), t2 AS ( + INSERT INTO y SELECT * FROM y RETURNING * +) +SELECT 1; + +SELECT * FROM y; +SELECT * FROM yy; + +-- triggers + +TRUNCATE TABLE y; +INSERT INTO y SELECT generate_series(1, 10); + +CREATE FUNCTION y_trigger() RETURNS trigger AS $$ +begin + raise notice 'y_trigger: a = %', new.a; + return new; +end; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER y_trig BEFORE INSERT ON y FOR EACH ROW + EXECUTE PROCEDURE y_trigger(); + +WITH t AS ( + INSERT INTO y + VALUES + (21), + (22), + (23) + RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +DROP TRIGGER y_trig ON y; + +CREATE TRIGGER y_trig AFTER INSERT ON y FOR EACH ROW + EXECUTE PROCEDURE y_trigger(); + +WITH t AS ( + INSERT INTO y + VALUES + (31), + (32), + (33) + RETURNING * +) +SELECT * FROM t LIMIT 1; + +SELECT * FROM y; + +DROP TRIGGER y_trig ON y; + +CREATE OR REPLACE FUNCTION y_trigger() RETURNS trigger AS $$ +begin + raise notice 'y_trigger'; + return null; +end; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER y_trig AFTER INSERT ON y FOR EACH STATEMENT + EXECUTE PROCEDURE y_trigger(); + +WITH t AS ( + INSERT INTO y + VALUES + (41), + (42), + (43) + RETURNING * +) +SELECT * FROM t; + +SELECT * FROM y; + +DROP TRIGGER y_trig ON y; +DROP FUNCTION y_trigger(); + +-- WITH attached to inherited UPDATE or DELETE + +CREATE TEMP TABLE parent ( id int, val text ); +CREATE TEMP TABLE child1 ( ) INHERITS ( parent ); +CREATE TEMP TABLE child2 ( ) INHERITS ( parent ); + +INSERT INTO parent VALUES ( 1, 'p1' ); +INSERT INTO child1 VALUES ( 11, 'c11' ),( 12, 'c12' ); +INSERT INTO child2 VALUES ( 23, 'c21' ),( 24, 'c22' ); + +WITH rcte AS ( SELECT sum(id) AS totalid FROM parent ) +UPDATE parent SET id = id + totalid FROM rcte; + +SELECT * FROM parent; + +WITH wcte AS ( INSERT INTO child1 VALUES ( 42, 'new' ) RETURNING id AS newid ) +UPDATE parent SET id = id + newid FROM wcte; + +SELECT * FROM parent; + +WITH rcte AS ( SELECT max(id) AS maxid FROM parent ) +DELETE FROM parent USING rcte WHERE id = maxid; + +SELECT * FROM parent; + +WITH wcte AS ( INSERT INTO child2 VALUES ( 42, 'new2' ) RETURNING id AS newid ) +DELETE FROM parent USING wcte WHERE id = newid; + +SELECT * FROM parent; + +-- check EXPLAIN VERBOSE for a wCTE with RETURNING + +EXPLAIN (VERBOSE, COSTS OFF) +WITH wcte AS ( INSERT INTO int8_tbl VALUES ( 42, 47 ) RETURNING q2 ) +DELETE FROM a USING wcte WHERE aa = q2; + +-- error cases + +-- data-modifying WITH tries to use its own output +WITH RECURSIVE t AS ( + INSERT INTO y + SELECT * FROM t +) +VALUES(FALSE); + +-- no RETURNING in a referenced data-modifying WITH +WITH t AS ( + INSERT INTO y VALUES(0) +) +SELECT * FROM t; + +-- data-modifying WITH allowed only at the top level +SELECT * FROM ( + WITH t AS (UPDATE y SET a=a+1 RETURNING *) + SELECT * FROM t +) ss; + +-- most variants of rules aren't allowed +CREATE RULE y_rule AS ON INSERT TO y WHERE a=0 DO INSTEAD DELETE FROM y; +WITH t AS ( + INSERT INTO y VALUES(0) +) +VALUES(FALSE); +DROP RULE y_rule ON y; + +-- check that parser lookahead for WITH doesn't cause any odd behavior +--create table foo (with baz); -- fail, WITH is a reserved word +--create table foo (with ordinality); -- fail, WITH is a reserved word +with ordinality as (select 1 as x) select * from ordinality; + +-- check sane response to attempt to modify CTE relation +WITH test AS (SELECT 42) INSERT INTO test VALUES (1); + +-- check response to attempt to modify table with same name as a CTE (perhaps +-- surprisingly it works, because CTEs don't hide tables from data-modifying +-- statements) +create temp table test (i int); +with test as (select 42) insert into test select * from test; +select * from test; +drop table test; From a34d8d372982d795dc7ed698ddae22fed81da5b7 Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 16:45:34 +0000 Subject: [PATCH 2/6] Address analyzer review findings - Resolve CTE names only where PostgreSQL permits a CTE source, so INSERT, UPDATE, DELETE, and SELECT INTO targets stay PHYSICAL - Fold identifiers and normalized SQL with ASCII-only lowercasing to match PostgreSQL's UTF-8 identifier folding - Put all sibling CTE names in scope under WITH RECURSIVE - Rework MERGE grammar as ordered WHEN clauses, accepting DO NOTHING, repeated WHEN clauses, and INSERT DEFAULT VALUES --- .../postgresql/parser/PostgreSQLParser.g4 | 27 ++++---- .../singlr/postgresql/AnalysisCollector.java | 32 ++++++++-- .../postgresql/PostgresQueryAnalyzer.java | 4 +- .../singlr/postgresql/NormalizationTest.java | 8 +++ .../postgresql/PostgresQueryAnalyzerTest.java | 62 +++++++++++++++++++ .../StatementClassificationTest.java | 3 + 6 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 index cb1dfe8..946cfd4 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -2842,22 +2842,21 @@ returning_clause // https://www.postgresql.org/docs/current/sql-merge.html mergestmt - : MERGE INTO? qualified_name alias_clause? USING (select_with_parens | qualified_name) alias_clause? ON a_expr ( - merge_insert_clause merge_update_clause? - | merge_update_clause merge_insert_clause? - ) merge_delete_clause? + : MERGE INTO? qualified_name alias_clause? + USING (select_with_parens | qualified_name) alias_clause? + ON a_expr merge_when_clause+ ; -merge_insert_clause - : WHEN NOT MATCHED (AND a_expr)? THEN? INSERT (OPEN_PAREN insert_column_list CLOSE_PAREN)? values_clause - ; - -merge_update_clause - : WHEN MATCHED (AND a_expr)? THEN? UPDATE SET set_clause_list - ; - -merge_delete_clause - : WHEN MATCHED THEN? DELETE_P +merge_when_clause + : WHEN MATCHED (AND a_expr)? THEN? ( + UPDATE SET set_clause_list + | DELETE_P + | DO NOTHING + ) + | WHEN NOT MATCHED (AND a_expr)? THEN? ( + INSERT (OPEN_PAREN insert_column_list CLOSE_PAREN)? (values_clause | DEFAULT VALUES) + | DO NOTHING + ) ; deletestmt diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java index 1b38d7b..9d22386 100644 --- a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -50,7 +50,6 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.Set; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; @@ -164,7 +163,8 @@ private void scanWithOwner( if (cte.preparablestmt().selectstmt() == null) { features.add(QueryFeature.WRITABLE_CTE); } - Set bodyScope = union(outerScope, names.subList(0, recursive ? i + 1 : i)); + List visibleNames = recursive ? names : names.subList(0, i); + Set bodyScope = union(outerScope, visibleNames); scan(cte.preparablestmt(), bodyScope); } Set fullScope = union(outerScope, names); @@ -198,7 +198,7 @@ private void inspect(ParseTree node, Set cteScope) { functions.add( new FunctionReference( null, - special.getStart().getText().toLowerCase(Locale.ROOT), + asciiLowercase(special.getStart().getText()), special.getStart().getLine(), special.getStart().getCharPositionInLine())); case PlsqlvariablenameContext parameter -> parameters.add(parameter.getText().substring(1)); @@ -260,12 +260,23 @@ private void addRelation(Qualified_nameContext relation, Set cteScope) { String name = parts.removeLast(); String schema = parts.isEmpty() ? null : String.join(".", parts); RelationReference.Kind kind = - schema == null && cteScope.contains(name) + schema == null && isCteSource(relation) && cteScope.contains(name) ? RelationReference.Kind.CTE : RelationReference.Kind.PHYSICAL; relations.add(new RelationReference(schema, name, aliasFor(relation), kind)); } + private static boolean isCteSource(Qualified_nameContext relation) { + if (!(relation.getParent() instanceof Relation_exprContext expression)) { + return false; + } + if (!(expression.getParent() instanceof Relation_expr_opt_aliasContext target)) { + return true; + } + return !(target.getParent() instanceof UpdatestmtContext + || target.getParent() instanceof DeletestmtContext); + } + private void addColumn(ColumnrefContext column) { List parts = new ArrayList<>(); parts.add(identifierText(column.colid())); @@ -319,7 +330,7 @@ private void addFunctionRelation(Func_tableContext functionRelation) { relations.add( new RelationReference( null, - windowless.getStart().getText().toLowerCase(Locale.ROOT), + asciiLowercase(windowless.getStart().getText()), alias, RelationReference.Kind.FUNCTION)); } @@ -458,6 +469,15 @@ private static String identifierText(ParserRuleContext identifier) { && raw.charAt(raw.length() - 1) == '"') { return raw.substring(3, raw.length() - 1).replace("\"\"", "\""); } - return raw.toLowerCase(Locale.ROOT); + return asciiLowercase(raw); + } + + static String asciiLowercase(String value) { + var folded = new StringBuilder(value.length()); + value + .codePoints() + .map(codePoint -> codePoint >= 'A' && codePoint <= 'Z' ? codePoint + 32 : codePoint) + .forEach(folded::appendCodePoint); + return folded.toString(); } } diff --git a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java index 5393594..a0d0aad 100644 --- a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java +++ b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java @@ -7,7 +7,6 @@ import ai.singlr.postgresql.parser.PostgreSQLLexer; import ai.singlr.postgresql.parser.PostgreSQLParser; -import java.util.Locale; import java.util.Set; import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.BaseErrorListener; @@ -170,7 +169,8 @@ private static String normalize(CommonTokenStream tokens) { normalized.append(isStringLiteral(previousType) && isStringLiteral(type) ? '\n' : ' '); } var text = token.getText(); - normalized.append(VERBATIM_TOKEN_TYPES.contains(type) ? text : text.toLowerCase(Locale.ROOT)); + normalized.append( + VERBATIM_TOKEN_TYPES.contains(type) ? text : AnalysisCollector.asciiLowercase(text)); previousType = type; } return normalized.toString(); diff --git a/src/test/java/ai/singlr/postgresql/NormalizationTest.java b/src/test/java/ai/singlr/postgresql/NormalizationTest.java index 9bb01d3..9cc3a9b 100644 --- a/src/test/java/ai/singlr/postgresql/NormalizationTest.java +++ b/src/test/java/ai/singlr/postgresql/NormalizationTest.java @@ -54,6 +54,14 @@ void shouldDifferOnSemanticChanges() { assertNotEquals(canonical, normalize("SELECT a FROM t WHERE x > 2")); } + @Test + @DisplayName("non-ascii identifier case changes the normalized form") + void shouldPreserveNonAsciiIdentifierCase() { + assertEquals("select marker from Таблица", normalize("SELECT marker FROM Таблица")); + assertNotEquals( + normalize("SELECT marker FROM Таблица"), normalize("SELECT marker FROM таблица")); + } + @Test @DisplayName("quoted identifier case changes the normalized form") void shouldDifferOnQuotedIdentifierCase() { diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java index 8b4a3cd..c3273c9 100644 --- a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -232,6 +232,68 @@ void shouldNotMatchSchemaQualifiedAsCte() { analysis.relations()); } + @Test + @DisplayName("CTE name never hides an insert target") + void shouldKeepInsertTargetPhysicalDespiteCteName() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH target AS (SELECT 1 AS id) INSERT INTO target SELECT id FROM target"); + + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("target:PHYSICAL", "target:CTE"), kinds); + } + + @Test + @DisplayName("CTE name never hides update and delete targets") + void shouldKeepUpdateAndDeleteTargetsPhysicalDespiteCteName() { + var update = + PostgresQueryAnalyzer.analyze( + "WITH target AS (SELECT 1 AS id) UPDATE target SET id = 2 FROM target"); + var updateKinds = update.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("target:PHYSICAL", "target:CTE"), updateKinds); + + var delete = + PostgresQueryAnalyzer.analyze( + "WITH target AS (SELECT 1 AS id) DELETE FROM target USING target t WHERE t.id = 1"); + var deleteKinds = delete.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("target:PHYSICAL", "target:CTE"), deleteKinds); + } + + @Test + @DisplayName("CTE name never hides a select into target") + void shouldKeepSelectIntoTargetPhysicalDespiteCteName() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH target AS (SELECT 1 AS id) SELECT id INTO target FROM target"); + + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("target:PHYSICAL", "target:CTE"), kinds); + } + + @Test + @DisplayName("recursive CTE sees later sibling CTE") + void shouldScopeForwardReferenceInRecursiveWith() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH RECURSIVE x AS (SELECT * FROM y), y AS (SELECT 1) SELECT * FROM x"); + + assertEquals( + List.of( + new RelationReference(null, "y", null, RelationReference.Kind.CTE), + new RelationReference(null, "x", null, RelationReference.Kind.CTE)), + analysis.relations()); + } + + @Test + @DisplayName("identifier folding is ascii-only so distinct unicode names stay distinct") + void shouldFoldOnlyAsciiIdentifierCase() { + var upper = PostgresQueryAnalyzer.analyze("SELECT marker FROM Таблица"); + var lower = PostgresQueryAnalyzer.analyze("SELECT marker FROM таблица"); + + assertEquals("Таблица", upper.relations().getFirst().name()); + assertEquals("таблица", lower.relations().getFirst().name()); + } + @Test @DisplayName("functions are captured in select, where, join, group, window and from") void shouldCaptureFunctionsEverywhere() { diff --git a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java index 1292dd9..989bc89 100644 --- a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java +++ b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java @@ -26,6 +26,9 @@ INSERT INTO t VALUES (1) | INSERT UPDATE t SET a = 1 | UPDATE DELETE FROM t | DELETE MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = 1 WHEN NOT MATCHED THEN INSERT VALUES (1) | MERGE + MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DO NOTHING | MERGE + MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DO NOTHING | MERGE + MERGE INTO t USING s ON t.id = s.id WHEN MATCHED AND t.a > 1 THEN UPDATE SET a = 2 WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT DEFAULT VALUES | MERGE CREATE TABLE t (id int) | DDL CREATE INDEX idx ON t (id) | DDL CREATE VIEW v AS SELECT 1 | DDL From 8c00a0d2f4ab5ba099fcb1d115112fe93211e557 Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 17:12:50 +0000 Subject: [PATCH 3/6] Address second-round analyzer review findings - Reject psql meta-commands: the META lexer mode silently turned backslash commands into statement separators, so an allowed SELECT could smuggle \! or \copy past analysis; a backslash now lexes as ErrorCharacter and fails closed (corpus files updated accordingly) - Reject Unicode-escaped identifiers (U&"...") at lex time: their effective name differs from their spelling, so name-based policies could be bypassed with an equivalent spelling - Collect JOIN USING names as column references - Report DROP TABLE / VIEW / MATERIALIZED VIEW / FOREIGN TABLE targets as physical relations - Lex nested block comments iteratively with a depth counter: the recursive rule needed ~1s for 5 KB of 1250-deep nesting and overflowed the stack near 1500; now 6 ms at depth 20000 - Accept PostgreSQL newline string continuation ('a'\n'b'), matching scan.l semantics verified against PostgreSQL 16: line comments count as separating whitespace, block comments and dollar-quoted strings do not, same-line adjacency stays an error --- .../postgresql/parser/PostgreSQLLexer.g4 | 44 +++++-------- .../postgresql/parser/PostgreSQLParser.g4 | 5 +- .../singlr/postgresql/AnalysisCollector.java | 40 +++++++++-- .../postgresql/PostgresQueryAnalyzer.java | 12 +++- .../parser/PostgreSQLLexerBase.java | 20 ++++-- .../parser/PostgreSQLParserBase.java | 26 ++++++++ .../singlr/postgresql/HostileInputTest.java | 45 +++++++++++++ .../postgresql/PostgresQueryAnalyzerTest.java | 66 +++++++++++++++++++ .../singlr/postgresql/UpstreamCorpusTest.java | 4 ++ .../resources/postgresql-corpus/insert.sql | 13 ---- .../resources/postgresql-corpus/limit.sql | 4 -- .../postgresql-corpus/transactions.sql | 48 +++++++------- .../resources/postgresql-corpus/update.sql | 3 - src/test/resources/postgresql-corpus/with.sql | 1 - 14 files changed, 246 insertions(+), 85 deletions(-) diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 index f23b70f..48e7e45 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 @@ -43,6 +43,12 @@ options { caseInsensitive = true; } +// BlockComment is assembled across BlockCommentMode rules via more(), so no single rule +// defines it; EndBlockComment() stamps this type on the finished token. +tokens { + BlockComment +} + // Insert here @header for C++ lexer. @@ -1355,32 +1361,16 @@ Newline: ('\r' '\n'? | '\n') -> channel (HIDDEN); LineComment: '--' ~ [\r\n]* -> channel (HIDDEN); -BlockComment: - ('/*' ('/'* BlockComment | ~ [/*] | '/'+ ~ [/*] | '*'+ ~ [/*])* '*'* '*/') -> channel (HIDDEN) -; +// Nested block comments are scanned with an explicit mode and depth counter so lexing stays +// linear-time and constant-stack on adversarial nesting. An unterminated comment becomes an +// ErrorCharacter token, which the parser rejects. -UnterminatedBlockComment: - '/*' ( - '/'* BlockComment - | // these characters are not part of special sequences in a block comment - ~ [/*] - | // handle / or * characters which are not part of /* or */ and do not appear at the end of the file - ('/'+ ~ [/*] | '*'+ ~ [/*]) - )* - // Handle the case of / or * characters at the end of the file, or a nested unterminated block comment - ('/'+ | '*'+ | '/'* UnterminatedBlockComment)? - // Optional assertion to make sure this rule is working as intended - {this.UnterminatedBlockCommentDebugAssert();} -; +BlockCommentStart: '/*' {this.commentDepth = 1;} -> pushMode(BlockCommentMode), more; // -// META-COMMANDS - -// - -// http://www.postgresql.org/docs/9.3/static/app-psql.html - -MetaCommand: '\\' -> pushMode(META), more ; +// psql meta-commands (backslash commands) are deliberately NOT recognized: they are not +// PostgreSQL SQL, and treating them as statement separators would let executable commands +// pass structural analysis unreported. A backslash lexes as ErrorCharacter and fails closed. // @@ -1471,6 +1461,8 @@ DollarText: EndDollarStringConstant: ('$' Tag? '$') {this.IsTag()}? {this.PopTag();} -> popMode; -mode META; -MetaSemi : {this.IsSemiColon()}? ';' -> type(SEMI), popMode ; -MetaOther : ~[;\r\n\\"] .*? ('\\\\' | [\r\n]+) -> type(SEMI), popMode ; +mode BlockCommentMode; +BlockCommentNested: '/*' {this.commentDepth++;} -> more; +BlockCommentEnd: '*/' {this.EndBlockComment();}; +BlockCommentChar: . -> more; +BlockCommentEof: EOF -> type(ErrorCharacter), popMode; diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 index 946cfd4..b838a6d 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -4355,8 +4355,11 @@ iconst | HexadecimalIntegral ; +// PostgreSQL concatenates adjacent string constants separated only by whitespace containing +// at least one newline, where the continuation is a plain quoted string. Same-line adjacency +// stays a syntax error. sconst - : anysconst uescape_? + : anysconst ({this.IsStringContinuation()}? StringConstant)* uescape_? ; anysconst diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java index 9d22386..c326cb5 100644 --- a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -10,6 +10,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.ColumnrefContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Common_table_exprContext; import ai.singlr.postgresql.parser.PostgreSQLParser.DeletestmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.DropstmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.For_locking_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Func_alias_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Func_applicationContext; @@ -21,6 +22,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.Insert_targetContext; import ai.singlr.postgresql.parser.PostgreSQLParser.InsertstmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Into_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Join_qualContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Locked_rels_listContext; import ai.singlr.postgresql.parser.PostgreSQLParser.MergestmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Over_clauseContext; @@ -75,6 +77,9 @@ final class AnalysisCollector { "revokerolestmt", "importforeignschemastmt"); + private static final Set RELATION_DROP_TYPES = + Set.of("table", "view", "materializedview", "foreigntable"); + private final List relations = new ArrayList<>(); private final List columns = new ArrayList<>(); private final List functions = new ArrayList<>(); @@ -192,6 +197,7 @@ private static Set union(Set scope, List names) { private void inspect(ParseTree node, Set cteScope) { switch (node) { case Qualified_nameContext relation -> addRelation(relation, cteScope); + case DropstmtContext drop -> addDroppedRelations(drop); case ColumnrefContext column -> addColumn(column); case Func_applicationContext call -> addFunction(call); case Func_expr_common_subexprContext special -> @@ -211,6 +217,11 @@ private void inspect(ParseTree node, Set cteScope) { columns.add(new ColumnReference(null, identifierText(item.colid()))); case Set_targetContext target -> columns.add(new ColumnReference(null, identifierText(target.colid()))); + case Join_qualContext join when join.USING() != null -> + join.name_list() + .name() + .forEach( + name -> columns.add(new ColumnReference(null, identifierText(name.colid())))); case Select_with_parensContext subquery -> { var parent = subquery.getParent(); if (!(parent instanceof SelectstmtContext @@ -266,6 +277,30 @@ private void addRelation(Qualified_nameContext relation, Set cteScope) { relations.add(new RelationReference(schema, name, aliasFor(relation), kind)); } + private void addDroppedRelations(DropstmtContext drop) { + if (drop.object_type_any_name() == null + || drop.any_name_list_() == null + || !RELATION_DROP_TYPES.contains(asciiLowercase(drop.object_type_any_name().getText()))) { + return; + } + for (var anyName : drop.any_name_list_().any_name()) { + List parts = new ArrayList<>(); + parts.add(identifierText(anyName.colid())); + if (anyName.attrs() != null) { + for (var attr : anyName.attrs().attr_name()) { + parts.add(identifierText(attr)); + } + } + String name = parts.removeLast(); + relations.add( + new RelationReference( + parts.isEmpty() ? null : String.join(".", parts), + name, + null, + RelationReference.Kind.PHYSICAL)); + } + } + private static boolean isCteSource(Qualified_nameContext relation) { if (!(relation.getParent() instanceof Relation_exprContext expression)) { return false; @@ -464,11 +499,6 @@ private static String identifierText(ParserRuleContext identifier) { if (raw.length() >= 2 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') { return raw.substring(1, raw.length() - 1).replace("\"\"", "\""); } - if (raw.length() >= 4 - && (raw.startsWith("U&\"") || raw.startsWith("u&\"")) - && raw.charAt(raw.length() - 1) == '"') { - return raw.substring(3, raw.length() - 1).replace("\"\"", "\""); - } return asciiLowercase(raw); } diff --git a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java index a0d0aad..47cfbfc 100644 --- a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java +++ b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java @@ -31,6 +31,11 @@ * #MAX_NESTING_DEPTH} levels of bracket nesting, which together bound parse time and memory on * hostile input. SQL text and literal content never appear in exception messages and are never * logged. + * + *

Two lexically valid PostgreSQL forms are rejected outright because they cannot be analyzed + * faithfully: psql meta-commands (backslash commands), which are not SQL and could smuggle + * executable commands past analysis, and Unicode-escaped identifiers ({@code U&"..."}), whose + * effective name differs from their spelling and could evade name-based policies. */ public final class PostgresQueryAnalyzer { @@ -46,7 +51,6 @@ public final class PostgresQueryAnalyzer { private static final Set VERBATIM_TOKEN_TYPES = Set.of( PostgreSQLLexer.QuotedIdentifier, - PostgreSQLLexer.UnicodeQuotedIdentifier, PostgreSQLLexer.StringConstant, PostgreSQLLexer.UnicodeEscapeStringConstant, PostgreSQLLexer.EscapeStringConstant, @@ -114,6 +118,12 @@ private static void checkBounds(CommonTokenStream tokens) { int depth = 0; for (Token token : tokens.getTokens()) { int type = token.getType(); + if (type == PostgreSQLLexer.UnicodeQuotedIdentifier) { + throw new QueryAnalysisException( + "unicode escaped identifiers are not supported", + token.getLine(), + token.getCharPositionInLine()); + } if (type == PostgreSQLLexer.OPEN_PAREN || type == PostgreSQLLexer.OPEN_BRACKET) { depth++; if (depth > MAX_NESTING_DEPTH) { diff --git a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java index 779d0ea..ebde47e 100644 --- a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java +++ b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java @@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal public abstract class PostgreSQLLexerBase extends Lexer { protected final Stack tags = new Stack<>(); + protected int commentDepth; protected PostgreSQLLexerBase(CharStream input) { super(input); @@ -47,8 +48,18 @@ public void PopTag() { tags.pop(); } - public void UnterminatedBlockCommentDebugAssert() { - //Debug.Assert(InputStream.LA(1) == -1 /*EOF*/); + // scim-sql local modification: nested block comments are lexed iteratively with a depth + // counter instead of the upstream recursive rule, which was quadratic-time and could + // overflow the stack on adversarial nesting. + public void EndBlockComment() { + commentDepth--; + if (commentDepth == 0) { + setType(PostgreSQLLexer.BlockComment); + setChannel(HIDDEN); + popMode(); + } else { + more(); + } } public boolean CheckLaMinus() { @@ -84,9 +95,4 @@ public boolean CheckIfUtf32Letter() { } return Character.isLetter(c[0]); } - - public boolean IsSemiColon() - { - return ';' == (char)getInputStream().LA(1); - } } diff --git a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java index dc0b557..4692f18 100644 --- a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java +++ b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLParserBase.java @@ -127,6 +127,32 @@ public PostgreSQLParser GetPostgreSQLParser(String script) { return parser; } + // scim-sql local modification: implements PostgreSQL's scanner-level string continuation + // (scan.l {quotecontinue}): a quoted constant continues a preceding quote-style constant + // only when separated by horizontal whitespace and line comments with at least one newline. + // Block comments do not qualify, and dollar-quoted strings never continue. + public boolean IsStringContinuation() { + var stream = (CommonTokenStream) getInputStream(); + var previous = stream.LT(-1); + if (previous == null || previous.getType() == PostgreSQLLexer.EndDollarStringConstant) { + return false; + } + var hidden = stream.getHiddenTokensToLeft(stream.LT(1).getTokenIndex()); + if (hidden == null) { + return false; + } + var sawNewline = false; + for (var token : hidden) { + var type = token.getType(); + if (type == PostgreSQLLexer.Newline) { + sawNewline = true; + } else if (type != PostgreSQLLexer.Whitespace && type != PostgreSQLLexer.LineComment) { + return false; + } + } + return sawNewline; + } + public boolean OnlyAcceptableOps() { var c = ((CommonTokenStream)this.getInputStream()).LT(1); diff --git a/src/test/java/ai/singlr/postgresql/HostileInputTest.java b/src/test/java/ai/singlr/postgresql/HostileInputTest.java index 6583a21..528e9ca 100644 --- a/src/test/java/ai/singlr/postgresql/HostileInputTest.java +++ b/src/test/java/ai/singlr/postgresql/HostileInputTest.java @@ -146,6 +146,51 @@ void shouldHandleNestedBlockComments() { assertEquals(1, analysis.statementCount()); } + @Test + @DisplayName("deeply nested block comments lex in linear time and constant stack") + void shouldHandleDeepBlockCommentNesting() { + var sql = "SELECT 1 " + "/*".repeat(5_000) + "x" + "*/".repeat(5_000); + assertTimeoutPreemptively( + Duration.ofSeconds(5), + () -> assertEquals(1, PostgresQueryAnalyzer.analyze(sql).statementCount())); + } + + @Test + @DisplayName("unterminated nested block comment is rejected") + void shouldRejectUnterminatedBlockComment() { + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 1 /* /* x */")); + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 1 /*")); + } + + @Test + @DisplayName("psql meta-commands are rejected, never treated as statement separators") + void shouldRejectPsqlMetaCommands() { + for (var sql : + new String[] { + "SELECT 1; \\! id\n", + "SELECT 1;\n\\echo pwned\n", + "\\copy secrets TO '/tmp/out'", + "SELECT 1\\; SELECT 2;" + }) { + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(sql), sql); + } + } + + @Test + @DisplayName("unicode escaped identifiers are rejected, unicode escaped strings parse") + void shouldRejectUnicodeEscapedIdentifiers() { + var exception = + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT * FROM U&\"d\\0061t\"")); + assertTrue(exception.reason().contains("unicode")); + assertThrows( + QueryAnalysisException.class, + () -> PostgresQueryAnalyzer.analyze("SELECT * FROM U&\"d!0061t\" UESCAPE '!'")); + assertEquals(1, PostgresQueryAnalyzer.analyze("SELECT U&'\\0061'").statementCount()); + } + @Test @DisplayName("dollar tag confusion does not break statement counting") void shouldHandleDollarTagTricks() { diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java index c3273c9..135fb63 100644 --- a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -415,4 +416,69 @@ void shouldHandleArraySubscript() { assertEquals(List.of(new ColumnReference(null, "tags")), analysis.columns()); } + + @Test + @DisplayName("join using reports the join columns") + void shouldReportJoinUsingColumns() { + var analysis = + PostgresQueryAnalyzer.analyze("SELECT a.id FROM a JOIN b USING (tenant_id, secret)"); + + assertEquals( + List.of( + new ColumnReference("a", "id"), + new ColumnReference(null, "tenant_id"), + new ColumnReference(null, "secret")), + analysis.columns()); + } + + @Test + @DisplayName("drop table and drop view report their target relations") + void shouldReportDroppedRelations() { + var dropTable = PostgresQueryAnalyzer.analyze("DROP TABLE private.users, audit"); + + assertEquals(StatementKind.DDL, dropTable.statementKind()); + assertEquals( + List.of( + new RelationReference("private", "users", null, RelationReference.Kind.PHYSICAL), + new RelationReference(null, "audit", null, RelationReference.Kind.PHYSICAL)), + dropTable.relations()); + assertEquals( + List.of(new RelationReference("private", "v", null, RelationReference.Kind.PHYSICAL)), + PostgresQueryAnalyzer.analyze("DROP VIEW IF EXISTS private.v").relations()); + assertEquals( + List.of(new RelationReference(null, "m", null, RelationReference.Kind.PHYSICAL)), + PostgresQueryAnalyzer.analyze("DROP MATERIALIZED VIEW m").relations()); + assertEquals( + List.of(new RelationReference(null, "f", null, RelationReference.Kind.PHYSICAL)), + PostgresQueryAnalyzer.analyze("DROP FOREIGN TABLE f").relations()); + } + + @Test + @DisplayName("drop of non-relation objects reports no relations") + void shouldNotReportNonRelationDrops() { + assertEquals(List.of(), PostgresQueryAnalyzer.analyze("DROP COLLATION c").relations()); + assertEquals(List.of(), PostgresQueryAnalyzer.analyze("DROP INDEX idx").relations()); + assertEquals(List.of(), PostgresQueryAnalyzer.analyze("DROP SCHEMA s").relations()); + } + + @Test + @DisplayName("string constants separated by a newline concatenate as in postgresql") + void shouldAcceptNewlineConcatenatedStrings() { + assertEquals(1, PostgresQueryAnalyzer.analyze("SELECT 'a'\n'b'").statementCount()); + assertEquals( + 1, PostgresQueryAnalyzer.analyze("SELECT 'a' -- note\n 'b'\n'c'").statementCount()); + assertEquals( + 1, PostgresQueryAnalyzer.analyze("SELECT U&'d!0061t'\n'x' UESCAPE '!'").statementCount()); + } + + @Test + @DisplayName("string adjacency without a plain newline separation stays a syntax error") + void shouldRejectSameLineStringAdjacency() { + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 'a' 'b'")); + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT 'a' /*\n*/ 'b'")); + assertThrows( + QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze("SELECT $$a$$\n'b'")); + } } diff --git a/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java b/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java index 469266b..ef8a46a 100644 --- a/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java +++ b/src/test/java/ai/singlr/postgresql/UpstreamCorpusTest.java @@ -19,6 +19,10 @@ * PostgreSQL regression examples vendored from ANTLR grammars-v4 at commit * 76093c04af6a51f38a67d14f7e71ff0a9b4400da (sql/postgresql/examples). Each file must analyze end to * end; failures indicate a regression in the vendored grammar or the analyzer. + * + *

Local modification: psql meta-command lines ({@code \d+}, {@code \set}) were removed and + * client-side {@code \;} separators replaced with plain {@code ;}, because the analyzer + * deliberately rejects psql meta-commands as non-SQL. */ @DisplayName("Upstream regression corpus") class UpstreamCorpusTest { diff --git a/src/test/resources/postgresql-corpus/insert.sql b/src/test/resources/postgresql-corpus/insert.sql index 67c263c..6c75264 100644 --- a/src/test/resources/postgresql-corpus/insert.sql +++ b/src/test/resources/postgresql-corpus/insert.sql @@ -79,7 +79,6 @@ create rule irule2 as on insert to inserttest2 do also create rule irule3 as on insert to inserttest2 do also insert into inserttest (f4[1].if1, f4[1].if2[2]) select new.f1, new.f2; -\d+ inserttest2 drop table inserttest2; drop table inserttest; @@ -278,7 +277,6 @@ from hash_parted order by part; -- test \d+ output on a table which has both partitioned and unpartitioned -- partitions -\d+ list_parted -- cleanup drop table range_parted, list_parted; @@ -288,7 +286,6 @@ drop table hash_parted; -- including null create table list_parted (a int) partition by list (a); create table part_default partition of list_parted default; -\d+ part_default insert into part_default values (null); insert into part_default values (1); insert into part_default values (-1); @@ -559,7 +556,6 @@ copy donothingbrtrig_test from stdout; /* 1 baz 2 qux -\. */ select tableoid::regclass, * from donothingbrtrig_test; @@ -578,15 +574,6 @@ create table mcrparted6_common_ge_10 partition of mcrparted for values from ('co create table mcrparted7_gt_common_lt_d partition of mcrparted for values from ('common', maxvalue) to ('d', minvalue); create table mcrparted8_ge_d partition of mcrparted for values from ('d', minvalue) to (maxvalue, maxvalue); -\d+ mcrparted -\d+ mcrparted1_lt_b -\d+ mcrparted2_b -\d+ mcrparted3_c_to_common -\d+ mcrparted4_common_lt_0 -\d+ mcrparted5_common_0_to_10 -\d+ mcrparted6_common_ge_10 -\d+ mcrparted7_gt_common_lt_d -\d+ mcrparted8_ge_d insert into mcrparted values ('aaa', 0), ('b', 0), ('bz', 10), ('c', -10), ('comm', -10), ('common', -10), ('common', 0), ('common', 10), diff --git a/src/test/resources/postgresql-corpus/limit.sql b/src/test/resources/postgresql-corpus/limit.sql index 81618bc..6d4da6c 100644 --- a/src/test/resources/postgresql-corpus/limit.sql +++ b/src/test/resources/postgresql-corpus/limit.sql @@ -170,18 +170,14 @@ SELECT ''::text AS two, unique1, unique2, stringu1 -- test ruleutils CREATE VIEW limit_thousand_v_1 AS SELECT thousand FROM onek WHERE thousand < 995 ORDER BY thousand FETCH FIRST 5 ROWS WITH TIES OFFSET 10; -\d+ limit_thousand_v_1 CREATE VIEW limit_thousand_v_2 AS SELECT thousand FROM onek WHERE thousand < 995 ORDER BY thousand OFFSET 10 FETCH FIRST 5 ROWS ONLY; -\d+ limit_thousand_v_2 CREATE VIEW limit_thousand_v_3 AS SELECT thousand FROM onek WHERE thousand < 995 ORDER BY thousand FETCH FIRST NULL ROWS WITH TIES; -- fails CREATE VIEW limit_thousand_v_3 AS SELECT thousand FROM onek WHERE thousand < 995 ORDER BY thousand FETCH FIRST (NULL+1) ROWS WITH TIES; -\d+ limit_thousand_v_3 CREATE VIEW limit_thousand_v_4 AS SELECT thousand FROM onek WHERE thousand < 995 ORDER BY thousand FETCH FIRST NULL ROWS ONLY; -\d+ limit_thousand_v_4 -- leave these views -- use of random() is to keep planner from folding the expressions together explain (verbose, costs off) diff --git a/src/test/resources/postgresql-corpus/transactions.sql b/src/test/resources/postgresql-corpus/transactions.sql index afd0039..03767f8 100644 --- a/src/test/resources/postgresql-corpus/transactions.sql +++ b/src/test/resources/postgresql-corpus/transactions.sql @@ -494,80 +494,80 @@ DROP TABLE abc; create temp table i_table (f1 int); -- psql will show only the last result in a multi-statement Query -SELECT 1\; SELECT 2\; SELECT 3; +SELECT 1; SELECT 2; SELECT 3; select * from i_table; rollback; -- we are not in a transaction at this point -- can use regular begin/commit/rollback within a single Query -begin\; insert into i_table values(3)\; commit; +begin; insert into i_table values(3); commit; rollback; -- we are not in a transaction at this point -begin\; insert into i_table values(4)\; rollback; +begin; insert into i_table values(4); rollback; rollback; -- we are not in a transaction at this point -- begin converts implicit transaction into a regular one that -- can extend past the end of the Query -select 1\; begin\; insert into i_table values(5); +select 1; begin; insert into i_table values(5); commit; -select 1\; begin\; insert into i_table values(6); +select 1; begin; insert into i_table values(6); rollback; -- commit in implicit-transaction state commits but issues a warning. -insert into i_table values(7)\; commit\; insert into i_table values(8)\; select 1/0; +insert into i_table values(7); commit; insert into i_table values(8); select 1/0; -- similarly, rollback aborts but issues a warning. -insert into i_table values(9)\; rollback\; select 2; +insert into i_table values(9); rollback; select 2; select * from i_table; rollback; -- we are not in a transaction at this point -- implicit transaction block is still a transaction block, for e.g. VACUUM -SELECT 1\; VACUUM; -SELECT 1\; COMMIT\; VACUUM; +SELECT 1; VACUUM; +SELECT 1; COMMIT; VACUUM; -- we disallow savepoint-related commands in implicit-transaction state -SELECT 1\; SAVEPOINT sp; -SELECT 1\; COMMIT\; SAVEPOINT sp; -ROLLBACK TO SAVEPOINT sp\; SELECT 2; -SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3; +SELECT 1; SAVEPOINT sp; +SELECT 1; COMMIT; SAVEPOINT sp; +ROLLBACK TO SAVEPOINT sp; SELECT 2; +SELECT 2; RELEASE SAVEPOINT sp; SELECT 3; -- but this is OK, because the BEGIN converts it to a regular xact -SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT; +SELECT 1; BEGIN; SAVEPOINT sp; ROLLBACK TO SAVEPOINT sp; COMMIT; -- Tests for AND CHAIN in implicit transaction blocks -SET TRANSACTION READ ONLY\; COMMIT AND CHAIN; -- error +SET TRANSACTION READ ONLY; COMMIT AND CHAIN; -- error SHOW transaction_read_only; -SET TRANSACTION READ ONLY\; ROLLBACK AND CHAIN; -- error +SET TRANSACTION READ ONLY; ROLLBACK AND CHAIN; -- error SHOW transaction_read_only; CREATE TABLE abc (a int); -- COMMIT/ROLLBACK + COMMIT/ROLLBACK AND CHAIN -INSERT INTO abc VALUES (7)\; COMMIT\; INSERT INTO abc VALUES (8)\; COMMIT AND CHAIN; -- 7 commit, 8 error -INSERT INTO abc VALUES (9)\; ROLLBACK\; INSERT INTO abc VALUES (10)\; ROLLBACK AND CHAIN; -- 9 rollback, 10 error +INSERT INTO abc VALUES (7); COMMIT; INSERT INTO abc VALUES (8); COMMIT AND CHAIN; -- 7 commit, 8 error +INSERT INTO abc VALUES (9); ROLLBACK; INSERT INTO abc VALUES (10); ROLLBACK AND CHAIN; -- 9 rollback, 10 error -- COMMIT/ROLLBACK AND CHAIN + COMMIT/ROLLBACK -INSERT INTO abc VALUES (11)\; COMMIT AND CHAIN\; INSERT INTO abc VALUES (12)\; COMMIT; -- 11 error, 12 not reached -INSERT INTO abc VALUES (13)\; ROLLBACK AND CHAIN\; INSERT INTO abc VALUES (14)\; ROLLBACK; -- 13 error, 14 not reached +INSERT INTO abc VALUES (11); COMMIT AND CHAIN; INSERT INTO abc VALUES (12); COMMIT; -- 11 error, 12 not reached +INSERT INTO abc VALUES (13); ROLLBACK AND CHAIN; INSERT INTO abc VALUES (14); ROLLBACK; -- 13 error, 14 not reached -- START TRANSACTION + COMMIT/ROLLBACK AND CHAIN -START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (15)\; COMMIT AND CHAIN; -- 15 ok +START TRANSACTION ISOLATION LEVEL REPEATABLE READ; INSERT INTO abc VALUES (15); COMMIT AND CHAIN; -- 15 ok SHOW transaction_isolation; -- transaction is active at this point COMMIT; -START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (16)\; ROLLBACK AND CHAIN; -- 16 ok +START TRANSACTION ISOLATION LEVEL REPEATABLE READ; INSERT INTO abc VALUES (16); ROLLBACK AND CHAIN; -- 16 ok SHOW transaction_isolation; -- transaction is active at this point ROLLBACK; -- START TRANSACTION + COMMIT/ROLLBACK + COMMIT/ROLLBACK AND CHAIN -START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (17)\; COMMIT\; INSERT INTO abc VALUES (18)\; COMMIT AND CHAIN; -- 17 commit, 18 error +START TRANSACTION ISOLATION LEVEL REPEATABLE READ; INSERT INTO abc VALUES (17); COMMIT; INSERT INTO abc VALUES (18); COMMIT AND CHAIN; -- 17 commit, 18 error SHOW transaction_isolation; -- out of transaction block -START TRANSACTION ISOLATION LEVEL REPEATABLE READ\; INSERT INTO abc VALUES (19)\; ROLLBACK\; INSERT INTO abc VALUES (20)\; ROLLBACK AND CHAIN; -- 19 rollback, 20 error +START TRANSACTION ISOLATION LEVEL REPEATABLE READ; INSERT INTO abc VALUES (19); ROLLBACK; INSERT INTO abc VALUES (20); ROLLBACK AND CHAIN; -- 19 rollback, 20 error SHOW transaction_isolation; -- out of transaction block SELECT * FROM abc ORDER BY 1; diff --git a/src/test/resources/postgresql-corpus/update.sql b/src/test/resources/postgresql-corpus/update.sql index 5953d4d..3f36374 100644 --- a/src/test/resources/postgresql-corpus/update.sql +++ b/src/test/resources/postgresql-corpus/update.sql @@ -181,8 +181,6 @@ ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_100_200 FOR VALUES FROM (100) CREATE TABLE part_c_1_100 (e varchar, d int, c numeric, b bigint, a text); ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_1_100 FOR VALUES FROM (1) TO (100); -\set init_range_parted 'truncate range_parted; insert into range_parted VALUES (''a'', 1, 1, 1), (''a'', 10, 200, 1), (''b'', 12, 96, 1), (''b'', 13, 97, 2), (''b'', 15, 105, 16), (''b'', 17, 105, 19)' -\set show_data 'select tableoid::regclass::text COLLATE "C" partname, * from range_parted ORDER BY 1, 2, 3, 4, 5, 6' ----:init_range_parted; ----:show_data; @@ -461,7 +459,6 @@ DROP TRIGGER d15_insert_trig ON part_d_15_20; -- Creating default partition for range ----:init_range_parted; create table part_def partition of range_parted default; -\d+ part_def insert into range_parted values ('c', 9); -- ok update part_def set a = 'd' where a = 'c'; diff --git a/src/test/resources/postgresql-corpus/with.sql b/src/test/resources/postgresql-corpus/with.sql index 213c19e..9b4077b 100644 --- a/src/test/resources/postgresql-corpus/with.sql +++ b/src/test/resources/postgresql-corpus/with.sql @@ -211,7 +211,6 @@ UNION ALL ) SELECT sum(n) FROM t; -\d+ sums_1_100 -- corner case in which sub-WITH gets initialized first with recursive q as ( From 190edbd0b035d92c3af3b1328d4cb431d69dc78d Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 17:27:01 +0000 Subject: [PATCH 4/6] Address third-round analyzer review findings - Reject identifiers longer than 63 UTF-8 bytes (post-unescape) so the analyzer never reports a name PostgreSQL would silently truncate to a different effective identifier - Collect bare ON CONFLICT arbiter and CREATE INDEX columns via the colid alternative of index_elem - Accept WITH ... MERGE: grammar allows a leading with_clause_, MERGE is a with-clause owner for CTE scoping, and the USING relation (but never the MERGE target) can resolve as a CTE source --- .../postgresql/parser/PostgreSQLParser.g4 | 2 +- .../singlr/postgresql/AnalysisCollector.java | 7 ++++ .../postgresql/PostgresQueryAnalyzer.java | 29 ++++++++++++-- .../singlr/postgresql/HostileInputTest.java | 33 ++++++++++++++++ .../postgresql/PostgresQueryAnalyzerTest.java | 38 +++++++++++++++++++ .../StatementClassificationTest.java | 1 + 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 index b838a6d..6c34e9e 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -2842,7 +2842,7 @@ returning_clause // https://www.postgresql.org/docs/current/sql-merge.html mergestmt - : MERGE INTO? qualified_name alias_clause? + : with_clause_? MERGE INTO? qualified_name alias_clause? USING (select_with_parens | qualified_name) alias_clause? ON a_expr merge_when_clause+ ; diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java index c326cb5..744dcc3 100644 --- a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -18,6 +18,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.Func_expr_windowlessContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Func_nameContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Func_tableContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Index_elemContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Insert_column_itemContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Insert_targetContext; import ai.singlr.postgresql.parser.PostgreSQLParser.InsertstmtContext; @@ -150,6 +151,7 @@ private static With_clauseContext ownedWithClause(ParseTree node) { case InsertstmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; case UpdatestmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; case DeletestmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; + case MergestmtContext c -> c.with_clause_() != null ? c.with_clause_().with_clause() : null; default -> null; }; } @@ -217,6 +219,8 @@ private void inspect(ParseTree node, Set cteScope) { columns.add(new ColumnReference(null, identifierText(item.colid()))); case Set_targetContext target -> columns.add(new ColumnReference(null, identifierText(target.colid()))); + case Index_elemContext item when item.colid() != null -> + columns.add(new ColumnReference(null, identifierText(item.colid()))); case Join_qualContext join when join.USING() != null -> join.name_list() .name() @@ -302,6 +306,9 @@ private void addDroppedRelations(DropstmtContext drop) { } private static boolean isCteSource(Qualified_nameContext relation) { + if (relation.getParent() instanceof MergestmtContext merge) { + return merge.qualified_name().size() > 1 && merge.qualified_name(1) == relation; + } if (!(relation.getParent() instanceof Relation_exprContext expression)) { return false; } diff --git a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java index 47cfbfc..2cb58d2 100644 --- a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java +++ b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java @@ -7,6 +7,7 @@ import ai.singlr.postgresql.parser.PostgreSQLLexer; import ai.singlr.postgresql.parser.PostgreSQLParser; +import java.nio.charset.StandardCharsets; import java.util.Set; import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.BaseErrorListener; @@ -32,10 +33,12 @@ * hostile input. SQL text and literal content never appear in exception messages and are never * logged. * - *

Two lexically valid PostgreSQL forms are rejected outright because they cannot be analyzed + *

Three lexically valid PostgreSQL forms are rejected outright because they cannot be analyzed * faithfully: psql meta-commands (backslash commands), which are not SQL and could smuggle - * executable commands past analysis, and Unicode-escaped identifiers ({@code U&"..."}), whose - * effective name differs from their spelling and could evade name-based policies. + * executable commands past analysis, Unicode-escaped identifiers ({@code U&"..."}), whose effective + * name differs from their spelling and could evade name-based policies, and identifiers longer than + * {@value #MAX_IDENTIFIER_BYTES} UTF-8 bytes, which PostgreSQL silently truncates so the reported + * name would differ from the one the server resolves. */ public final class PostgresQueryAnalyzer { @@ -48,6 +51,9 @@ public final class PostgresQueryAnalyzer { /** Maximum accepted parenthesis and bracket nesting depth. */ public static final int MAX_NESTING_DEPTH = 128; + /** Maximum accepted identifier length in UTF-8 bytes, matching a default NAMEDATALEN build. */ + public static final int MAX_IDENTIFIER_BYTES = 63; + private static final Set VERBATIM_TOKEN_TYPES = Set.of( PostgreSQLLexer.QuotedIdentifier, @@ -124,6 +130,7 @@ private static void checkBounds(CommonTokenStream tokens) { token.getLine(), token.getCharPositionInLine()); } + checkIdentifierLength(token, type); if (type == PostgreSQLLexer.OPEN_PAREN || type == PostgreSQLLexer.OPEN_BRACKET) { depth++; if (depth > MAX_NESTING_DEPTH) { @@ -138,6 +145,22 @@ private static void checkBounds(CommonTokenStream tokens) { } } + private static void checkIdentifierLength(Token token, int type) { + if (type != PostgreSQLLexer.Identifier && type != PostgreSQLLexer.QuotedIdentifier) { + return; + } + String identifier = token.getText(); + if (type == PostgreSQLLexer.QuotedIdentifier) { + identifier = identifier.substring(1, identifier.length() - 1).replace("\"\"", "\""); + } + if (identifier.getBytes(StandardCharsets.UTF_8).length > MAX_IDENTIFIER_BYTES) { + throw new QueryAnalysisException( + "identifiers longer than " + MAX_IDENTIFIER_BYTES + " bytes are not supported", + token.getLine(), + token.getCharPositionInLine()); + } + } + private static PostgreSQLParser.RootContext parse(CommonTokenStream tokens) { var parser = new PostgreSQLParser(tokens); parser.removeErrorListeners(); diff --git a/src/test/java/ai/singlr/postgresql/HostileInputTest.java b/src/test/java/ai/singlr/postgresql/HostileInputTest.java index 528e9ca..8eb1e6c 100644 --- a/src/test/java/ai/singlr/postgresql/HostileInputTest.java +++ b/src/test/java/ai/singlr/postgresql/HostileInputTest.java @@ -191,6 +191,39 @@ void shouldRejectUnicodeEscapedIdentifiers() { assertEquals(1, PostgresQueryAnalyzer.analyze("SELECT U&'\\0061'").statementCount()); } + @Test + @DisplayName("identifiers longer than 63 bytes are rejected before analysis") + void shouldRejectOverlengthIdentifiers() { + var atLimit = "a".repeat(PostgresQueryAnalyzer.MAX_IDENTIFIER_BYTES); + for (var sql : + new String[] { + "SELECT 1 FROM " + atLimit + "x", + "SELECT 1 FROM \"" + atLimit + "x\"", + "SELECT \"" + "я".repeat(32) + "\" FROM t" + }) { + var exception = + assertThrows(QueryAnalysisException.class, () -> PostgresQueryAnalyzer.analyze(sql), sql); + assertTrue(exception.reason().contains("63 bytes"), exception.reason()); + } + } + + @Test + @DisplayName("identifiers at exactly 63 bytes parse, measured after quote unescaping") + void shouldAcceptIdentifiersAtByteLimit() { + var atLimit = "a".repeat(PostgresQueryAnalyzer.MAX_IDENTIFIER_BYTES); + assertEquals( + atLimit, + PostgresQueryAnalyzer.analyze("SELECT 1 FROM " + atLimit).relations().getFirst().name()); + + var escaped = "a".repeat(62) + "\"\""; + assertEquals( + "a".repeat(62) + "\"", + PostgresQueryAnalyzer.analyze("SELECT 1 FROM \"" + escaped + "\"") + .relations() + .getFirst() + .name()); + } + @Test @DisplayName("dollar tag confusion does not break statement counting") void shouldHandleDollarTagTricks() { diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java index 135fb63..ee8bd8c 100644 --- a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -260,6 +260,32 @@ void shouldKeepUpdateAndDeleteTargetsPhysicalDespiteCteName() { assertEquals(List.of("target:PHYSICAL", "target:CTE"), deleteKinds); } + @Test + @DisplayName("merge accepts a leading with clause and resolves the using source as a CTE") + void shouldAnalyzeMergeWithCte() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH src AS (SELECT id FROM staging) MERGE INTO target t USING src s" + + " ON t.id = s.id WHEN NOT MATCHED THEN INSERT VALUES (s.id)"); + + assertEquals(StatementKind.MERGE, analysis.statementKind()); + assertTrue(analysis.features().contains(QueryFeature.CTE)); + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("staging:PHYSICAL", "target:PHYSICAL", "src:CTE"), kinds); + } + + @Test + @DisplayName("CTE name never hides a merge target") + void shouldKeepMergeTargetPhysicalDespiteCteName() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH target AS (SELECT 1 AS id) MERGE INTO target USING target t" + + " ON target.id = t.id WHEN MATCHED THEN DO NOTHING"); + + var kinds = analysis.relations().stream().map(r -> r.name() + ":" + r.kind()).toList(); + assertEquals(List.of("target:PHYSICAL", "target:CTE"), kinds); + } + @Test @DisplayName("CTE name never hides a select into target") void shouldKeepSelectIntoTargetPhysicalDespiteCteName() { @@ -359,6 +385,18 @@ void shouldAnalyzeInsert() { assertEquals(Set.of("kind", "payload"), analysis.parameters()); } + @Test + @DisplayName("on conflict arbiter columns are reported") + void shouldReportOnConflictArbiterColumns() { + var analysis = + PostgresQueryAnalyzer.analyze( + "INSERT INTO t (a) VALUES (1) ON CONFLICT (tenant_id) DO NOTHING"); + + assertEquals( + List.of(new ColumnReference(null, "a"), new ColumnReference(null, "tenant_id")), + analysis.columns()); + } + @Test @DisplayName("update reports set targets and alias") void shouldAnalyzeUpdate() { diff --git a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java index 989bc89..f38bc19 100644 --- a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java +++ b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java @@ -29,6 +29,7 @@ INSERT INTO t VALUES (1) | INSERT MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DO NOTHING | MERGE MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DO NOTHING | MERGE MERGE INTO t USING s ON t.id = s.id WHEN MATCHED AND t.a > 1 THEN UPDATE SET a = 2 WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT DEFAULT VALUES | MERGE + WITH src AS (SELECT 1 AS id) MERGE INTO t USING src ON t.id = src.id WHEN MATCHED THEN DO NOTHING | MERGE CREATE TABLE t (id int) | DDL CREATE INDEX idx ON t (id) | DDL CREATE VIEW v AS SELECT 1 | DDL From d492bd718b792b84e6d15642a79d6687923eb45f Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 18:25:18 +0000 Subject: [PATCH 5/6] Address fourth-round analyzer review findings - Split greedy :name lexing into COLON + name when the colon continues an expression, so compact JSON key:value and array slices (arr[lo:hi]) parse; arr[:name] deliberately binds to the named-parameter extension (documented) - Parse PostgreSQL 14+ unquoted CREATE FUNCTION bodies (RETURN expr and BEGIN ATOMIC stmt; ... END); nested body statements do not affect the root statement count - Report relations targeted through any_name: COMMENT ON TABLE/COLUMN/ CONSTRAINT..ON/TRIGGER..ON, SECURITY LABEL ON TABLE/COLUMN, and DROP TRIGGER/RULE/POLICY .. ON - Classify REASSIGN OWNED as DDL - Keep normalizedSql single-line: string continuation joins with a space (collision-safe: same-line string adjacency is rejected by the parser) - Refresh NOTICE.md local-modification provenance (also records the earlier iterative block-comment lexing change) --- NOTICE.md | 26 +++-- .../postgresql/parser/PostgreSQLParser.g4 | 22 +++- .../singlr/postgresql/AnalysisCollector.java | 101 ++++++++++++++---- .../postgresql/PostgresQueryAnalyzer.java | 10 +- .../ai/singlr/postgresql/package-info.java | 5 +- .../parser/PostgreSQLLexerBase.java | 57 ++++++++++ .../singlr/postgresql/NamedParameterTest.java | 43 ++++++++ .../singlr/postgresql/NormalizationTest.java | 7 ++ .../postgresql/PostgresQueryAnalyzerTest.java | 71 ++++++++++++ .../StatementClassificationTest.java | 6 ++ 10 files changed, 311 insertions(+), 37 deletions(-) diff --git a/NOTICE.md b/NOTICE.md index e0a391f..f43e750 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -36,15 +36,29 @@ Every deviation from upstream is listed here and marked with a `:user_id`) are first-class expression values. 2. `PostgreSQLParser.g4` — removed `PLSQLVARIABLENAME` from the `identifier` rule so `:name` can never be an identifier, alias, or relation name. -3. `*.java` support files — added a +3. `PostgreSQLParser.g4` — split `createfunctionstmt` into a header plus + either the upstream AS-string option list or the PostgreSQL 14+ unquoted + SQL body (`RETURN expr` / `BEGIN ATOMIC stmt; ... END`), which upstream + does not parse. +4. `PostgreSQLLexerBase.java` — `nextToken()` splits a greedy + `PLSQLVARIABLENAME` match into `COLON` plus the re-lexed name whenever the + previous default-channel token can end an expression, so JSON `key:value` + separators and array-slice bounds (`arr[lo:hi]`) written without + whitespace stay operators instead of becoming named parameters. +5. `PostgreSQLLexerBase.java` — nested block comments are lexed iteratively + with a depth counter instead of the upstream recursive rule, which was + quadratic-time and could overflow the stack on adversarial nesting. +6. `*.java` support files — added a `package ai.singlr.postgresql.parser;` declaration (upstream files have no - package). No other changes; the files are excluded from code formatting to - keep them diffable against upstream. + package). The files are excluded from code formatting to keep them + diffable against upstream. -Known consequences, inherited from upstream lexing of `:name`: +Known consequences of the `:name` named-parameter extension: -- Array slices with identifier bounds (`arr[lo:hi]`) do not parse because - `:hi` lexes as a named parameter. Numeric bounds (`arr[1:2]`) are unaffected. +- A colon directly after `[` binds to the parameter extension, so + `arr[:name]` is a subscript by named parameter, not a slice with an + omitted lower bound. Slices with an expression lower bound (`arr[lo:hi]`, + `arr[1:2]`) are unaffected. - The `:"identifier"` PL/SQL form is rejected. ### Expected grammar warnings diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 index 6c34e9e..c056156 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -1908,10 +1908,30 @@ nulls_order_ ; +// scim-sql local modification: split out createfunction_header and added the PostgreSQL 14+ +// unquoted SQL body forms (RETURN expr / BEGIN ATOMIC stmt; ... END), which upstream only +// accepts as AS-string bodies. createfunctionstmt + : createfunction_header createfunc_opt_list + | createfunction_header createfunc_opt_item_no_as* createfunction_sql_body createfunc_opt_item_no_as* + ; + +createfunction_header : CREATE or_replace_? (FUNCTION | PROCEDURE) func_name func_args_with_defaults ( RETURNS (func_return | TABLE OPEN_PAREN table_func_column_list CLOSE_PAREN) - )? createfunc_opt_list + )? + ; + +createfunc_opt_item_no_as + : LANGUAGE nonreservedword_or_sconst + | TRANSFORM transform_type_list + | WINDOW + | common_func_opt_item + ; + +createfunction_sql_body + : RETURN a_expr + | BEGIN_P ATOMIC (stmt SEMI)* END_P ; or_replace_ diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java index 744dcc3..2b1850f 100644 --- a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -7,7 +7,9 @@ import ai.singlr.postgresql.parser.PostgreSQLParser; import ai.singlr.postgresql.parser.PostgreSQLParser.Alias_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Any_nameContext; import ai.singlr.postgresql.parser.PostgreSQLParser.ColumnrefContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.CommentstmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Common_table_exprContext; import ai.singlr.postgresql.parser.PostgreSQLParser.DeletestmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.DropstmtContext; @@ -26,6 +28,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.Join_qualContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Locked_rels_listContext; import ai.singlr.postgresql.parser.PostgreSQLParser.MergestmtContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Object_type_any_nameContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Over_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.PlsqlvariablenameContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Qualified_nameContext; @@ -33,6 +36,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.Relation_exprContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Relation_expr_opt_aliasContext; import ai.singlr.postgresql.parser.PostgreSQLParser.RootContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.SeclabelstmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Select_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Select_no_parensContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Select_with_parensContext; @@ -76,9 +80,10 @@ final class AnalysisCollector { "revokestmt", "grantrolestmt", "revokerolestmt", - "importforeignschemastmt"); + "importforeignschemastmt", + "reassignownedstmt"); - private static final Set RELATION_DROP_TYPES = + private static final Set RELATION_OBJECT_TYPES = Set.of("table", "view", "materializedview", "foreigntable"); private final List relations = new ArrayList<>(); @@ -199,7 +204,9 @@ private static Set union(Set scope, List names) { private void inspect(ParseTree node, Set cteScope) { switch (node) { case Qualified_nameContext relation -> addRelation(relation, cteScope); - case DropstmtContext drop -> addDroppedRelations(drop); + case DropstmtContext drop -> addDropTargets(drop); + case CommentstmtContext comment -> addCommentTargets(comment); + case SeclabelstmtContext label -> addSecurityLabelTargets(label); case ColumnrefContext column -> addColumn(column); case Func_applicationContext call -> addFunction(call); case Func_expr_common_subexprContext special -> @@ -281,28 +288,82 @@ private void addRelation(Qualified_nameContext relation, Set cteScope) { relations.add(new RelationReference(schema, name, aliasFor(relation), kind)); } - private void addDroppedRelations(DropstmtContext drop) { - if (drop.object_type_any_name() == null - || drop.any_name_list_() == null - || !RELATION_DROP_TYPES.contains(asciiLowercase(drop.object_type_any_name().getText()))) { + private void addDropTargets(DropstmtContext drop) { + if (drop.object_type_name_on_any_name() != null && drop.any_name() != null) { + addAnyNameRelation(drop.any_name()); + return; + } + if (drop.any_name_list_() == null || !isRelationTarget(drop.object_type_any_name())) { return; } for (var anyName : drop.any_name_list_().any_name()) { - List parts = new ArrayList<>(); - parts.add(identifierText(anyName.colid())); - if (anyName.attrs() != null) { - for (var attr : anyName.attrs().attr_name()) { - parts.add(identifierText(attr)); - } + addAnyNameRelation(anyName); + } + } + + private void addCommentTargets(CommentstmtContext comment) { + if (comment.any_name() == null) { + return; + } + if (comment.COLUMN() != null) { + addColumnTarget(comment.any_name()); + } else if (isRelationTarget(comment.object_type_any_name()) + || comment.object_type_name_on_any_name() != null + || (comment.CONSTRAINT() != null && comment.DOMAIN_P() == null)) { + addAnyNameRelation(comment.any_name()); + } + } + + private void addSecurityLabelTargets(SeclabelstmtContext label) { + if (label.any_name() == null) { + return; + } + if (label.COLUMN() != null) { + addColumnTarget(label.any_name()); + } else if (isRelationTarget(label.object_type_any_name())) { + addAnyNameRelation(label.any_name()); + } + } + + private static boolean isRelationTarget(Object_type_any_nameContext objectType) { + return objectType != null + && RELATION_OBJECT_TYPES.contains(asciiLowercase(objectType.getText())); + } + + private void addColumnTarget(Any_nameContext anyName) { + List parts = anyNameParts(anyName); + String column = parts.removeLast(); + if (parts.isEmpty()) { + columns.add(new ColumnReference(null, column)); + return; + } + columns.add(new ColumnReference(String.join(".", parts), column)); + addPhysicalRelation(parts); + } + + private void addAnyNameRelation(Any_nameContext anyName) { + addPhysicalRelation(anyNameParts(anyName)); + } + + private void addPhysicalRelation(List parts) { + String name = parts.removeLast(); + relations.add( + new RelationReference( + parts.isEmpty() ? null : String.join(".", parts), + name, + null, + RelationReference.Kind.PHYSICAL)); + } + + private static List anyNameParts(Any_nameContext anyName) { + List parts = new ArrayList<>(); + parts.add(identifierText(anyName.colid())); + if (anyName.attrs() != null) { + for (var attr : anyName.attrs().attr_name()) { + parts.add(identifierText(attr)); } - String name = parts.removeLast(); - relations.add( - new RelationReference( - parts.isEmpty() ? null : String.join(".", parts), - name, - null, - RelationReference.Kind.PHYSICAL)); } + return parts; } private static boolean isCteSource(Qualified_nameContext relation) { diff --git a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java index 2cb58d2..4bed1ce 100644 --- a/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java +++ b/src/main/java/ai/singlr/postgresql/PostgresQueryAnalyzer.java @@ -199,7 +199,7 @@ private static String normalize(CommonTokenStream tokens) { continue; } if (!normalized.isEmpty() && !insideDollarString(previousType, type)) { - normalized.append(isStringLiteral(previousType) && isStringLiteral(type) ? '\n' : ' '); + normalized.append(' '); } var text = token.getText(); normalized.append( @@ -214,12 +214,4 @@ private static boolean insideDollarString(int previousType, int type) { || previousType == PostgreSQLLexer.DollarText) && (type == PostgreSQLLexer.DollarText || type == PostgreSQLLexer.EndDollarStringConstant); } - - private static boolean isStringLiteral(int type) { - return type == PostgreSQLLexer.StringConstant - || type == PostgreSQLLexer.EscapeStringConstant - || type == PostgreSQLLexer.UnicodeEscapeStringConstant - || type == PostgreSQLLexer.BinaryStringConstant - || type == PostgreSQLLexer.HexadecimalStringConstant; - } } diff --git a/src/main/java/ai/singlr/postgresql/package-info.java b/src/main/java/ai/singlr/postgresql/package-info.java index 4960d30..eb6fc3b 100644 --- a/src/main/java/ai/singlr/postgresql/package-info.java +++ b/src/main/java/ai/singlr/postgresql/package-info.java @@ -19,6 +19,9 @@ * *

The grammar is the ANTLR grammars-v4 PostgreSQL grammar, vendored at a pinned upstream commit * with one deliberate extension: named parameters such as {@code :start_at} are first-class - * expression values. See the repository NOTICE.md for provenance and the exact local modifications. + * expression values. A colon that directly continues an expression — a JSON {@code key:value} + * separator or an array-slice bound such as {@code arr[lo:hi]} — stays an operator; the single + * ambiguous form {@code arr[:name]} binds to the parameter extension. See the repository NOTICE.md + * for provenance and the exact local modifications. */ package ai.singlr.postgresql; diff --git a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java index ebde47e..9fd2346 100644 --- a/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java +++ b/src/main/java/ai/singlr/postgresql/parser/PostgreSQLLexerBase.java @@ -24,18 +24,75 @@ of this software and associated documentation files (the "Software"), to deal import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.Token; +import java.util.Set; import java.util.Stack; public abstract class PostgreSQLLexerBase extends Lexer { protected final Stack tags = new Stack<>(); protected int commentDepth; + // scim-sql local modification: a named parameter (:name) is only valid where an expression + // starts. When ':name' directly follows a token that can end an expression, the colon is + // PostgreSQL's colon operator (a JSON key:value separator or an array-slice bound), so the + // greedy PLSQLVARIABLENAME match is split into COLON and the re-lexed name. + private static final Set EXPRESSION_END_TOKEN_TYPES = Set.of( + PostgreSQLLexer.Identifier, + PostgreSQLLexer.QuotedIdentifier, + PostgreSQLLexer.StringConstant, + PostgreSQLLexer.UnicodeEscapeStringConstant, + PostgreSQLLexer.EscapeStringConstant, + PostgreSQLLexer.BinaryStringConstant, + PostgreSQLLexer.HexadecimalStringConstant, + PostgreSQLLexer.EndDollarStringConstant, + PostgreSQLLexer.Integral, + PostgreSQLLexer.Numeric, + PostgreSQLLexer.PARAM, + PostgreSQLLexer.PLSQLVARIABLENAME, + PostgreSQLLexer.CLOSE_PAREN, + PostgreSQLLexer.CLOSE_BRACKET, + PostgreSQLLexer.NULL_P, + PostgreSQLLexer.TRUE_P, + PostgreSQLLexer.FALSE_P, + PostgreSQLLexer.END_P, + PostgreSQLLexer.CURRENT_DATE, + PostgreSQLLexer.CURRENT_TIME, + PostgreSQLLexer.CURRENT_TIMESTAMP, + PostgreSQLLexer.LOCALTIME, + PostgreSQLLexer.LOCALTIMESTAMP, + PostgreSQLLexer.CURRENT_ROLE, + PostgreSQLLexer.CURRENT_USER, + PostgreSQLLexer.SESSION_USER, + PostgreSQLLexer.USER, + PostgreSQLLexer.CURRENT_CATALOG, + PostgreSQLLexer.CURRENT_SCHEMA); + + private int lastDefaultChannelTokenType = Token.INVALID_TYPE; + protected PostgreSQLLexerBase(CharStream input) { super(input); } + @Override + public Token nextToken() { + Token token = super.nextToken(); + if (token.getType() == PostgreSQLLexer.PLSQLVARIABLENAME + && EXPRESSION_END_TOKEN_TYPES.contains(lastDefaultChannelTokenType)) { + getInputStream().seek(token.getStartIndex() + 1); + setLine(token.getLine()); + setCharPositionInLine(token.getCharPositionInLine() + 1); + token = getTokenFactory().create(_tokenFactorySourcePair, PostgreSQLLexer.COLON, null, + Token.DEFAULT_CHANNEL, token.getStartIndex(), token.getStartIndex(), + token.getLine(), token.getCharPositionInLine()); + } + if (token.getChannel() == Token.DEFAULT_CHANNEL) { + lastDefaultChannelTokenType = token.getType(); + } + return token; + } + public void PushTag() { tags.push(getText()); } diff --git a/src/test/java/ai/singlr/postgresql/NamedParameterTest.java b/src/test/java/ai/singlr/postgresql/NamedParameterTest.java index 7150e8b..3d9bf97 100644 --- a/src/test/java/ai/singlr/postgresql/NamedParameterTest.java +++ b/src/test/java/ai/singlr/postgresql/NamedParameterTest.java @@ -58,6 +58,49 @@ void shouldNotConfuseOperators() { assertEquals(Set.of("real_param"), analysis.parameters()); } + @Test + @DisplayName("json key colon adjacency is an operator, not a parameter") + void shouldNotConfuseJsonColon() { + var spaced = PostgresQueryAnalyzer.analyze("SELECT JSON_OBJECT('a' : owner) FROM t"); + var compact = PostgresQueryAnalyzer.analyze("SELECT JSON_OBJECT('a':owner) FROM t"); + + assertEquals(Set.of(), compact.parameters()); + assertEquals(spaced.normalizedSql(), compact.normalizedSql()); + assertEquals( + Set.of(), PostgresQueryAnalyzer.analyze("SELECT JSON_OBJECT(k:v) FROM t").parameters()); + } + + @Test + @DisplayName("json keys and values may still be parameters") + void shouldCaptureJsonValueParameters() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT JSON_OBJECT('k':owner) FROM t WHERE id = :id AND x = JSON_OBJECT('v' : :v)"); + + assertEquals(Set.of("id", "v"), analysis.parameters()); + assertEquals( + Set.of("key", "val"), + PostgresQueryAnalyzer.analyze("SELECT JSON_OBJECT(:key : :val)").parameters()); + } + + @Test + @DisplayName("array slices with identifier bounds parse and produce no parameters") + void shouldNotConfuseSliceBounds() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT arr[lo:hi], arr[1:n], arr[f(x):g(y)] FROM t" + " WHERE x = :p"); + + assertEquals(Set.of("p"), analysis.parameters()); + } + + @Test + @DisplayName("colon directly after an opening bracket stays a parameter") + void shouldKeepParameterAfterOpeningBracket() { + var analysis = PostgresQueryAnalyzer.analyze("SELECT arr[:idx] FROM t"); + + assertEquals(Set.of("idx"), analysis.parameters()); + } + @Test @DisplayName("colon-like content in strings, comments and quoted identifiers is inert") void shouldIgnoreColonContent() { diff --git a/src/test/java/ai/singlr/postgresql/NormalizationTest.java b/src/test/java/ai/singlr/postgresql/NormalizationTest.java index 9cc3a9b..aee3adb 100644 --- a/src/test/java/ai/singlr/postgresql/NormalizationTest.java +++ b/src/test/java/ai/singlr/postgresql/NormalizationTest.java @@ -77,6 +77,13 @@ void shouldDifferOnParameterName() { normalize("SELECT * FROM t WHERE id = :a"), normalize("SELECT * FROM t WHERE id = :b")); } + @Test + @DisplayName("string continuation normalizes to a single line") + void shouldStaySingleLine() { + assertEquals("select 'a' 'b'", normalize("SELECT 'a'\n'b'")); + assertEquals(-1, normalize("SELECT 'a'\n'b'\n'c' FROM t").indexOf('\n')); + } + @Test @DisplayName("normalization keeps dollar-quoted content verbatim") void shouldPreserveDollarQuotedContent() { diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java index ee8bd8c..749a04b 100644 --- a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -499,6 +499,77 @@ void shouldNotReportNonRelationDrops() { assertEquals(List.of(), PostgresQueryAnalyzer.analyze("DROP SCHEMA s").relations()); } + @Test + @DisplayName("ddl targeting a relation through an object name reports the relation") + void shouldReportAnyNameRelationTargets() { + var expected = + List.of(new RelationReference("private", "users", null, RelationReference.Kind.PHYSICAL)); + + assertEquals( + expected, + PostgresQueryAnalyzer.analyze("COMMENT ON TABLE private.users IS 'x'").relations()); + assertEquals( + expected, + PostgresQueryAnalyzer.analyze("SECURITY LABEL ON TABLE private.users IS 'x'").relations()); + assertEquals( + expected, PostgresQueryAnalyzer.analyze("DROP TRIGGER tr ON private.users").relations()); + assertEquals( + expected, + PostgresQueryAnalyzer.analyze("DROP POLICY IF EXISTS p ON private.users").relations()); + assertEquals( + expected, PostgresQueryAnalyzer.analyze("DROP RULE r ON private.users").relations()); + assertEquals( + expected, + PostgresQueryAnalyzer.analyze("COMMENT ON CONSTRAINT c ON private.users IS 'x'") + .relations()); + assertEquals( + expected, + PostgresQueryAnalyzer.analyze("COMMENT ON TRIGGER tr ON private.users IS 'x'").relations()); + } + + @Test + @DisplayName("comment and security label on a column report the relation and column") + void shouldReportColumnTargetRelations() { + var comment = PostgresQueryAnalyzer.analyze("COMMENT ON COLUMN private.users.email IS 'x'"); + + assertEquals( + List.of(new RelationReference("private", "users", null, RelationReference.Kind.PHYSICAL)), + comment.relations()); + assertEquals(List.of(new ColumnReference("private.users", "email")), comment.columns()); + assertEquals( + List.of(new RelationReference(null, "users", null, RelationReference.Kind.PHYSICAL)), + PostgresQueryAnalyzer.analyze("SECURITY LABEL ON COLUMN users.email IS 'x'").relations()); + } + + @Test + @DisplayName("comments on non-relation objects report no relations") + void shouldNotReportNonRelationAnyNameTargets() { + assertEquals( + List.of(), PostgresQueryAnalyzer.analyze("COMMENT ON SEQUENCE s IS 'x'").relations()); + assertEquals( + List.of(), + PostgresQueryAnalyzer.analyze("COMMENT ON CONSTRAINT c ON DOMAIN d IS 'x'").relations()); + assertEquals( + List.of(), + PostgresQueryAnalyzer.analyze("COMMENT ON OPERATOR CLASS oc USING btree IS 'x'") + .relations()); + assertEquals( + List.of(), + PostgresQueryAnalyzer.analyze("SECURITY LABEL ON SEQUENCE s IS 'x'").relations()); + } + + @Test + @DisplayName("statements nested in a function body do not add to the statement count") + void shouldNotCountFunctionBodyStatements() { + var analysis = + PostgresQueryAnalyzer.analyze( + "CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC" + + " DELETE FROM audit; INSERT INTO audit VALUES (1); END"); + + assertEquals(1, analysis.statementCount()); + assertTrue(analysis.relations().stream().anyMatch(relation -> relation.name().equals("audit"))); + } + @Test @DisplayName("string constants separated by a newline concatenate as in postgresql") void shouldAcceptNewlineConcatenatedStrings() { diff --git a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java index f38bc19..230d8c4 100644 --- a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java +++ b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java @@ -36,6 +36,10 @@ CREATE INDEX idx ON t (id) | DDL CREATE MATERIALIZED VIEW mv AS SELECT 1 | DDL CREATE ROLE reporting | DDL CREATE FUNCTION f() RETURNS int AS 'select 1' LANGUAGE sql | DDL + CREATE FUNCTION f(i int) RETURNS int LANGUAGE SQL RETURN i + 1 | DDL + CREATE FUNCTION f(i int) RETURNS int RETURN i + 1 | DDL + CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC INSERT INTO audit VALUES (1); END | DDL + REASSIGN OWNED BY old_role TO new_role | DDL ALTER TABLE t ADD COLUMN b int | DDL ALTER TABLE t RENAME TO t2 | DDL DROP TABLE t | DDL @@ -79,6 +83,8 @@ void shouldClassify(String sql, StatementKind expected) { EXPLAIN SELECT * FROM users | users CREATE VIEW v AS SELECT * FROM users | users CREATE TABLE copy_t AS SELECT * FROM users | users + CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN (SELECT count(*) FROM users) | users + CREATE PROCEDURE p() LANGUAGE SQL BEGIN ATOMIC DELETE FROM users; END | users """) void shouldCaptureRelationsInWrappedStatements(String sql, String relation) { var names = From e8deaa31989eaccb977078627b2eb610cbdd4b20 Mon Sep 17 00:00:00 2001 From: Uday Chandra Date: Fri, 10 Jul 2026 20:54:20 +0000 Subject: [PATCH 6/6] Close modern-PostgreSQL grammar gaps against gram.y (PG14-18) The vendored grammars-v4 grammar (verified at parity with upstream HEAD) ships several defects and unported productions; each fix below follows PostgreSQL's authoritative gram.y and is recorded in NOTICE.md: - json_aggregate_func existed but was never referenced: wired into func_expr/func_expr_windowless with FILTER/OVER, returning clause made optional; aggregates now reported as functions - MERGE: PG17 RETURNING clause and WHEN NOT MATCHED BY SOURCE/BY TARGET - GROUP BY [ALL|DISTINCT] (PG14) - CTE SEARCH and CYCLE clauses (PG14); fixed upstream BREADTH: 'BREATH' lexer typo that made SEARCH BREADTH FIRST unparseable - IS [NOT] JSON [VALUE|ARRAY|OBJECT|SCALAR] predicate (PG16) - JSON_TABLE table function (PG17), reported as a FUNCTION relation with alias like xmltable, which now also reports one - XMLTABLE column PATH option (PATH is a lexer keyword here, so the generic identifier option could not match it) - json_format_clause now uses the real FORMAT token; upstream's FORMAT_LA literal is Bison lookahead residue that never occurs in SQL text - UNIQUE/PRIMARY KEY ... WITHOUT OVERLAPS (PG18) Conformance battery over PG14-18 syntax went 18/30 to 30/30; all forms pinned as tests. Grammar warnings remain pinned with no new entries. --- NOTICE.md | 24 +++++- .../postgresql/parser/PostgreSQLLexer.g4 | 3 +- .../postgresql/parser/PostgreSQLParser.g4 | 80 ++++++++++++++++--- .../singlr/postgresql/AnalysisCollector.java | 26 ++++++ .../postgresql/PostgresQueryAnalyzerTest.java | 79 ++++++++++++++++++ .../StatementClassificationTest.java | 14 ++++ 6 files changed, 211 insertions(+), 15 deletions(-) diff --git a/NOTICE.md b/NOTICE.md index f43e750..40eb168 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -40,15 +40,33 @@ Every deviation from upstream is listed here and marked with a either the upstream AS-string option list or the PostgreSQL 14+ unquoted SQL body (`RETURN expr` / `BEGIN ATOMIC stmt; ... END`), which upstream does not parse. -4. `PostgreSQLLexerBase.java` — `nextToken()` splits a greedy +4. `PostgreSQLParser.g4` — closed modern-PostgreSQL gaps against `gram.y` + (PostgreSQL 14 through 18), each also marked in place: + `json_aggregate_func` was defined upstream but never referenced (wired + into `func_expr` / `func_expr_windowless`, and its `json_returning_clause` + made optional as in `gram.y`); MERGE gained the PostgreSQL 17 `RETURNING` + clause and `WHEN NOT MATCHED BY SOURCE | BY TARGET`; `group_clause` + accepts the PostgreSQL 14 `ALL | DISTINCT` set quantifier; + `common_table_expr` gained the PostgreSQL 14 `SEARCH` and `CYCLE` + clauses; the PostgreSQL 16 `IS [NOT] JSON` predicate was added to + `a_expr_is_not`; the PostgreSQL 17 `JSON_TABLE` table function was added + and wired into `table_ref`; `UNIQUE`/`PRIMARY KEY` constraints accept the + PostgreSQL 18 `WITHOUT OVERLAPS` marker; `xmltable_column_option_el` + accepts `PATH` explicitly (the lexer keyword cannot match the generic + identifier option); `json_format_clause` uses the real `FORMAT` token — + upstream's `FORMAT_LA` literal is Bison lookahead-token residue that + never occurs in SQL text, so `FORMAT JSON` could never parse. +5. `PostgreSQLLexer.g4` — fixed the upstream `BREADTH: 'BREATH'` typo that + made `SEARCH BREADTH FIRST` unparseable. +6. `PostgreSQLLexerBase.java` — `nextToken()` splits a greedy `PLSQLVARIABLENAME` match into `COLON` plus the re-lexed name whenever the previous default-channel token can end an expression, so JSON `key:value` separators and array-slice bounds (`arr[lo:hi]`) written without whitespace stay operators instead of becoming named parameters. -5. `PostgreSQLLexerBase.java` — nested block comments are lexed iteratively +7. `PostgreSQLLexerBase.java` — nested block comments are lexed iteratively with a depth counter instead of the upstream recursive rule, which was quadratic-time and could overflow the stack on adversarial nesting. -6. `*.java` support files — added a +8. `*.java` support files — added a `package ai.singlr.postgresql.parser;` declaration (upstream files have no package). The files are excluded from code formatting to keep them diffable against upstream. diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 index 48e7e45..4f39ecb 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLLexer.g4 @@ -182,7 +182,8 @@ SYSTEM_USER: 'SYSTEM_USER'; ABSENT: 'ABSENT'; ASENSITIVE: 'ASENSITIVE'; ATOMIC: 'ATOMIC'; -BREADTH: 'BREATH'; +// scim-sql local modification: upstream typo 'BREATH' made SEARCH BREADTH FIRST unparseable. +BREADTH: 'BREADTH'; COMPRESSION: 'COMPRESSION'; CONDITIONAL: 'CONDITIONAL'; DEPTH: 'DEPTH'; diff --git a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 index c056156..468fd0f 100644 --- a/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 +++ b/src/main/antlr4/ai/singlr/postgresql/parser/PostgreSQLParser.g4 @@ -768,12 +768,14 @@ tableconstraint constraintelem : CHECK OPEN_PAREN a_expr CLOSE_PAREN constraintattributespec + // scim-sql local modification: added the PostgreSQL 18 WITHOUT OVERLAPS temporal + // constraint marker from gram.y. | UNIQUE ( - OPEN_PAREN columnlist CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec + OPEN_PAREN columnlist (WITHOUT OVERLAPS)? CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec | existingindex constraintattributespec ) | PRIMARY KEY ( - OPEN_PAREN columnlist CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec + OPEN_PAREN columnlist (WITHOUT OVERLAPS)? CLOSE_PAREN c_include_? definition_? optconstablespace? constraintattributespec | existingindex constraintattributespec ) | EXCLUDE access_method_clause? OPEN_PAREN exclusionconstraintlist CLOSE_PAREN c_include_? definition_? optconstablespace? exclusionwhereclause? @@ -2861,19 +2863,21 @@ returning_clause ; // https://www.postgresql.org/docs/current/sql-merge.html +// scim-sql local modification: added the PostgreSQL 17 RETURNING clause and the +// WHEN NOT MATCHED BY SOURCE / BY TARGET variants from gram.y. mergestmt : with_clause_? MERGE INTO? qualified_name alias_clause? USING (select_with_parens | qualified_name) alias_clause? - ON a_expr merge_when_clause+ + ON a_expr merge_when_clause+ returning_clause? ; merge_when_clause - : WHEN MATCHED (AND a_expr)? THEN? ( + : WHEN (MATCHED | NOT MATCHED BY SOURCE) (AND a_expr)? THEN? ( UPDATE SET set_clause_list | DELETE_P | DO NOTHING ) - | WHEN NOT MATCHED (AND a_expr)? THEN? ( + | WHEN NOT MATCHED (BY TARGET)? (AND a_expr)? THEN? ( INSERT (OPEN_PAREN insert_column_list CLOSE_PAREN)? (values_clause | DEFAULT VALUES) | DO NOTHING ) @@ -3016,8 +3020,17 @@ cte_list : common_table_expr (COMMA common_table_expr)* ; +// scim-sql local modification: added the PostgreSQL 14 SEARCH and CYCLE clauses from gram.y. common_table_expr - : name name_list_? AS materialized_? OPEN_PAREN preparablestmt CLOSE_PAREN + : name name_list_? AS materialized_? OPEN_PAREN preparablestmt CLOSE_PAREN search_clause? cycle_clause? + ; + +search_clause + : SEARCH (DEPTH | BREADTH) FIRST_P BY columnlist SET colid + ; + +cycle_clause + : CYCLE columnlist SET colid (TO aexprconst DEFAULT aexprconst)? USING colid ; materialized_ @@ -3136,8 +3149,9 @@ first_or_next | NEXT ; +// scim-sql local modification: added the PostgreSQL 14 set quantifier (GROUP BY ALL | DISTINCT). group_clause - : GROUP_P BY group_by_list + : GROUP_P BY (ALL | DISTINCT)? group_by_list ; @@ -3214,14 +3228,17 @@ from_list : table_ref (COMMA table_ref)* ; +// scim-sql local modification: added json_table (PostgreSQL 17) as a table reference. table_ref : ( relation_expr alias_clause? tablesample_clause? | func_table func_alias_clause? | xmltable alias_clause? + | json_table alias_clause? | select_with_parens alias_clause? | LATERAL_P ( xmltable alias_clause? + | json_table alias_clause? | func_table func_alias_clause? | select_with_parens alias_clause? ) @@ -3326,6 +3343,35 @@ tablefuncelement : colid typename collate_clause_? ; +// scim-sql local modification: added the PostgreSQL 17 JSON_TABLE table function from gram.y; +// upstream has the JSON_TABLE token but no production. +json_table + : JSON_TABLE OPEN_PAREN json_value_expr COMMA a_expr json_table_path_name_? + json_passing_clause? COLUMNS OPEN_PAREN json_table_column_definition_list CLOSE_PAREN + json_on_error_clause? CLOSE_PAREN + ; + +json_table_path_name_ + : AS name + ; + +json_table_column_definition_list + : json_table_column_definition (COMMA json_table_column_definition)* + ; + +json_table_column_definition + : colid FOR ORDINALITY + | colid typename EXISTS json_table_column_path_clause_? json_on_error_clause? + | colid typename json_format_clause? json_table_column_path_clause_? + json_wrapper_behavior json_quotes_clause? json_behavior_clause? + | NESTED PATH? sconst json_table_path_name_? COLUMNS + OPEN_PAREN json_table_column_definition_list CLOSE_PAREN + ; + +json_table_column_path_clause_ + : PATH sconst + ; + xmltable : XMLTABLE OPEN_PAREN ( c_expr xmlexists_argument COLUMNS xmltable_column_list @@ -3345,8 +3391,11 @@ xmltable_column_option_list : xmltable_column_option_el+ ; +// scim-sql local modification: PATH is a lexer keyword here (PostgreSQL treats it as a plain +// identifier), so the generic identifier option cannot match it; accept it explicitly. xmltable_column_option_el : DEFAULT a_expr + | PATH a_expr | identifier a_expr | NOT NULL_P | NULL_P @@ -3621,6 +3670,7 @@ a_expr_isnull /*12*/ +// scim-sql local modification: added the PostgreSQL 16 IS [NOT] JSON predicate from gram.y. a_expr_is_not : a_expr_compare ( IS NOT? ( @@ -3632,6 +3682,7 @@ a_expr_is_not | OF OPEN_PAREN type_list CLOSE_PAREN | DOCUMENT_P | unicode_normal_form? NORMALIZED + | JSON (VALUE_P | ARRAY | OBJECT_P | SCALAR)? json_key_uniqueness_constraint? ) )? ; @@ -3760,13 +3811,17 @@ func_application ) CLOSE_PAREN ; +// scim-sql local modification: json_aggregate_func was defined upstream but never referenced; +// wired into func_expr and func_expr_windowless exactly as in PostgreSQL gram.y. func_expr : func_application within_group_clause? filter_clause? over_clause? + | json_aggregate_func filter_clause? over_clause? | func_expr_common_subexpr ; func_expr_windowless : func_application + | json_aggregate_func | func_expr_common_subexpr ; @@ -4200,9 +4255,10 @@ json_value_expr: a_expr json_format_clause? ; +// scim-sql local modification: FORMAT_LA is Bison lookahead-token residue whose literal +// ('FORMAT_LA') never occurs in SQL text; real input reads FORMAT JSON [ENCODING name]. json_format_clause: - FORMAT_LA JSON ENCODING name - | FORMAT_LA JSON + FORMAT JSON (ENCODING name)? ; @@ -4274,18 +4330,20 @@ json_value_expr_list: | json_value_expr_list ',' json_value_expr ; +// scim-sql local modification: json_returning_clause is optional as in PostgreSQL gram.y +// (json_returning_clause_opt); upstream required it. json_aggregate_func: JSON_OBJECTAGG '(' json_name_and_value json_object_constructor_null_clause? json_key_uniqueness_constraint? - json_returning_clause + json_returning_clause? ')' | JSON_ARRAYAGG '(' json_value_expr json_array_aggregate_order_by_clause? json_array_constructor_null_clause? - json_returning_clause + json_returning_clause? ')' ; diff --git a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java index 2b1850f..d9c116a 100644 --- a/src/main/java/ai/singlr/postgresql/AnalysisCollector.java +++ b/src/main/java/ai/singlr/postgresql/AnalysisCollector.java @@ -26,6 +26,8 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.InsertstmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Into_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Join_qualContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Json_aggregate_funcContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.Json_tableContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Locked_rels_listContext; import ai.singlr.postgresql.parser.PostgreSQLParser.MergestmtContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Object_type_any_nameContext; @@ -52,6 +54,7 @@ import ai.singlr.postgresql.parser.PostgreSQLParser.Values_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.Window_clauseContext; import ai.singlr.postgresql.parser.PostgreSQLParser.With_clauseContext; +import ai.singlr.postgresql.parser.PostgreSQLParser.XmltableContext; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; @@ -217,7 +220,16 @@ private void inspect(ParseTree node, Set cteScope) { special.getStart().getLine(), special.getStart().getCharPositionInLine())); case PlsqlvariablenameContext parameter -> parameters.add(parameter.getText().substring(1)); + case Json_aggregate_funcContext aggregate -> + functions.add( + new FunctionReference( + null, + asciiLowercase(aggregate.getStart().getText()), + aggregate.getStart().getLine(), + aggregate.getStart().getCharPositionInLine())); case Func_tableContext functionRelation -> addFunctionRelation(functionRelation); + case XmltableContext xmlTable -> addTableFunctionRelation(xmlTable); + case Json_tableContext jsonTable -> addTableFunctionRelation(jsonTable); case Target_starContext star -> { features.add(QueryFeature.STAR_PROJECTION); columns.add(new ColumnReference(null, "*")); @@ -440,6 +452,20 @@ private void addFunctionRelation(Func_tableContext functionRelation) { } } + private void addTableFunctionRelation(ParserRuleContext tableFunction) { + features.add(QueryFeature.FUNCTION_RELATION); + String alias = + tableFunction.getParent() instanceof Table_refContext tableRef + ? followingAlias(tableRef, tableFunction) + : null; + relations.add( + new RelationReference( + null, + asciiLowercase(tableFunction.getStart().getText()), + alias, + RelationReference.Kind.FUNCTION)); + } + private static List windowlessFunctions( Func_tableContext functionRelation) { if (functionRelation.func_expr_windowless() != null) { diff --git a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java index 749a04b..4777df2 100644 --- a/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java +++ b/src/test/java/ai/singlr/postgresql/PostgresQueryAnalyzerTest.java @@ -558,6 +558,85 @@ void shouldNotReportNonRelationAnyNameTargets() { PostgresQueryAnalyzer.analyze("SECURITY LABEL ON SEQUENCE s IS 'x'").relations()); } + @Test + @DisplayName("json aggregates are reported as functions and accept filter and over clauses") + void shouldReportJsonAggregates() { + var analysis = + PostgresQueryAnalyzer.analyze( + "SELECT JSON_OBJECTAGG(k : v) FILTER (WHERE v IS NOT NULL) OVER (PARTITION BY g)," + + " JSON_ARRAYAGG(v ORDER BY v) FROM t"); + + var names = analysis.functions().stream().map(FunctionReference::name).toList(); + assertTrue(names.contains("json_objectagg"), names.toString()); + assertTrue(names.contains("json_arrayagg"), names.toString()); + assertTrue(analysis.features().contains(QueryFeature.WINDOW)); + } + + @Test + @DisplayName("search and cycle clauses parse and keep cte resolution intact") + void shouldAnalyzeSearchAndCycleClauses() { + var analysis = + PostgresQueryAnalyzer.analyze( + "WITH RECURSIVE tr AS (SELECT id, pid FROM edges UNION ALL" + + " SELECT e.id, e.pid FROM edges e JOIN tr ON e.pid = tr.id)" + + " SEARCH DEPTH FIRST BY id SET ord" + + " CYCLE id SET looped USING path" + + " SELECT * FROM tr WHERE weight > :w"); + + assertTrue(analysis.features().contains(QueryFeature.RECURSIVE_CTE)); + assertEquals(Set.of("w"), analysis.parameters()); + var kinds = + analysis.relations().stream() + .map(relation -> relation.name() + ":" + relation.kind()) + .toList(); + assertTrue(kinds.contains("edges:PHYSICAL"), kinds.toString()); + assertTrue(kinds.contains("tr:CTE"), kinds.toString()); + } + + @Test + @DisplayName("merge returning and by source/target variants report their references") + void shouldAnalyzeModernMerge() { + var analysis = + PostgresQueryAnalyzer.analyze( + "MERGE INTO t USING s ON t.id = s.id" + + " WHEN NOT MATCHED BY SOURCE THEN UPDATE SET a = :a" + + " WHEN NOT MATCHED BY TARGET THEN INSERT VALUES (1)" + + " RETURNING t.id, s.id"); + + assertEquals(StatementKind.MERGE, analysis.statementKind()); + assertEquals(Set.of("a"), analysis.parameters()); + assertTrue(analysis.columns().contains(new ColumnReference("t", "id"))); + assertTrue(analysis.columns().contains(new ColumnReference("s", "id"))); + } + + @Test + @DisplayName("xmltable and json_table are function relations with aliases") + void shouldReportTableFunctionRelations() { + var xml = + PostgresQueryAnalyzer.analyze( + "SELECT * FROM XMLTABLE('/r' PASSING x COLUMNS c1 int PATH 'c1') AS xt"); + assertTrue( + xml.relations() + .contains( + new RelationReference(null, "xmltable", "xt", RelationReference.Kind.FUNCTION)), + xml.relations().toString()); + assertTrue(xml.features().contains(QueryFeature.FUNCTION_RELATION)); + + var json = + PostgresQueryAnalyzer.analyze( + "SELECT jt.* FROM JSON_TABLE(j, '$[*]' AS root COLUMNS (seq FOR ORDINALITY," + + " id int PATH '$.id', has_kids boolean EXISTS PATH '$.kids'," + + " NESTED PATH '$.kids[*]' COLUMNS (kid text PATH '$.name'))) AS jt" + + " WHERE jt.id = :id"); + assertTrue( + json.relations() + .contains( + new RelationReference(null, "json_table", "jt", RelationReference.Kind.FUNCTION)), + json.relations().toString()); + assertTrue(json.features().contains(QueryFeature.FUNCTION_RELATION)); + assertEquals(Set.of("id"), json.parameters()); + } + @Test @DisplayName("statements nested in a function body do not add to the statement count") void shouldNotCountFunctionBodyStatements() { diff --git a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java index 230d8c4..51b711a 100644 --- a/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java +++ b/src/test/java/ai/singlr/postgresql/StatementClassificationTest.java @@ -29,6 +29,20 @@ INSERT INTO t VALUES (1) | INSERT MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DO NOTHING | MERGE MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DO NOTHING | MERGE MERGE INTO t USING s ON t.id = s.id WHEN MATCHED AND t.a > 1 THEN UPDATE SET a = 2 WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT DEFAULT VALUES | MERGE + MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = 1 RETURNING t.id | MERGE + MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN DELETE WHEN NOT MATCHED BY TARGET THEN INSERT VALUES (1) | MERGE + SELECT JSON_OBJECTAGG(k : v) FROM t | SELECT + SELECT JSON_ARRAYAGG(v ORDER BY v RETURNING jsonb) FROM t | SELECT + SELECT a, b FROM t GROUP BY DISTINCT ROLLUP (a), ROLLUP (b) | SELECT + SELECT x IS JSON OBJECT, y IS NOT JSON ARRAY, z IS JSON WITH UNIQUE KEYS FROM t | SELECT + WITH RECURSIVE tr AS (SELECT id FROM t2 UNION ALL SELECT t3.id FROM t3 JOIN tr ON t3.pid = tr.id) SEARCH BREADTH FIRST BY id SET ord SELECT * FROM tr | SELECT + WITH RECURSIVE tr AS (SELECT id FROM t2 UNION ALL SELECT t3.id FROM t3 JOIN tr ON t3.pid = tr.id) CYCLE id SET looped TO true DEFAULT false USING path SELECT * FROM tr | SELECT + SELECT JSON_OBJECT('a' : x FORMAT JSON RETURNING jsonb FORMAT JSON) FROM t | SELECT + SELECT breadth, breath, depth, path FROM t | SELECT + SELECT a, b FROM t GROUP BY ALL a, b | SELECT + MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN DO NOTHING WHEN NOT MATCHED THEN DO NOTHING | MERGE + CREATE TABLE tt (r int4range, PRIMARY KEY (r WITHOUT OVERLAPS)) | DDL + CREATE TABLE tt (id int, r int4range, UNIQUE (id, r WITHOUT OVERLAPS)) | DDL WITH src AS (SELECT 1 AS id) MERGE INTO t USING src ON t.id = src.id WHEN MATCHED THEN DO NOTHING | MERGE CREATE TABLE t (id int) | DDL CREATE INDEX idx ON t (id) | DDL