From ae02e106951a1048fba855246a415436ed88e909 Mon Sep 17 00:00:00 2001 From: rebelice Date: Tue, 12 May 2026 11:25:21 +0900 Subject: [PATCH] Improve PG loader compatibility coverage --- SCENARIOS-pg-loader-compat.md | 409 +++++ docs/plans/2026-05-11-pg-loader-compat.md | 102 ++ pg/catalog/alter.go | 85 + pg/catalog/analyze.go | 175 +- pg/catalog/coerce.go | 12 +- pg/catalog/constraint.go | 49 +- pg/catalog/depend.go | 66 +- pg/catalog/functioncmds.go | 32 +- pg/catalog/loader_compat_oracle_test.go | 131 ++ pg/catalog/loader_compat_test.go | 1974 +++++++++++++++++++++ pg/catalog/query.go | 5 + pg/catalog/relation_test.go | 11 +- pg/catalog/sdl_deps_test.go | 11 +- pg/catalog/tablecmds.go | 32 +- pg/catalog/view.go | 120 +- pg/catalog/view_test.go | 2 +- pg/parser/alter_misc.go | 6 + pg/parser/alter_table.go | 10 +- pg/parser/create_table.go | 149 +- pg/parser/grant.go | 16 +- pg/parser/loader_compat_parser_test.go | 47 + pg/parser/parser.go | 30 +- pg/parser/schema.go | 55 +- pg/pgregress/known_failures.json | 5 +- 24 files changed, 3380 insertions(+), 154 deletions(-) create mode 100644 SCENARIOS-pg-loader-compat.md create mode 100644 docs/plans/2026-05-11-pg-loader-compat.md create mode 100644 pg/catalog/loader_compat_oracle_test.go create mode 100644 pg/catalog/loader_compat_test.go create mode 100644 pg/parser/loader_compat_parser_test.go diff --git a/SCENARIOS-pg-loader-compat.md b/SCENARIOS-pg-loader-compat.md new file mode 100644 index 00000000..0c4a76e7 --- /dev/null +++ b/SCENARIOS-pg-loader-compat.md @@ -0,0 +1,409 @@ +# PG Loader Compatibility + +> Goal: PostgreSQL 17 accepted schema DDL, object-control DDL, view definitions, and function signatures should parse and load through omni without compatibility-only rejections. +> Verification: default unit tests cover permanent regressions; `go test -tags=oracle ./pg/catalog` compares selected scenarios against a real PostgreSQL 17 oracle. + +Status: [ ] pending, [x] covered, [~] partial + +Implementation status (2026-05-11): all six phases are covered by the loader compatibility corpus. The executable corpus currently includes 159 PostgreSQL-accepted loader cases, 14 PostgreSQL-rejected compatibility cases, parser tail-safety tests, targeted dependency assertions, and a PostgreSQL 17 oracle test for the accept/reject corpus. Verified with `go test ./pg/parser -count=1`, `go test ./pg/catalog -count=1`, and `go test -tags=oracle ./pg/catalog -run 'TestLoaderCompatPG17Oracle(Accepts|Rejects)Corpus' -count=1`. + +Notable PG behavior clarified during implementation: `numeric` child FKs to `integer` parents are rejected by PG17; user-defined variadic functions with only a variadic parameter reject zero expanded arguments; `CREATE VIEW v(a) AS SELECT x, y` is accepted and names only the leading output column; duplicate view output column names are rejected; CHECK and generated-column expressions reject subqueries; parent partition indexes created before partitions may cause PG to auto-create attached child indexes. + +## Phase 1: Real Dump Regressions + +### 1.1 Known Raw Dump Hits + +- [x] P1.1.01 `CREATE TABLE t ();` loads as a zero-column ordinary table. +- [x] P1.1.02 `FOREIGN KEY (...) REFERENCES ... MATCH SIMPLE` is consumed as a valid match option. +- [x] P1.1.03 `COMMENT ON FUNCTION f(arg_name type, ...)` resolves by argument types and ignores names for identity. +- [x] P1.1.04 `CREATE FUNCTION ... RETURNS TABLE(...) LANGUAGE plpgsql` loads with table return metadata. +- [x] P1.1.05 `concat_ws(text, variadic any)` resolves when called with more than `pg_proc.pronargs`. +- [x] P1.1.06 `jsonb ->> c.col_name` resolves when `c.col_name` is inferred from `unnest(text[]) AS c(col_name)`. +- [x] P1.1.07 A view can read from a local `WITH cte AS (...)` range entry. +- [x] P1.1.08 `ALTER INDEX parent ATTACH PARTITION child` resolves parent and child in the index namespace. +- [x] P1.1.09 A `bigint` foreign key can reference an `integer` primary key through cross-type equality. +- [x] P1.1.10 A later `LATERAL` item can reference an alias introduced by a previous `LATERAL` item. + +### 1.2 Known-Hit Variants + +- [x] P1.2.01 A zero-column table can be created in a non-public schema. +- [x] P1.2.02 A zero-column table can be commented on. +- [x] P1.2.03 A zero-column table can be granted privileges. +- [x] P1.2.04 A zero-column table can be referenced by `CREATE VIEW v AS SELECT * FROM t`. +- [x] P1.2.05 `MATCH SIMPLE` works on a table-level multi-column foreign key. +- [x] P1.2.06 `MATCH SIMPLE` works with `ON UPDATE CASCADE`. +- [x] P1.2.07 `MATCH SIMPLE` works with `ON DELETE SET NULL`. +- [x] P1.2.08 `MATCH SIMPLE` works with `DEFERRABLE INITIALLY DEFERRED`. +- [x] P1.2.09 `COMMENT ON FUNCTION` accepts schema-qualified function names with argument names. +- [x] P1.2.10 `COMMENT ON FUNCTION` accepts quoted argument names. +- [x] P1.2.11 `COMMENT ON FUNCTION` accepts qualified argument types such as `pg_catalog.int4`. +- [x] P1.2.12 `COMMENT ON FUNCTION` accepts variadic argument mode in the identity list. +- [x] P1.2.13 `RETURNS TABLE` with one column loads as a set-returning function. +- [x] P1.2.14 `RETURNS TABLE` with multiple columns loads as record-returning metadata. +- [x] P1.2.15 `RETURNS TABLE` with qualified column types loads. +- [x] P1.2.16 `concat_ws` in a view resolves with unknown string literals. +- [x] P1.2.17 `concat_ws` in a view resolves with integer, uuid, and timestamp arguments. +- [x] P1.2.18 `jsonb ->> alias.col` works when the alias comes from `unnest(varchar[])`. +- [x] P1.2.19 `jsonb ->> alias.col` works when the alias comes from a CTE output column. +- [x] P1.2.20 Simple CTE range resolution works with an explicit CTE column alias list. +- [x] P1.2.21 Simple CTE range resolution works when the CTE name shadows a base table. +- [x] P1.2.22 `ALTER INDEX ATTACH PARTITION` works when parent and child indexes are schema-qualified. +- [x] P1.2.23 `ALTER INDEX ATTACH PARTITION` records child-index to parent-index dependency. +- [x] P1.2.24 `bigint` to `integer` foreign key works in a composite key. +- [x] P1.2.25 `integer` to `bigint` foreign key works in the reverse direction. +- [x] P1.2.26 Chained `LATERAL` subqueries can reference both base table and prior lateral aliases. +- [x] P1.2.27 `LEFT JOIN LATERAL` can reference the left relation in the lateral subquery. +- [x] P1.2.28 `CROSS JOIN LATERAL` can reference the left relation in the lateral subquery. +- [x] P1.2.29 LATERAL correlation inside a view does not panic provenance tracking. +- [x] P1.2.30 The real dump regression corpus runs through PG17 oracle and omni loader with matching accept status. + +## Phase 2: Parser Grammar Drift + +### 2.1 Function Identity Lists + +- [x] P2.1.01 `GRANT EXECUTE ON FUNCTION f(arg_name type, ...)` accepts optional argument names. +- [x] P2.1.02 `REVOKE EXECUTE ON FUNCTION f(arg_name type, ...)` accepts optional argument names. +- [x] P2.1.03 `ALTER FUNCTION f(arg_name type, ...)` accepts optional argument names. +- [x] P2.1.04 `DROP FUNCTION f(arg_name type, ...)` accepts optional argument names. +- [x] P2.1.05 `COMMENT ON FUNCTION f(IN a integer)` resolves by type. +- [x] P2.1.06 `COMMENT ON FUNCTION f(INOUT a integer)` resolves by type. +- [x] P2.1.07 `COMMENT ON FUNCTION f(VARIADIC a integer[])` resolves by type. +- [x] P2.1.08 `GRANT EXECUTE ON FUNCTION f(IN a integer)` resolves by type. +- [x] P2.1.09 `GRANT EXECUTE ON FUNCTION f(INOUT a integer)` resolves by type. +- [x] P2.1.10 `GRANT EXECUTE ON FUNCTION f(VARIADIC a integer[])` resolves by type. +- [x] P2.1.11 `REVOKE EXECUTE ON FUNCTION f(IN a integer)` resolves by type. +- [x] P2.1.12 `ALTER FUNCTION f(IN a integer) OWNER TO role_name` resolves by type. +- [x] P2.1.13 `ALTER FUNCTION f(IN a integer) SET SCHEMA target_schema` resolves by type. +- [x] P2.1.14 `ALTER FUNCTION f(IN a integer) IMMUTABLE` resolves by type. +- [x] P2.1.15 `DROP FUNCTION f(IN a integer)` resolves by type. +- [x] P2.1.16 `DROP PROCEDURE p(INOUT a integer)` resolves by type. +- [x] P2.1.17 `DROP ROUTINE r(a integer)` resolves by type. +- [x] P2.1.18 `COMMENT ON PROCEDURE p(arg_name integer)` resolves by type. +- [x] P2.1.19 `COMMENT ON ROUTINE r(arg_name integer)` resolves by type. +- [x] P2.1.20 Function identity lists accept schema-qualified types. +- [x] P2.1.21 Function identity lists accept array type syntax. +- [x] P2.1.22 Function identity lists accept quoted argument names. +- [x] P2.1.23 Function identity lists accept quoted function names. +- [x] P2.1.24 Function identity lists accept multi-part schema-qualified function names. +- [x] P2.1.25 Function identity lists ignore OUT-only parameters where PostgreSQL ignores them. + +### 2.2 Keyword Token Parity + +- [x] P2.2.01 `MATCH SIMPLE` is accepted when `simple` scans as the `SIMPLE` keyword token. +- [x] P2.2.02 `MATCH FULL` is accepted when `full` scans as a keyword token. +- [x] P2.2.03 `MATCH PARTIAL` follows PostgreSQL accept/reject behavior. +- [x] P2.2.04 Non-reserved keywords can be used as table names where PostgreSQL permits them. +- [x] P2.2.05 Non-reserved keywords can be used as column names where PostgreSQL permits them. +- [x] P2.2.06 Non-reserved keywords can be used as function names where PostgreSQL permits them. +- [x] P2.2.07 Non-reserved keywords can be used as type names where PostgreSQL permits them. +- [x] P2.2.08 Non-reserved keywords can be used as schema names where PostgreSQL permits them. +- [x] P2.2.09 Reserved keywords require quoting in the same positions PostgreSQL requires quoting. +- [x] P2.2.10 Type/function name keywords follow PostgreSQL's `type_func_name_keyword` behavior. +- [x] P2.2.11 Column label keywords follow PostgreSQL's label behavior in view target lists. +- [x] P2.2.12 Object-control statements preserve keyword identifiers in comments. +- [x] P2.2.13 Object-control statements preserve keyword identifiers in grants. +- [x] P2.2.14 Object-control statements preserve keyword identifiers in drops. +- [x] P2.2.15 Object-control statements preserve keyword identifiers in alters. + +### 2.3 Parser Dispatch and Tail Safety + +- [x] P2.3.01 Invalid `DROP FUNCTION ()` is rejected without creating a ghost statement. +- [x] P2.3.02 Invalid `ALTER FUNCTION f()` with no action is rejected like PostgreSQL. +- [x] P2.3.03 Invalid `COMMENT ON FUNCTION f(integer` reports a syntax error and consumes no trailing ghost statement. +- [x] P2.3.04 Invalid `GRANT EXECUTE ON FUNCTION f(integer TO role` reports a syntax error. +- [x] P2.3.05 Invalid `CREATE TABLE t (FOREIGN KEY (x) REFERENCES p MATCH)` reports a syntax error. +- [x] P2.3.06 Invalid `ALTER INDEX i ATTACH PARTITION` reports a syntax error. +- [x] P2.3.07 Invalid `WITH c AS SELECT 1 SELECT * FROM c` reports a syntax error. +- [x] P2.3.08 Invalid `LATERAL ()` is rejected in FROM. +- [x] P2.3.09 Invalid `LATERAL relation_name` is rejected in FROM. +- [x] P2.3.10 Parser statement count matches PostgreSQL for multi-statement input with an invalid middle statement. +- [x] P2.3.11 Parser locations remain stable for function identity list statements. +- [x] P2.3.12 Parser locations remain stable for CTE and LATERAL view statements. + +## Phase 3: Catalog DDL Semantic Compatibility + +### 3.1 Foreign Key Type Compatibility + +- [x] P3.1.01 `bigint` child to `integer` parent is accepted through cross-type equality. +- [x] P3.1.02 `smallint` child to `integer` parent is accepted through cross-type equality. +- [x] P3.1.03 `integer` child to `bigint` parent is accepted through cross-type equality. +- [x] P3.1.04 `integer` child to `smallint` parent follows PostgreSQL behavior. +- [x] P3.1.05 `smallint` child to `bigint` parent follows PostgreSQL behavior. +- [x] P3.1.06 `bigint` child to `smallint` parent follows PostgreSQL behavior. +- [x] P3.1.07 `numeric` child to `integer` parent follows PostgreSQL behavior. +- [x] P3.1.08 `integer` child to `numeric` parent follows PostgreSQL behavior. +- [x] P3.1.09 `text` child to `varchar` parent follows PostgreSQL behavior. +- [x] P3.1.10 `varchar` child to `text` parent follows PostgreSQL behavior. +- [x] P3.1.11 `bpchar` child to `text` parent follows PostgreSQL behavior. +- [x] P3.1.12 `text` child to `bpchar` parent follows PostgreSQL behavior. +- [x] P3.1.13 Domain child to base parent follows PostgreSQL behavior. +- [x] P3.1.14 Base child to domain parent follows PostgreSQL behavior. +- [x] P3.1.15 Domain child to same-domain parent follows PostgreSQL behavior. +- [x] P3.1.16 Domain child to different-domain parent follows PostgreSQL behavior. +- [x] P3.1.17 Composite foreign key validates each cross-type column pair. +- [x] P3.1.18 Composite foreign key rejects when one column pair has no equality operator. +- [x] P3.1.19 Foreign key to a unique index with cross-type equality is accepted. +- [x] P3.1.20 Foreign key to a primary key with included columns is accepted when key columns match. + +### 3.2 Relation Shape and Table DDL + +- [x] P3.2.01 Ordinary tables may have zero user columns. +- [x] P3.2.02 Temporary tables may have zero user columns. +- [x] P3.2.03 Unlogged tables may have zero user columns. +- [x] P3.2.04 Partitioned tables may have zero user columns where PostgreSQL permits it. +- [x] P3.2.05 `CREATE TABLE AS SELECT` with zero output columns follows PostgreSQL behavior. +- [x] P3.2.06 `CREATE VIEW v AS SELECT` with zero target columns follows PostgreSQL behavior. +- [x] P3.2.07 Inherited tables preserve inherited zero-column parents. +- [x] P3.2.08 `CREATE TABLE LIKE zero_column_table` follows PostgreSQL behavior. +- [x] P3.2.09 `ALTER TABLE zero_column_table ADD COLUMN` works. +- [x] P3.2.10 `ALTER TABLE table DROP COLUMN` can produce a zero-user-column table where PostgreSQL permits it. + +### 3.3 Partition and Index DDL + +- [x] P3.3.01 Partitioned index attach records index-to-index dependency state. +- [x] P3.3.02 `ALTER INDEX parent ATTACH PARTITION child` works with schema-qualified index names. +- [x] P3.3.03 `ALTER INDEX parent ATTACH PARTITION child` rejects a child index on the wrong table. +- [x] P3.3.04 `ALTER INDEX parent ATTACH PARTITION child` rejects a parent index on a non-partitioned table. +- [x] P3.3.05 Partitioned parent index dependency survives dump/load order where parent index is created first. +- [x] P3.3.06 Partitioned parent index dependency survives dump/load order where child table is created first. +- [x] P3.3.07 Partitioned parent index dependency survives `CREATE INDEX ON ONLY parent`. +- [x] P3.3.08 Partitioned unique index attach validates key column compatibility. +- [x] P3.3.09 Partitioned expression index attach follows PostgreSQL behavior. +- [x] P3.3.10 Partitioned partial index attach follows PostgreSQL behavior. +- [x] P3.3.11 Partitioned index attach records dependency usable by diff. +- [x] P3.3.12 Partitioned index attach records dependency usable by migration planning. +- [x] P3.3.13 Partitioned table attach records parent-child table dependency. +- [x] P3.3.14 Partitioned table detach follows PostgreSQL behavior where loader supports it. +- [x] P3.3.15 Partition bounds with `DEFAULT` load and preserve parent-child relation. + +### 3.4 Constraint and Default Semantics + +- [x] P3.4.01 Generated columns validate expression type coercion. +- [x] P3.4.02 Generated columns record dependencies on referenced columns. +- [x] P3.4.03 Identity columns record owned sequence dependencies. +- [x] P3.4.04 Column defaults validate expression type coercion. +- [x] P3.4.05 Column defaults record dependencies on functions. +- [x] P3.4.06 Column defaults record dependencies on sequences. +- [x] P3.4.07 CHECK constraints validate boolean expression coercion. +- [x] P3.4.08 CHECK constraints record function dependencies. +- [x] P3.4.09 Exclusion constraints resolve operator classes. +- [x] P3.4.10 Unique constraints with `NULLS NOT DISTINCT` load. +- [x] P3.4.11 Deferrable unique constraints load. +- [x] P3.4.12 Deferrable foreign keys load. +- [x] P3.4.13 `NOT VALID` foreign keys load. +- [x] P3.4.14 `VALIDATE CONSTRAINT` follows PostgreSQL behavior. +- [x] P3.4.15 Constraint comments survive load. + +## Phase 4: Function and Operator Resolver + +### 4.1 Variadic Builtins and User Functions + +- [x] P4.1.01 `concat_ws('-', text, integer)` resolves through variadic expansion. +- [x] P4.1.02 `jsonb_build_object('id', id, 'name', name)` resolves through variadic expansion. +- [x] P4.1.03 Explicit `VARIADIC array[...]` calls are resolved with PostgreSQL-compatible array handling. +- [x] P4.1.04 User-defined variadic functions resolve when called with expanded arguments. +- [x] P4.1.05 `concat(text, integer, timestamp)` resolves through variadic expansion. +- [x] P4.1.06 `format('%s %s', a, b)` resolves through variadic expansion. +- [x] P4.1.07 `json_build_object` resolves through variadic expansion. +- [x] P4.1.08 `json_build_array` resolves through variadic expansion. +- [x] P4.1.09 `jsonb_build_array` resolves through variadic expansion. +- [x] P4.1.10 User-defined variadic functions resolve when called with zero variadic elements. +- [x] P4.1.11 User-defined variadic functions resolve when called with one variadic element. +- [x] P4.1.12 User-defined variadic functions resolve when called with mixed coercible argument types. +- [x] P4.1.13 Explicit `VARIADIC` rejects non-array arguments like PostgreSQL. +- [x] P4.1.14 Explicit `VARIADIC NULL` follows PostgreSQL behavior. +- [x] P4.1.15 Variadic aggregate calls follow PostgreSQL behavior. + +### 4.2 Polymorphic Return and SRF Types + +- [x] P4.2.01 `unnest(text[]) AS alias(col)` exposes `col` as `text`. +- [x] P4.2.02 `unnest(integer[]) AS alias(col)` exposes `col` as `integer`. +- [x] P4.2.03 `unnest(bigint[])` exposes `bigint`. +- [x] P4.2.04 `unnest(uuid[])` exposes `uuid`. +- [x] P4.2.05 `unnest(jsonb[])` exposes `jsonb`. +- [x] P4.2.06 `unnest(anyarray)` with a domain array follows PostgreSQL behavior. +- [x] P4.2.07 `array_length(anyarray, int)` resolves for typed arrays. +- [x] P4.2.08 `array_position(anycompatiblearray, anycompatible)` resolves for text arrays. +- [x] P4.2.09 `array_position(anycompatiblearray, anycompatible)` resolves for integer arrays. +- [x] P4.2.10 `array_append(anycompatiblearray, anycompatible)` resolves and returns array type. +- [x] P4.2.11 `array_prepend(anycompatible, anycompatiblearray)` resolves and returns array type. +- [x] P4.2.12 `coalesce(anycompatible, anycompatible)` resolves common type. +- [x] P4.2.13 `greatest(anycompatible, anycompatible)` resolves common type. +- [x] P4.2.14 `least(anycompatible, anycompatible)` resolves common type. +- [x] P4.2.15 Record-returning builtins expose OUT parameter names. +- [x] P4.2.16 Record-returning user functions expose OUT parameter names. +- [x] P4.2.17 RETURNS TABLE user functions expose table column names. +- [x] P4.2.18 Set-returning functions in FROM expose alias column lists. + +### 4.3 Unknown Literal and Default Argument Resolution + +- [x] P4.3.01 Function calls with defaulted arguments resolve when omitted arguments are within `pronargdefaults`. +- [x] P4.3.02 Unknown string literal resolves to text when a text overload exists. +- [x] P4.3.03 Unknown string literal resolves to uuid when explicitly cast by context. +- [x] P4.3.04 Unknown string literal resolves to jsonb for jsonb operators where PostgreSQL does. +- [x] P4.3.05 Unknown numeric literal resolves to integer for integer-only overloads. +- [x] P4.3.06 Unknown numeric literal resolves to numeric for numeric-preferred overloads. +- [x] P4.3.07 Unknown NULL argument resolves through overload candidate selection. +- [x] P4.3.08 Multiple unknown arguments resolve by preferred type category. +- [x] P4.3.09 Mixed known and unknown arguments resolve through PostgreSQL's last-gasp heuristic. +- [x] P4.3.10 User-defined functions with one default argument resolve. +- [x] P4.3.11 User-defined functions with multiple default arguments resolve. +- [x] P4.3.12 User-defined functions reject omitted non-default arguments. +- [x] P4.3.13 Overloaded user-defined functions with defaults pick the PostgreSQL-compatible candidate. +- [x] P4.3.14 Named argument calls follow PostgreSQL behavior where parser supports them. + +### 4.4 Operator Resolution + +- [x] P4.4.01 `jsonb -> text` resolves. +- [x] P4.4.02 `jsonb -> int` resolves. +- [x] P4.4.03 `jsonb ->> text` resolves. +- [x] P4.4.04 `jsonb ->> int` resolves. +- [x] P4.4.05 `json -> text` resolves. +- [x] P4.4.06 `json ->> text` resolves. +- [x] P4.4.07 `text || unknown` resolves. +- [x] P4.4.08 `unknown || text` resolves. +- [x] P4.4.09 `integer + bigint` resolves. +- [x] P4.4.10 `bigint + integer` resolves. +- [x] P4.4.11 `numeric + integer` resolves. +- [x] P4.4.12 `date + integer` resolves. +- [x] P4.4.13 `timestamp + interval` resolves. +- [x] P4.4.14 `text LIKE unknown` resolves. +- [x] P4.4.15 `text ILIKE unknown` resolves. +- [x] P4.4.16 `array @> array` resolves for integer arrays. +- [x] P4.4.17 `jsonb @> jsonb` resolves. +- [x] P4.4.18 Range operators resolve for `int4range`. + +## Phase 5: View Analyzer Scope + +### 5.1 CTE Range Resolution + +- [x] P5.1.01 A simple CTE is visible to the main query body. +- [x] P5.1.02 CTE column aliases are visible with their alias names and inferred types. +- [x] P5.1.03 Multiple CTEs in one WITH list can reference earlier CTEs. +- [x] P5.1.04 A CTE can reference a base table with the same column names. +- [x] P5.1.05 A CTE name shadows a base relation in the same search path. +- [x] P5.1.06 A nested subquery can reference an outer visible CTE where PostgreSQL permits it. +- [x] P5.1.07 A nested WITH can shadow an outer CTE. +- [x] P5.1.08 A nested WITH can reference a prior outer CTE when not shadowed. +- [x] P5.1.09 Recursive CTE non-recursive term establishes column types. +- [x] P5.1.10 Recursive CTE recursive term can reference the CTE name. +- [x] P5.1.11 Recursive CTE rejects invalid non-UNION shapes. +- [x] P5.1.12 CTE with `MATERIALIZED` loads. +- [x] P5.1.13 CTE with `NOT MATERIALIZED` loads. +- [x] P5.1.14 CTE with explicit column aliases fewer than output columns follows PostgreSQL behavior. +- [x] P5.1.15 CTE with explicit column aliases more than output columns follows PostgreSQL behavior. +- [x] P5.1.16 CTE in a view records dependencies on base relations. +- [x] P5.1.17 CTE in a view records dependencies on base columns. +- [x] P5.1.18 CTE in a view records dependencies on functions used inside the CTE. + +### 5.2 LATERAL and Correlated Scope + +- [x] P5.2.01 A later lateral subquery can reference a preceding lateral alias. +- [x] P5.2.02 `LEFT JOIN LATERAL` can reference the left relation. +- [x] P5.2.03 `CROSS JOIN LATERAL` can reference the left relation. +- [x] P5.2.04 `INNER JOIN LATERAL` can reference the left relation. +- [x] P5.2.05 A lateral subquery can reference multiple preceding FROM items. +- [x] P5.2.06 A lateral subquery can reference a preceding lateral alias. +- [x] P5.2.07 A third lateral item can reference two earlier lateral aliases. +- [x] P5.2.08 A lateral set-returning function can reference the left relation. +- [x] P5.2.09 `LATERAL unnest(t.arr) AS u(val)` exposes alias column type. +- [x] P5.2.10 `LATERAL generate_series(1, t.n) AS g(x)` exposes alias column type. +- [x] P5.2.11 `ROWS FROM (...) WITH ORDINALITY` exposes ordinality. +- [x] P5.2.12 LATERAL with column definition list follows PostgreSQL behavior. +- [x] P5.2.13 Non-lateral subquery cannot reference preceding FROM items. +- [x] P5.2.14 LATERAL inside a joined table has the correct left-side visibility. +- [x] P5.2.15 LATERAL under nested joins respects PostgreSQL join scope. + +### 5.3 Correlated Subqueries + +- [x] P5.3.01 Correlated scalar subquery in SELECT list carries `LevelsUp` safely. +- [x] P5.3.02 Correlated scalar subquery in WHERE carries `LevelsUp` safely. +- [x] P5.3.03 Correlated `EXISTS` subquery in WHERE carries `LevelsUp` safely. +- [x] P5.3.04 Correlated `IN (SELECT ...)` subquery carries `LevelsUp` safely. +- [x] P5.3.05 Correlated subquery in JOIN condition carries `LevelsUp` safely. +- [x] P5.3.06 Correlated subquery in CHECK expression follows PostgreSQL behavior. +- [x] P5.3.07 Correlated subquery in generated column expression follows PostgreSQL behavior. +- [x] P5.3.08 Nested correlated subqueries with `LevelsUp=2` do not panic walkers. +- [x] P5.3.09 Dependency walker records base relation dependencies for correlated refs. +- [x] P5.3.10 Provenance walker ignores outer vars when local target provenance is expected. +- [x] P5.3.11 Ruleutils deparses correlated references with the correct alias. +- [x] P5.3.12 Query span tooling handles outer vars without local range-table panics. + +### 5.4 View Target and Range Function Shapes + +- [x] P5.4.01 View over `SELECT * FROM unnest(array)` has stable column names. +- [x] P5.4.02 View over `SELECT * FROM unnest(array) AS u(val)` has alias column name. +- [x] P5.4.03 View over `SELECT * FROM unnest(array1, array2) AS u(a,b)` has both alias columns. +- [x] P5.4.04 View over record-returning function with column definition list loads. +- [x] P5.4.05 View over RETURNS TABLE user function exposes table columns. +- [x] P5.4.06 View over OUT-parameter user function exposes OUT names. +- [x] P5.4.07 View over `jsonb_to_record` with column definition list loads. +- [x] P5.4.08 View over `jsonb_to_recordset` with column definition list loads. +- [x] P5.4.09 View over `generate_series` with alias loads. +- [x] P5.4.10 View over set-returning function in SELECT target follows PostgreSQL behavior. +- [x] P5.4.11 View over star expansion through CTE preserves column order. +- [x] P5.4.12 View over star expansion through LATERAL preserves column order. +- [x] P5.4.13 View over duplicate output column names follows PostgreSQL behavior. +- [x] P5.4.14 View column alias list overrides target names. +- [x] P5.4.15 View column alias list count mismatch follows PostgreSQL behavior. + +## Phase 6: Object Namespace and Dependency + +### 6.1 Typed Object Resolution + +- [x] P6.1.01 `ALTER INDEX ... ATTACH PARTITION ...` resolves indexes from `schema.Indexes`. +- [x] P6.1.02 `ALTER INDEX ... DEPENDS ON EXTENSION ...` resolves the index object without relation namespace fallback. +- [x] P6.1.03 `ALTER INDEX ... RENAME TO ...` resolves indexes from `schema.Indexes`. +- [x] P6.1.04 `ALTER SEQUENCE ... RENAME TO ...` resolves sequences from `schema.Sequences`. +- [x] P6.1.05 `ALTER SEQUENCE ... OWNER TO ...` resolves sequences from `schema.Sequences`. +- [x] P6.1.06 `ALTER VIEW ... RENAME TO ...` resolves relation namespace with view relkind validation. +- [x] P6.1.07 `ALTER MATERIALIZED VIEW ... RENAME TO ...` resolves relation namespace with matview relkind validation. +- [x] P6.1.08 `ALTER TYPE ... RENAME TO ...` resolves type namespace. +- [x] P6.1.09 `ALTER DOMAIN ... RENAME TO ...` resolves type namespace. +- [x] P6.1.10 `ALTER FUNCTION ... RENAME TO ...` resolves `ObjectWithArgs`. +- [x] P6.1.11 `ALTER PROCEDURE ... RENAME TO ...` resolves `ObjectWithArgs`. +- [x] P6.1.12 `ALTER ROUTINE ... RENAME TO ...` resolves `ObjectWithArgs`. +- [x] P6.1.13 `COMMENT ON INDEX` resolves index namespace. +- [x] P6.1.14 `COMMENT ON SEQUENCE` resolves sequence namespace. +- [x] P6.1.15 `COMMENT ON TYPE` resolves type namespace. +- [x] P6.1.16 `COMMENT ON FUNCTION` resolves function identity by type. +- [x] P6.1.17 `DROP INDEX` resolves index namespace. +- [x] P6.1.18 `DROP SEQUENCE` resolves sequence namespace. +- [x] P6.1.19 `DROP TYPE` resolves type namespace. +- [x] P6.1.20 `DROP FUNCTION` resolves function identity by type. + +### 6.2 Object-Control Cross Product + +- [x] P6.2.01 COMMENT, GRANT, DROP, ALTER, and RENAME share object identity helpers for common target types. +- [x] P6.2.02 `GRANT SELECT ON TABLE` resolves table targets. +- [x] P6.2.03 `GRANT SELECT ON VIEW` resolves view targets. +- [x] P6.2.04 `GRANT USAGE ON SEQUENCE` resolves sequence targets. +- [x] P6.2.05 `GRANT EXECUTE ON FUNCTION` resolves function targets by identity. +- [x] P6.2.06 `REVOKE` mirrors each GRANT target resolver. +- [x] P6.2.07 `DROP OWNED BY` follows PostgreSQL behavior for loaded objects. +- [x] P6.2.08 `ALTER OWNER` follows PostgreSQL behavior for loaded objects. +- [x] P6.2.09 `ALTER SET SCHEMA` follows PostgreSQL behavior for loaded objects. +- [x] P6.2.10 `COMMENT IS NULL` removes comments for loaded objects. + +### 6.3 Dependency Integrity + +- [x] P6.3.01 Attached child indexes record an internal dependency on the parent index. +- [x] P6.3.02 View dependencies are recorded for base relations referenced directly. +- [x] P6.3.03 View dependencies are recorded for base columns referenced directly. +- [x] P6.3.04 View dependencies are recorded for objects referenced through CTE expansions. +- [x] P6.3.05 View dependencies are recorded for objects referenced through LATERAL expansions. +- [x] P6.3.06 Function dependencies are recorded for argument types. +- [x] P6.3.07 Function dependencies are recorded for return types. +- [x] P6.3.08 Function dependencies are recorded for default expressions. +- [x] P6.3.09 Generated column dependencies are recorded for referenced columns. +- [x] P6.3.10 CHECK expression dependencies are recorded for functions and columns. +- [x] P6.3.11 Policy expression dependencies are recorded for functions and columns. +- [x] P6.3.12 Trigger dependencies are recorded for trigger function and table. +- [x] P6.3.13 Index dependencies are recorded for indexed columns. +- [x] P6.3.14 Expression index dependencies are recorded for functions and columns. +- [x] P6.3.15 Partial index dependencies are recorded for predicate functions and columns. +- [x] P6.3.16 Partitioned index dependency state survives schema diff. +- [x] P6.3.17 Partitioned index dependency state survives migration planning. +- [x] P6.3.18 Dependency-driven drop planning follows PostgreSQL cascade expectations. +- [x] P6.3.19 Dependency-driven rename planning preserves object identity. +- [x] P6.3.20 Dependency snapshots are stable across load/deparse/reload. diff --git a/docs/plans/2026-05-11-pg-loader-compat.md b/docs/plans/2026-05-11-pg-loader-compat.md new file mode 100644 index 00000000..dcc03cbb --- /dev/null +++ b/docs/plans/2026-05-11-pg-loader-compat.md @@ -0,0 +1,102 @@ +# PG Loader Compatibility Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Close PostgreSQL-supported syntax and analysis gaps that currently fail in omni's PG parser/catalog loader. + +**Architecture:** Add focused regression tests in `pg/catalog` or `pg/parser` for each PG-supported construct, verify they fail for the expected reason, then make the narrowest parser/catalog/analyzer change for each root cause. Keep each change scoped to the component that rejects behavior PostgreSQL accepts. + +**Tech Stack:** Go, `go test`, omni `pg/parser`, omni `pg/catalog`. + +### Task 1: Zero-column table and `MATCH SIMPLE` + +**Files:** +- Modify: `pg/catalog/tablecmds.go` +- Modify: `pg/parser/create_table.go` +- Test: `pg/catalog/loader_compat_test.go` + +**Step 1: Write the failing tests** + +Add tests that call `LoadSQL` for `CREATE TABLE t ();` and a foreign key using `MATCH SIMPLE`. + +**Step 2: Run tests to verify they fail** + +Run: `go test ./pg/catalog -run 'TestLoaderCompat' -count=1` +Expected: zero-column table fails with `tables must have at least one column`; `MATCH SIMPLE` fails near `SIMPLE`. + +**Step 3: Implement minimal fixes** + +Remove the catalog rejection for zero-column regular tables. Update `parseKeyMatch` to consume the `SIMPLE` token as well as identifier text. + +**Step 4: Run tests to verify they pass** + +Run: `go test ./pg/catalog -run 'TestLoaderCompat' -count=1` +Expected: PASS. + +### Task 2: Function comments and `RETURNS TABLE` + +**Files:** +- Modify: `pg/parser/define.go` or relevant function argument parser +- Modify: `pg/catalog/functioncmds.go` +- Test: `pg/catalog/loader_compat_test.go` + +**Step 1: Write failing tests** + +Add tests for `COMMENT ON FUNCTION f(arg_name integer)` and `RETURNS TABLE(...) LANGUAGE plpgsql`. + +**Step 2: Verify red** + +Run targeted `go test` and confirm failures are from named argument parsing or return validation. + +**Step 3: Implement minimal fixes** + +Accept optional argument names in `ObjectWithArgs` type lists and keep locating comments by argument type OIDs. Align `RETURNS TABLE` return validation with PostgreSQL's OUT parameter behavior. + +**Step 4: Verify green** + +Run the targeted loader compatibility test. + +### Task 3: View analyzer expression gaps + +**Files:** +- Modify: `pg/catalog/analyze.go` +- Test: `pg/catalog/loader_compat_test.go` + +**Step 1: Write failing tests** + +Add view tests for `concat_ws(text, variadic any)`, `jsonb ->> c.col_name` with `unnest(text[])` alias, CTE range resolution, and later LATERAL alias scope. + +**Step 2: Verify red** + +Run targeted tests and record exact failing analyzer path for each case. + +**Step 3: Implement one analyzer fix per failure** + +Use PostgreSQL-compatible type inference and range-table scope handling. Keep changes narrow and do not add broad fallback typing unless the test proves the exact missing behavior. + +**Step 4: Verify green** + +Run targeted tests, then relevant analyzer/view regression tests. + +### Task 4: Index partition attach and FK type compatibility + +**Files:** +- Modify: `pg/catalog/alter.go` +- Modify: `pg/catalog/constraint.go` +- Test: `pg/catalog/loader_compat_test.go` + +**Step 1: Write failing tests** + +Add tests for `ALTER INDEX parent ATTACH PARTITION child` and bigint FK referencing integer PK. + +**Step 2: Verify red** + +Run targeted tests and confirm whether failure is relation lookup, index parent metadata, or type compatibility direction. + +**Step 3: Implement minimal fixes** + +Fix partitioned parent index lookup/metadata and align FK compatibility with PostgreSQL's accepted implicit coercion direction. + +**Step 4: Verify green** + +Run targeted tests and partition/FK regression tests. diff --git a/pg/catalog/alter.go b/pg/catalog/alter.go index 7d32d06d..82d3946c 100644 --- a/pg/catalog/alter.go +++ b/pg/catalog/alter.go @@ -139,6 +139,14 @@ func (c *Catalog) AlterTableStmt(stmt *nodes.AlterTableStmt) error { relName = stmt.Relation.Relname } + if nodes.ObjectType(stmt.ObjType) == nodes.OBJECT_INDEX { + return c.AlterIndexStmt(stmt) + } + if nodes.ObjectType(stmt.ObjType) == nodes.OBJECT_SEQUENCE { + _, err := c.findSequence(schemaName, relName) + return err + } + schema, rel, err := c.findRelation(schemaName, relName) if err != nil { return err @@ -243,6 +251,73 @@ func (c *Catalog) AlterTableStmt(stmt *nodes.AlterTableStmt) error { return nil } +// AlterIndexStmt applies ALTER INDEX commands. +// +// pg: src/backend/commands/tablecmds.c — AlterTable dispatch for OBJECT_INDEX +func (c *Catalog) AlterIndexStmt(stmt *nodes.AlterTableStmt) error { + if stmt.Relation == nil { + return errInvalidParameterValue("ALTER INDEX requires an index name") + } + schema, err := c.resolveTargetSchema(stmt.Relation.Schemaname) + if err != nil { + return err + } + parentIdx := schema.Indexes[stmt.Relation.Relname] + if parentIdx == nil { + return errUndefinedObject("index", stmt.Relation.Relname) + } + if stmt.Cmds == nil { + return nil + } + + for _, item := range stmt.Cmds.Items { + atc, ok := item.(*nodes.AlterTableCmd) + if !ok { + continue + } + switch nodes.AlterTableType(atc.Subtype) { + case nodes.AT_AttachPartition: + pc, ok := atc.Def.(*nodes.PartitionCmd) + if !ok { + return fmt.Errorf("AT_AttachPartition: expected PartitionCmd") + } + if err := c.atExecAttachIndexPartition(parentIdx, pc); err != nil { + return err + } + } + } + return nil +} + +func (c *Catalog) atExecAttachIndexPartition(parentIdx *Index, pc *nodes.PartitionCmd) error { + if pc == nil || pc.Name == nil { + return errInvalidParameterValue("ATTACH PARTITION requires an index name") + } + parentRel := c.findRelByOID(parentIdx.RelOID) + if parentRel == nil || parentRel.RelKind != 'p' { + return errInvalidObjectDefinition(fmt.Sprintf("index %q is not on a partitioned table", parentIdx.Name)) + } + + childSchema, err := c.resolveTargetSchema(pc.Name.Schemaname) + if err != nil { + return err + } + childIdx := childSchema.Indexes[pc.Name.Relname] + if childIdx == nil { + return errUndefinedObject("index", pc.Name.Relname) + } + childRel := c.findRelByOID(childIdx.RelOID) + if childRel == nil { + return errUndefinedTable(pc.Name.Relname) + } + if childRel.PartitionOf != parentRel.OID { + return errInvalidObjectDefinition(fmt.Sprintf("index %q is not on a partition of %q", childIdx.Name, parentRel.Name)) + } + + c.recordDependency('i', childIdx.OID, 0, 'i', parentIdx.OID, 0, DepInternal) + return nil +} + // execAlterTableCmd executes a single ALTER TABLE subcommand. func (c *Catalog) execAlterTableCmd(schema *Schema, rel *Relation, relName string, atc *nodes.AlterTableCmd, identityOptions *nodes.List, recurse, recursing bool) error { cascade := atc.Behavior == int(nodes.DROP_CASCADE) @@ -274,6 +349,8 @@ func (c *Catalog) execAlterTableCmd(schema *Schema, rel *Relation, relName strin analyzed = coerced } col.DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(col.AttNum), analyzed, rel.OID, + DepNormal, DepNormal) rte := c.buildRelationRTE(rel) col.Default = c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) } @@ -285,6 +362,8 @@ func (c *Catalog) execAlterTableCmd(schema *Schema, rel *Relation, relName strin analyzed = coerced } col.DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(col.AttNum), analyzed, rel.OID, + DepNormal, DepNormal) rte := c.buildRelationRTE(rel) genExpr := c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) col.GenerationExpr = genExpr @@ -320,6 +399,8 @@ func (c *Catalog) execAlterTableCmd(schema *Schema, rel *Relation, relName strin analyzed = coerced } rel.Columns[idx].DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(rel.Columns[idx].AttNum), analyzed, rel.OID, + DepNormal, DepNormal) } rte := c.buildRelationRTE(rel) defStr := c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) @@ -1891,6 +1972,8 @@ func extractObjectName(obj nodes.Node) (schema, name string) { switch n := obj.(type) { case *nodes.List: return qualifiedName(n) + case *nodes.ObjectWithArgs: + return qualifiedName(n.Objname) case *nodes.String: return "", n.Str default: @@ -2194,6 +2277,8 @@ func (c *Catalog) atSetExpression(rel *Relation, colName string, expr nodes.Node analyzed = coerced } col.DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(col.AttNum), analyzed, rel.OID, + DepNormal, DepNormal) rte := c.buildRelationRTE(rel) col.GenerationExpr = c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) col.Default = col.GenerationExpr diff --git a/pg/catalog/analyze.go b/pg/catalog/analyze.go index f220cf1c..9c94e595 100644 --- a/pg/catalog/analyze.go +++ b/pg/catalog/analyze.go @@ -389,6 +389,7 @@ type analyzeCtx struct { catalog *Catalog query *Query parent *analyzeCtx // for correlated subqueries + disallowParentCols bool // true for non-LATERAL FROM subqueries that may see CTEs but not outer columns domainConstraint bool // true when analyzing a domain CHECK constraint domainBaseTypeOID uint32 // base type OID for domain VALUE keyword domainBaseTypMod int32 // base type modifier for domain VALUE keyword @@ -422,28 +423,15 @@ func (ac *analyzeCtx) transformRangeVar(rv *nodes.RangeVar) (JoinNode, error) { // Check if this is a CTE reference before looking up tables. // pg: src/backend/parser/parse_clause.c — getRTEForSpecialRelationTypes if rv.Schemaname == "" { - for i, cte := range ac.query.CTEList { - if cte.Name == rv.Relname { - alias := "" - if rv.Alias != nil { - alias = rv.Alias.Aliasname - } - return ac.transformCTERef(rv.Relname, alias, i) - } - } - // Also check visibleCTEs for recursive CTE references. - // During recursive CTE analysis, the partially-defined CTE is stored - // on the Catalog so that the recursive term (analyzed via a fresh - // analyzeCtx) can find it. - // pg: src/backend/parser/analyze.c — determineRecursiveColTypes - for i, cte := range ac.catalog.visibleCTEs { - if cte.Name == rv.Relname { - alias := "" - if rv.Alias != nil { - alias = rv.Alias.Aliasname - } - return ac.transformVisibleCTERef(cte, rv.Relname, alias, i) + if cte, idx, current := ac.lookupCTE(rv.Relname); cte != nil { + alias := "" + if rv.Alias != nil { + alias = rv.Alias.Aliasname } + if current { + return ac.transformCTERef(rv.Relname, alias, idx) + } + return ac.transformVisibleCTERef(cte, rv.Relname, alias, idx) } } @@ -491,6 +479,24 @@ func (ac *analyzeCtx) transformRangeVar(rv *nodes.RangeVar) (JoinNode, error) { return &RangeTableRef{RTIndex: rtIdx}, nil } +func (ac *analyzeCtx) lookupCTE(name string) (*CommonTableExprQ, int, bool) { + for ctx := ac; ctx != nil; ctx = ctx.parent { + for i := len(ctx.query.CTEList) - 1; i >= 0; i-- { + cte := ctx.query.CTEList[i] + if cte.Name == name { + return cte, i, ctx == ac + } + } + } + for i := len(ac.catalog.visibleCTEs) - 1; i >= 0; i-- { + cte := ac.catalog.visibleCTEs[i] + if cte.Name == name { + return cte, i, false + } + } + return nil, 0, false +} + // transformJoinExpr processes a JOIN expression. // // pg: src/backend/parser/parse_clause.c — transformJoinOnClause @@ -653,7 +659,7 @@ func (ac *analyzeCtx) transformRangeSubselect(rs *nodes.RangeSubselect) (JoinNod // LATERAL: analyze with parent context for correlation. subQuery, err = ac.analyzeSubSelect(sub) } else { - subQuery, err = ac.catalog.analyzeSelectStmt(sub) + subQuery, err = ac.analyzeSubSelectWithOptions(sub, true) } if err != nil { return nil, err @@ -711,6 +717,20 @@ func (ac *analyzeCtx) transformRangeFunction(rf *nodes.RangeFunction) (JoinNode, } fexpr := funcItem.Items[0] + if fc, ok := fexpr.(*nodes.FuncCall); ok && isMultiArgUnnest(fc) { + for _, arg := range fc.Args.Items { + unnestCall := *fc + unnestCall.Args = &nodes.List{Items: []nodes.Node{arg}} + analyzed, err := ac.transformExpr(&unnestCall) + if err != nil { + return nil, err + } + funcExprs = append(funcExprs, analyzed) + funcNames = append(funcNames, "unnest") + funcColdefLists = append(funcColdefLists, nil) + } + continue + } analyzed, err := ac.transformExpr(fexpr) if err != nil { return nil, err @@ -727,6 +747,13 @@ func (ac *analyzeCtx) transformRangeFunction(rf *nodes.RangeFunction) (JoinNode, return ac.addRangeTableEntryForFunction(rf, funcExprs, funcNames, funcColdefLists) } +func isMultiArgUnnest(fc *nodes.FuncCall) bool { + if fc == nil || fc.FuncVariadic || fc.Args == nil || len(fc.Args.Items) <= 1 || fc.Funcname == nil || len(fc.Funcname.Items) == 0 { + return false + } + return strings.EqualFold(stringVal(fc.Funcname.Items[len(fc.Funcname.Items)-1]), "unnest") +} + // addRangeTableEntryForFunction builds a RTEFunction RangeTableEntry. // // pg: src/backend/parser/parse_relation.c — addRangeTableEntryForFunction @@ -1004,7 +1031,7 @@ func (ac *analyzeCtx) transformTargetEntry(rt *nodes.ResTarget, startIdx int) ([ } // Track provenance for simple column refs. - if v, ok := expr.(*VarExpr); ok { + if v, ok := expr.(*VarExpr); ok && v.LevelsUp == 0 && v.RangeIdx >= 0 && v.RangeIdx < len(ac.query.RangeTable) { rte := ac.query.RangeTable[v.RangeIdx] te.ResOrigTbl = rte.RelOID te.ResOrigCol = v.AttNum @@ -1020,11 +1047,13 @@ func (ac *analyzeCtx) expandStar(cr *nodes.ColumnRef, startIdx int) ([]*TargetEn tableName := starTableName(cr) var entries []*TargetEntry + hasSource := false if tableName == "" && ac.query.JoinTree != nil { // Unqualified *: walk the join tree so that JOIN USING deduplication // is respected. Each top-level FROM item contributes its columns; // for JoinExprNode the RTE already has the correct (deduplicated) list. + hasSource = len(ac.query.JoinTree.FromList) > 0 for _, jn := range ac.query.JoinTree.FromList { ac.expandStarFromJoinNode(jn, &entries, startIdx) } @@ -1037,6 +1066,7 @@ func (ac *analyzeCtx) expandStar(cr *nodes.ColumnRef, startIdx int) ([]*TargetEn if tableName != "" && rte.ERef != tableName { continue } + hasSource = true for colIdx, colName := range rte.ColNames { var coll uint32 if colIdx < len(rte.ColCollations) { @@ -1064,6 +1094,9 @@ func (ac *analyzeCtx) expandStar(cr *nodes.ColumnRef, startIdx int) ([]*TargetEn } if len(entries) == 0 { + if hasSource { + return entries, nil + } if tableName != "" { return nil, errUndefinedTable(tableName) } @@ -1289,7 +1322,7 @@ func (ac *analyzeCtx) resolveColumnRef(tableName, colName string) (*VarExpr, err } } - if found == nil && ac.parent != nil { + if found == nil && ac.parent != nil && !ac.disallowParentCols { // Try resolving in parent context (correlated subquery). // pg: src/backend/parser/parse_expr.c — transformColumnRef // PG walks up the namespace chain to find outer references. @@ -1431,7 +1464,7 @@ func (ac *analyzeCtx) transformFuncCall(fc *nodes.FuncCall) (AnalyzedExpr, error return nil, errUndefinedFunction(funcName, argTypes) } - proc, matchedArgTypes, err := ac.catalog.resolveFuncOverload(procs, argTypes, fc.AggStar) + proc, matchedArgTypes, err := ac.catalog.resolveFuncOverload(procs, argTypes, fc.AggStar, fc.FuncVariadic) if err != nil { return nil, err } @@ -1969,6 +2002,10 @@ func (ac *analyzeCtx) transformNullTest(nt *nodes.NullTest) (AnalyzedExpr, error // // pg: src/backend/parser/analyze.c — transformSelectStmt (with parent pstate) func (ac *analyzeCtx) analyzeSubSelect(stmt *nodes.SelectStmt) (*Query, error) { + return ac.analyzeSubSelectWithOptions(stmt, false) +} + +func (ac *analyzeCtx) analyzeSubSelectWithOptions(stmt *nodes.SelectStmt, disallowParentCols bool) (*Query, error) { if stmt == nil { return nil, fmt.Errorf("NULL select statement") } @@ -1981,9 +2018,10 @@ func (ac *analyzeCtx) analyzeSubSelect(stmt *nodes.SelectStmt) (*Query, error) { // Simple SELECT with parent context. q := &Query{} subAc := &analyzeCtx{ - catalog: ac.catalog, - query: q, - parent: ac, + catalog: ac.catalog, + query: q, + parent: ac, + disallowParentCols: disallowParentCols, } // Process WITH clause if present. @@ -3060,16 +3098,22 @@ func (ac *analyzeCtx) analyzeCTEs(withClause *nodes.WithClause) error { if err != nil { return err } + if err := validateCTEAliasCount(cte.Ctename, aliases, fullQuery); err != nil { + return err + } // 6. Update the CTE entry with the full query. tmpCTE.Query = fullQuery tmpCTE.Materialized = cte.Ctematerialized } else { // Non-recursive CTE: analyze normally. - subQuery, err := ac.catalog.analyzeSelectStmt(sub) + subQuery, err := ac.analyzeSubSelect(sub) if err != nil { return err } + if err := validateCTEAliasCount(cte.Ctename, aliases, subQuery); err != nil { + return err + } cteQ := &CommonTableExprQ{ Name: cte.Ctename, @@ -3085,6 +3129,13 @@ func (ac *analyzeCtx) analyzeCTEs(withClause *nodes.WithClause) error { return nil } +func validateCTEAliasCount(name string, aliases []string, query *Query) error { + if len(aliases) == 0 || query == nil || len(aliases) <= len(query.TargetList) { + return nil + } + return fmt.Errorf("WITH query %q has %d columns available but %d columns specified", name, len(query.TargetList), len(aliases)) +} + // transformCTERef creates a range table entry for a CTE reference. // // pg: src/backend/parser/parse_clause.c — getRTEForSpecialRelationTypes (CTE case) @@ -3716,15 +3767,16 @@ func (c *Catalog) resolveOpWithCoercionFull(name string, leftType, rightType uin // and the resolved argument types (after resolving UNKNOWN). // // pg: src/backend/parser/parse_func.c — FuncnameGetCandidates + func_select_candidate -func (c *Catalog) resolveFuncOverload(procs []*BuiltinProc, argTypes []uint32, hasStar bool) (*BuiltinProc, []uint32, error) { +func (c *Catalog) resolveFuncOverload(procs []*BuiltinProc, argTypes []uint32, hasStar bool, explicitVariadic bool) (*BuiltinProc, []uint32, error) { // 1. Build candidates filtered by arg count. var candidates []funcCandidate for _, p := range procs { - if int(p.NArgs) != len(argTypes) { + candArgTypes, ok := candidateArgTypesForCall(p, len(argTypes), explicitVariadic) + if !ok { continue } candidates = append(candidates, funcCandidate{ - argTypes: p.ArgTypes[:p.NArgs], + argTypes: candArgTypes, proc: p, }) } @@ -3743,7 +3795,7 @@ func (c *Catalog) resolveFuncOverload(procs []*BuiltinProc, argTypes []uint32, h } } if exact { - return candidates[ci].proc, buildResolvedArgs(argTypes, candidates[ci].proc), nil + return candidates[ci].proc, buildResolvedArgs(argTypes, candidates[ci].argTypes), nil } } @@ -3758,7 +3810,7 @@ func (c *Catalog) resolveFuncOverload(procs []*BuiltinProc, argTypes []uint32, h return nil, nil, errUndefinedFunction(procs[0].Name, argTypes) } if len(coercible) == 1 { - return coercible[0].proc, buildResolvedArgs(argTypes, coercible[0].proc), nil + return coercible[0].proc, buildResolvedArgs(argTypes, coercible[0].argTypes), nil } // 4. Delegate to funcSelectCandidate for multi-phase resolution. @@ -3766,15 +3818,60 @@ func (c *Catalog) resolveFuncOverload(procs []*BuiltinProc, argTypes []uint32, h if best == nil { return nil, nil, errUndefinedFunction(procs[0].Name, argTypes) } - return best.proc, buildResolvedArgs(argTypes, best.proc), nil + return best.proc, buildResolvedArgs(argTypes, best.argTypes), nil +} + +func candidateArgTypesForCall(proc *BuiltinProc, nargs int, explicitVariadic bool) ([]uint32, bool) { + procNArgs := int(proc.NArgs) + if procNArgs < 0 || procNArgs > len(proc.ArgTypes) { + return nil, false + } + + if proc.Variadic == 0 { + minArgs := procNArgs - int(proc.NArgDefaults) + if minArgs < 0 { + minArgs = 0 + } + if nargs < minArgs || nargs > procNArgs { + return nil, false + } + return proc.ArgTypes[:nargs], true + } + + if explicitVariadic { + if nargs != procNArgs { + return nil, false + } + return proc.ArgTypes[:procNArgs], true + } + + minArgs := procNArgs + if nargs < minArgs { + return nil, false + } + fixedArgs := procNArgs - 1 + if fixedArgs < 0 { + return nil, false + } + + argTypes := make([]uint32, nargs) + copy(argTypes, proc.ArgTypes[:fixedArgs]) + variadicType := proc.Variadic + if variadicType == 0 { + variadicType = proc.ArgTypes[procNArgs-1] + } + for i := fixedArgs; i < nargs; i++ { + argTypes[i] = variadicType + } + return argTypes, true } -// buildResolvedArgs constructs the resolved argument type slice from the matched proc. -func buildResolvedArgs(argTypes []uint32, proc *BuiltinProc) []uint32 { +// buildResolvedArgs constructs the resolved argument type slice from the matched candidate. +func buildResolvedArgs(argTypes []uint32, candidateArgTypes []uint32) []uint32 { resolved := make([]uint32, len(argTypes)) for i := range argTypes { - if i < int(proc.NArgs) { - resolved[i] = proc.ArgTypes[i] + if i < len(candidateArgTypes) { + resolved[i] = candidateArgTypes[i] } else { resolved[i] = argTypes[i] } diff --git a/pg/catalog/coerce.go b/pg/catalog/coerce.go index ca87f2a0..96da62dd 100644 --- a/pg/catalog/coerce.go +++ b/pg/catalog/coerce.go @@ -192,8 +192,13 @@ func (c *Catalog) typeIsArray(typeOID uint32) bool { // // pg: src/backend/utils/cache/lsyscache.c — type_is_range func (c *Catalog) typeIsRange(typeOID uint32) bool { - _, ok := c.rangeTypes[typeOID] - return ok + if _, ok := c.rangeTypes[typeOID]; ok { + return true + } + if typ := c.typeByOID[typeOID]; typ != nil && typ.Type == 'r' { + return true + } + return false } // typeIsMultirange returns true if the given OID is a multirange type. @@ -205,6 +210,9 @@ func (c *Catalog) typeIsMultirange(typeOID uint32) bool { return true } } + if typ := c.typeByOID[typeOID]; typ != nil && typ.Type == 'm' { + return true + } return false } diff --git a/pg/catalog/constraint.go b/pg/catalog/constraint.go index 35a7b2d0..e3e1b763 100644 --- a/pg/catalog/constraint.go +++ b/pg/catalog/constraint.go @@ -352,7 +352,7 @@ func (c *Catalog) addFKConstraint(schema *Schema, rel *Relation, def ConstraintD for i := range localAttnums { localCol := rel.Columns[localAttnums[i]-1] refCol := refRel.Columns[refAttnums[i]-1] - if !c.CanCoerce(localCol.TypeOID, refCol.TypeOID, 'i') { + if c.findFKEqOp(refCol.TypeOID, localCol.TypeOID) == 0 { localType := c.typeByOID[localCol.TypeOID] refType := c.typeByOID[refCol.TypeOID] localName := "unknown" @@ -396,7 +396,7 @@ func (c *Catalog) addFKConstraint(schema *Schema, rel *Relation, def ConstraintD for i := range localAttnums { localCol := rel.Columns[localAttnums[i]-1] refCol := refRel.Columns[refAttnums[i]-1] - pfOp := c.findEqualityOp(localCol.TypeOID, refCol.TypeOID) + pfOp := c.findFKEqOp(refCol.TypeOID, localCol.TypeOID) ppOp := c.findEqualityOp(refCol.TypeOID, refCol.TypeOID) ffOp := c.findEqualityOp(localCol.TypeOID, localCol.TypeOID) pfEqOp = append(pfEqOp, pfOp) @@ -497,6 +497,10 @@ func (c *Catalog) addCheckConstraint(rel *Relation, def ConstraintDef) error { // Analyze the CHECK expression if we have the raw AST node. // pg: src/backend/commands/tablecmds.c — cookConstraint (CHECK analysis) if def.RawCheckExpr != nil { + if rawExprContainsSubLink(def.RawCheckExpr) { + return &Error{Code: CodeFeatureNotSupported, + Message: "cannot use subquery in check constraint"} + } if analyzed, err := c.AnalyzeStandaloneExpr(def.RawCheckExpr, rel); err == nil && analyzed != nil { con.CheckAnalyzed = analyzed rte := c.buildRelationRTE(rel) @@ -623,6 +627,47 @@ func (c *Catalog) addExcludeConstraint(schema *Schema, rel *Relation, def Constr return nil } +func (c *Catalog) findExactEqualityOp(leftType, rightType uint32) uint32 { + ops := c.LookupOperatorExact("=", leftType, rightType) + if len(ops) > 0 { + return ops[0].OID + } + return 0 +} + +func (c *Catalog) findFKEqOp(refType, localType uint32) uint32 { + if op := c.findExactEqualityOp(refType, localType); op != 0 { + return op + } + + refBase := c.getBaseType(refType) + localBase := c.getBaseType(localType) + if refBase != refType || localBase != localType { + if op := c.findExactEqualityOp(refBase, localBase); op != 0 { + return op + } + } + + // PG can implement a FK when the referencing column can be implicitly + // coerced to the referenced column's equality type, even without a direct + // cross-type equality operator (for example varchar -> text or int4 -> numeric). + if c.CanCoerce(localBase, refBase, 'i') { + if op := c.findExactEqualityOp(refBase, refBase); op != 0 { + return op + } + } + + refBT := c.typeByOID[refBase] + localBT := c.typeByOID[localBase] + if refBT != nil && localBT != nil && refBT.Category == 'S' && localBT.Category == 'S' { + if op := c.findExactEqualityOp(TEXTOID, TEXTOID); op != 0 { + return op + } + } + + return 0 +} + // findEqualityOp looks up the equality operator for two types. // Returns the operator OID, or 0 if not found. // diff --git a/pg/catalog/depend.go b/pg/catalog/depend.go index 6e06d1b6..1b3261e5 100644 --- a/pg/catalog/depend.go +++ b/pg/catalog/depend.go @@ -1,5 +1,7 @@ package catalog +import "strings" + // DepType classifies a dependency. type DepType byte @@ -15,12 +17,12 @@ const ( // // pg: src/include/catalog/pg_depend.h type DepEntry struct { - ObjType byte // 'r'=relation, 'i'=index, 'c'=constraint, 't'=type + ObjType byte // 'r'=relation, 'i'=index, 'c'=constraint, 't'=type ObjOID uint32 - ObjSubID int32 // pg: pg_depend.objsubid (attnum, or 0 for whole object) + ObjSubID int32 // pg: pg_depend.objsubid (attnum, or 0 for whole object) RefType byte RefOID uint32 - RefSubID int32 // pg: pg_depend.refobjsubid (attnum, or 0 for whole object) + RefSubID int32 // pg: pg_depend.refobjsubid (attnum, or 0 for whole object) DepType DepType } @@ -85,6 +87,14 @@ func (c *Catalog) recordDependencyOnSingleRelExpr( objType byte, objOID uint32, expr AnalyzedExpr, relOID uint32, behavior, selfBehavior DepType, +) { + c.recordDependencyOnSingleRelExprForObject(objType, objOID, 0, expr, relOID, behavior, selfBehavior) +} + +func (c *Catalog) recordDependencyOnSingleRelExprForObject( + objType byte, objOID uint32, objSubID int32, + expr AnalyzedExpr, relOID uint32, + behavior, selfBehavior DepType, ) { if expr == nil { return @@ -100,9 +110,9 @@ func (c *Catalog) recordDependencyOnSingleRelExpr( // pg: recordDependencyOnSingleRelExpr lines 1635-1653 for _, ref := range refs { if ref.refType == 'r' && ref.refOID == relOID { - c.recordDependency(objType, objOID, 0, ref.refType, ref.refOID, ref.refSubID, selfBehavior) + c.recordDependency(objType, objOID, objSubID, ref.refType, ref.refOID, ref.refSubID, selfBehavior) } else { - c.recordDependency(objType, objOID, 0, ref.refType, ref.refOID, ref.refSubID, behavior) + c.recordDependency(objType, objOID, objSubID, ref.refType, ref.refOID, ref.refSubID, behavior) } } } @@ -136,6 +146,9 @@ func (c *Catalog) walkExprRefsWithRel(expr AnalyzedExpr, relOID uint32, refs *[] } case *FuncCallExpr: addRef('f', v.FuncOID, 0) + if seqOID := c.nextvalSequenceOID(v); seqOID != 0 { + addRef('s', seqOID, 0) + } for _, arg := range v.Args { c.walkExprRefsWithRel(arg, relOID, refs, seen) } @@ -221,7 +234,7 @@ func (c *Catalog) walkExprRefsWithRel(expr AnalyzedExpr, relOID uint32, refs *[] c.walkExprRefsWithRel(arg, relOID, refs, seen) } c.walkExprRefsWithRel(v.AggFilter, relOID, refs, seen) - // ConstExpr, CoerceToDomainValueExpr, SQLValueFuncExpr — no references + // ConstExpr, CoerceToDomainValueExpr, SQLValueFuncExpr — no references } } @@ -263,6 +276,9 @@ func (c *Catalog) walkExprRefs(expr AnalyzedExpr, refs *[]depRef, seen map[depRe switch v := expr.(type) { case *FuncCallExpr: addRef('f', v.FuncOID) + if seqOID := c.nextvalSequenceOID(v); seqOID != 0 { + addRef('s', seqOID) + } for _, arg := range v.Args { c.walkExprRefs(arg, refs, seen) } @@ -349,7 +365,7 @@ func (c *Catalog) walkExprRefs(expr AnalyzedExpr, refs *[]depRef, seen map[depRe c.walkExprRefs(arg, refs, seen) } c.walkExprRefs(v.AggFilter, refs, seen) - // VarExpr, ConstExpr, CoerceToDomainValueExpr, SQLValueFuncExpr — no references + // VarExpr, ConstExpr, CoerceToDomainValueExpr, SQLValueFuncExpr — no references } } @@ -365,3 +381,39 @@ func (c *Catalog) removeDepsOn(refType byte, refOID uint32) { } c.deps = c.deps[:n] } + +func (c *Catalog) nextvalSequenceOID(fn *FuncCallExpr) uint32 { + if fn == nil || !strings.EqualFold(fn.FuncName, "nextval") || len(fn.Args) != 1 { + return 0 + } + constArg := unwrapConstExpr(fn.Args[0]) + if constArg == nil || constArg.Value == "" || constArg.TypeOID != REGCLASSOID { + return 0 + } + schemaName, seqName := splitRegclassName(constArg.Value) + seq, err := c.findSequence(schemaName, seqName) + if err != nil { + return 0 + } + return seq.OID +} + +func unwrapConstExpr(expr AnalyzedExpr) *ConstExpr { + switch v := expr.(type) { + case *ConstExpr: + return v + case *RelabelExpr: + return unwrapConstExpr(v.Arg) + case *CoerceViaIOExpr: + return unwrapConstExpr(v.Arg) + default: + return nil + } +} + +func splitRegclassName(name string) (string, string) { + if idx := strings.LastIndexByte(name, '.'); idx >= 0 { + return name[:idx], name[idx+1:] + } + return "", name +} diff --git a/pg/catalog/functioncmds.go b/pg/catalog/functioncmds.go index 39d5dc7e..22ad6938 100644 --- a/pg/catalog/functioncmds.go +++ b/pg/catalog/functioncmds.go @@ -189,6 +189,7 @@ func (c *Catalog) CreateFunctionStmt(stmt *nodes.CreateFunctionStmt) error { // pg: src/backend/commands/functioncmds.c — interpret_function_parameter_list // --------------------------------------------------------------- var argOIDs []uint32 + var variadicOID uint32 var varCount, outCount int var requiredResultType uint32 // 0 = InvalidOid var hasTableParams bool @@ -308,11 +309,13 @@ func (c *Catalog) CreateFunctionStmt(stmt *nodes.CreateFunctionStmt) error { switch toid { case ANYARRAYOID, ANYCOMPATIBLEARRAYOID, ANYOID: // These pseudo-types are okay for VARIADIC. + variadicOID = ANYOID default: bt := c.typeByOID[toid] if bt == nil || bt.Elem == 0 { return errInvalidFunctionDefinition("VARIADIC parameter must be an array") } + variadicOID = bt.Elem } } @@ -428,6 +431,9 @@ func (c *Catalog) CreateFunctionStmt(stmt *nodes.CreateFunctionStmt) error { } // pg: line 1182-1186 — validate against requiredResultType from OUT params + if hasTableParams && outCount == 1 && requiredResultType != 0 && retOID == RECORDOID { + retOID = requiredResultType + } if requiredResultType != 0 && retOID != requiredResultType { return errInvalidFunctionDefinition( fmt.Sprintf("function result type must be %s because of OUT parameters", @@ -506,18 +512,20 @@ func (c *Catalog) CreateFunctionStmt(stmt *nodes.CreateFunctionStmt) error { // Register as BuiltinProc so resolveFunc works. bp := &BuiltinProc{ - OID: procOID, - Name: name, - Kind: kind, - SecDef: secDef, - LeakProof: isLeakProof, - IsStrict: isStrict, - RetSet: returnSet, - Volatile: volatile, - Parallel: parallel, - NArgs: int16(len(argOIDs)), - RetType: retOID, - ArgTypes: argOIDs, + OID: procOID, + Name: name, + Kind: kind, + SecDef: secDef, + LeakProof: isLeakProof, + IsStrict: isStrict, + RetSet: returnSet, + Volatile: volatile, + Parallel: parallel, + Variadic: variadicOID, + NArgs: int16(len(argOIDs)), + NArgDefaults: nArgDefaults, + RetType: retOID, + ArgTypes: argOIDs, } c.procByOID[procOID] = bp c.procByName[name] = append(c.procByName[name], bp) diff --git a/pg/catalog/loader_compat_oracle_test.go b/pg/catalog/loader_compat_oracle_test.go new file mode 100644 index 00000000..e83e2066 --- /dev/null +++ b/pg/catalog/loader_compat_oracle_test.go @@ -0,0 +1,131 @@ +//go:build oracle + +package catalog + +import ( + "context" + "database/sql" + "fmt" + "strings" + "sync" + "testing" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/testcontainers/testcontainers-go" + tcpg "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +type loaderCompatOracle struct { + db *sql.DB + ctx context.Context +} + +var ( + loaderCompatOracleOnce sync.Once + loaderCompatOracleInst *loaderCompatOracle + loaderCompatOracleSetupErr error +) + +func startLoaderCompatOracle(t *testing.T) *loaderCompatOracle { + t.Helper() + loaderCompatOracleOnce.Do(func() { + defer func() { + if r := recover(); r != nil { + loaderCompatOracleSetupErr = fmt.Errorf("docker provider panic: %v", r) + } + }() + + ctx := context.Background() + container, err := tcpg.Run(ctx, "postgres:17-alpine", + tcpg.WithDatabase("omni_loader_compat"), + tcpg.WithUsername("postgres"), + tcpg.WithPassword("test"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2)), + ) + if err != nil { + loaderCompatOracleSetupErr = fmt.Errorf("container start: %w", err) + return + } + + connStr, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + _ = testcontainers.TerminateContainer(container) + loaderCompatOracleSetupErr = fmt.Errorf("conn string: %w", err) + return + } + db, err := sql.Open("pgx", connStr) + if err != nil { + _ = testcontainers.TerminateContainer(container) + loaderCompatOracleSetupErr = fmt.Errorf("db open: %w", err) + return + } + if err := db.PingContext(ctx); err != nil { + db.Close() + _ = testcontainers.TerminateContainer(container) + loaderCompatOracleSetupErr = fmt.Errorf("ping: %w", err) + return + } + + loaderCompatOracleInst = &loaderCompatOracle{db: db, ctx: ctx} + }) + + if loaderCompatOracleSetupErr != nil { + t.Skipf("loader compatibility oracle unavailable: %v", loaderCompatOracleSetupErr) + } + return loaderCompatOracleInst +} + +func (o *loaderCompatOracle) execIsolated(t *testing.T, ddl string) { + t.Helper() + if err := o.execIsolatedErr(t, ddl); err != nil { + t.Fatalf("PG17 rejected SQL that the compatibility corpus expects to be accepted: %v\nSQL:\n%s", err, ddl) + } +} + +func (o *loaderCompatOracle) execIsolatedErr(t *testing.T, ddl string) error { + t.Helper() + schema := strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_")) + schema = strings.ReplaceAll(schema, "-", "_") + if _, err := o.db.ExecContext(o.ctx, fmt.Sprintf(`DROP SCHEMA IF EXISTS %q CASCADE`, schema)); err != nil { + return fmt.Errorf("PG cleanup failed: %w", err) + } + if _, err := o.db.ExecContext(o.ctx, fmt.Sprintf(`CREATE SCHEMA %q`, schema)); err != nil { + return fmt.Errorf("PG schema setup failed: %w", err) + } + t.Cleanup(func() { + _, _ = o.db.ExecContext(o.ctx, fmt.Sprintf(`DROP SCHEMA IF EXISTS %q CASCADE`, schema)) + }) + + sql := fmt.Sprintf("SET search_path TO %q, public;\n%s", schema, ddl) + _, err := o.db.ExecContext(o.ctx, sql) + return err +} + +func TestLoaderCompatPG17OracleAcceptsCorpus(t *testing.T) { + oracle := startLoaderCompatOracle(t) + for _, tc := range loaderCompatAcceptCases() { + t.Run(tc.name, func(t *testing.T) { + oracle.execIsolated(t, tc.sql) + if _, err := LoadSQL(tc.sql); err != nil { + t.Fatalf("LoadSQL rejected PG17-accepted SQL: %v\nSQL:\n%s", err, tc.sql) + } + }) + } +} + +func TestLoaderCompatPG17OracleRejectsCorpus(t *testing.T) { + oracle := startLoaderCompatOracle(t) + for _, tc := range loaderCompatRejectCases() { + t.Run(tc.name, func(t *testing.T) { + if oracle.execIsolatedErr(t, tc.sql) == nil { + t.Fatalf("PG17 accepted SQL that the reject corpus expects to be rejected\nSQL:\n%s", tc.sql) + } + if _, err := LoadSQL(tc.sql); err == nil { + t.Fatalf("LoadSQL accepted SQL that the reject corpus expects to be rejected\nSQL:\n%s", tc.sql) + } + }) + } +} diff --git a/pg/catalog/loader_compat_test.go b/pg/catalog/loader_compat_test.go new file mode 100644 index 00000000..5d269317 --- /dev/null +++ b/pg/catalog/loader_compat_test.go @@ -0,0 +1,1974 @@ +package catalog + +import "testing" + +type loaderCompatCase struct { + name string + sql string +} + +func loaderCompatRejectCases() []loaderCompatCase { + return []loaderCompatCase{ + { + name: "numeric_fk_references_integer_pk", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id numeric REFERENCES parent(id) + ); + `, + }, + { + name: "composite_fk_rejects_one_incompatible_column_pair", + sql: ` + CREATE TABLE parent (id integer, tenant_id integer, PRIMARY KEY (id, tenant_id)); + CREATE TABLE child ( + parent_id numeric, + tenant_id integer, + FOREIGN KEY (parent_id, tenant_id) REFERENCES parent(id, tenant_id) + ); + `, + }, + { + name: "alter_index_attach_rejects_wrong_child_table", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + CREATE TABLE other (id integer); + CREATE INDEX parent_id_idx ON ONLY parent (id); + CREATE INDEX other_id_idx ON other (id); + ALTER INDEX parent_id_idx ATTACH PARTITION other_id_idx; + `, + }, + { + name: "alter_index_attach_rejects_nonpartitioned_parent_table", + sql: ` + CREATE TABLE parent (id integer); + CREATE TABLE child (id integer); + CREATE INDEX parent_id_idx ON parent (id); + CREATE INDEX child_id_idx ON child (id); + ALTER INDEX parent_id_idx ATTACH PARTITION child_id_idx; + `, + }, + { + name: "partitioned_zero_column_table_expression_key_rejected", + sql: `CREATE TABLE zc () PARTITION BY LIST (());`, + }, + { + name: "explicit_variadic_rejects_non_array_argument", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_bad_variadic AS + SELECT count_variadic(VARIADIC 1) AS value; + `, + }, + { + name: "user_variadic_function_zero_args_rejected", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_zero AS + SELECT count_variadic() AS value; + `, + }, + { + name: "recursive_cte_self_reference_without_union_rejected", + sql: ` + CREATE VIEW v_bad_recursive AS + WITH RECURSIVE r(n) AS (SELECT n + 1 FROM r) + SELECT n FROM r; + `, + }, + { + name: "cte_column_aliases_more_than_output_rejected", + sql: ` + CREATE VIEW v_bad_cte_alias AS + WITH c(a, b) AS (SELECT 1) + SELECT a FROM c; + `, + }, + { + name: "non_lateral_subquery_references_prior_from_item_rejected", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_bad_non_lateral AS + SELECT s.id + FROM base_items b, + (SELECT b.id AS id) s; + `, + }, + { + name: "check_constraint_subquery_rejected", + sql: ` + CREATE TABLE base_items (id integer); + CREATE TABLE t ( + id integer, + CHECK (id IN (SELECT id FROM base_items)) + ); + `, + }, + { + name: "generated_column_subquery_rejected", + sql: ` + CREATE TABLE base_items (id integer); + CREATE TABLE t ( + id integer, + generated integer GENERATED ALWAYS AS ((SELECT max(id) FROM base_items)) STORED + ); + `, + }, + { + name: "view_column_aliases_more_than_output_rejected", + sql: `CREATE VIEW v_bad_alias(a, b) AS SELECT 1;`, + }, + { + name: "view_duplicate_output_column_names_rejected", + sql: `CREATE VIEW v_dupe_cols AS SELECT 1 AS id, 2 AS id;`, + }, + } +} + +func loaderCompatAcceptCases() []loaderCompatCase { + return []loaderCompatCase{ + { + name: "zero_column_table", + sql: `CREATE TABLE zc ();`, + }, + { + name: "zero_column_table_in_schema", + sql: ` + CREATE SCHEMA loader_s1; + CREATE TABLE loader_s1.zc (); + `, + }, + { + name: "zero_column_table_comment", + sql: ` + CREATE TABLE zc (); + COMMENT ON TABLE zc IS 'zero columns'; + `, + }, + { + name: "zero_column_table_grant", + sql: ` + CREATE TABLE zc (); + GRANT SELECT ON zc TO PUBLIC; + `, + }, + { + name: "zero_column_table_view_star", + sql: ` + CREATE TABLE zc (); + CREATE VIEW v_zc AS SELECT * FROM zc; + `, + }, + { + name: "foreign_key_match_simple", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer, + FOREIGN KEY (parent_id) REFERENCES parent(id) MATCH SIMPLE + ); + `, + }, + { + name: "foreign_key_match_simple_multicolumn", + sql: ` + CREATE TABLE parent (a integer, b integer, PRIMARY KEY (a, b)); + CREATE TABLE child ( + a integer, + b integer, + FOREIGN KEY (a, b) REFERENCES parent(a, b) MATCH SIMPLE + ); + `, + }, + { + name: "foreign_key_match_simple_on_update_cascade", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) MATCH SIMPLE ON UPDATE CASCADE + ); + `, + }, + { + name: "foreign_key_match_simple_on_delete_set_null", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) MATCH SIMPLE ON DELETE SET NULL + ); + `, + }, + { + name: "foreign_key_match_simple_deferrable", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) MATCH SIMPLE DEFERRABLE INITIALLY DEFERRED + ); + `, + }, + { + name: "comment_function_argument_names", + sql: ` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + COMMENT ON FUNCTION f(a integer, b text) IS 'comment'; + `, + }, + { + name: "comment_schema_qualified_function_argument_names", + sql: ` + CREATE SCHEMA funcs; + CREATE FUNCTION funcs.f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + COMMENT ON FUNCTION funcs.f(a integer, b text) IS 'comment'; + `, + }, + { + name: "comment_function_quoted_argument_names", + sql: ` + CREATE FUNCTION f("select" integer, "from" text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + COMMENT ON FUNCTION f("select" integer, "from" text) IS 'comment'; + `, + }, + { + name: "comment_function_qualified_argument_types", + sql: ` + CREATE FUNCTION f(a pg_catalog.int4, b pg_catalog.text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + COMMENT ON FUNCTION f(a pg_catalog.int4, b pg_catalog.text) IS 'comment'; + `, + }, + { + name: "comment_variadic_function_identity", + sql: ` + CREATE FUNCTION f(VARIADIC nums integer[]) RETURNS integer + LANGUAGE sql AS 'SELECT cardinality(nums)'; + COMMENT ON FUNCTION f(VARIADIC nums integer[]) IS 'comment'; + `, + }, + { + name: "grant_function_argument_names", + sql: ` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + GRANT EXECUTE ON FUNCTION f(a integer, b text) TO PUBLIC; + `, + }, + { + name: "revoke_function_argument_names", + sql: ` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + GRANT EXECUTE ON FUNCTION f(a integer, b text) TO PUBLIC; + REVOKE EXECUTE ON FUNCTION f(a integer, b text) FROM PUBLIC; + `, + }, + { + name: "alter_function_argument_names", + sql: ` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + ALTER FUNCTION f(a integer, b text) RENAME TO f2; + `, + }, + { + name: "drop_function_argument_names", + sql: ` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + DROP FUNCTION f(a integer, b text); + `, + }, + { + name: "comment_function_identity_in_mode", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + COMMENT ON FUNCTION f(IN a integer) IS 'comment'; + `, + }, + { + name: "comment_function_identity_inout_mode", + sql: ` + CREATE FUNCTION f(INOUT a integer) + LANGUAGE sql AS 'SELECT $1'; + COMMENT ON FUNCTION f(INOUT a integer) IS 'comment'; + `, + }, + { + name: "grant_function_identity_in_mode", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + GRANT EXECUTE ON FUNCTION f(IN a integer) TO PUBLIC; + `, + }, + { + name: "grant_function_identity_inout_mode", + sql: ` + CREATE FUNCTION f(INOUT a integer) + LANGUAGE sql AS 'SELECT $1'; + GRANT EXECUTE ON FUNCTION f(INOUT a integer) TO PUBLIC; + `, + }, + { + name: "grant_function_identity_variadic_mode", + sql: ` + CREATE FUNCTION f(VARIADIC a integer[]) RETURNS integer + LANGUAGE sql AS 'SELECT cardinality($1)'; + GRANT EXECUTE ON FUNCTION f(VARIADIC a integer[]) TO PUBLIC; + `, + }, + { + name: "revoke_function_identity_in_mode", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + GRANT EXECUTE ON FUNCTION f(IN a integer) TO PUBLIC; + REVOKE EXECUTE ON FUNCTION f(IN a integer) FROM PUBLIC; + `, + }, + { + name: "alter_function_identity_in_mode_owner", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + ALTER FUNCTION f(IN a integer) OWNER TO CURRENT_USER; + `, + }, + { + name: "alter_function_identity_in_mode_options", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + ALTER FUNCTION f(IN a integer) IMMUTABLE; + `, + }, + { + name: "drop_function_identity_in_mode", + sql: ` + CREATE FUNCTION f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + DROP FUNCTION f(IN a integer); + `, + }, + { + name: "drop_procedure_identity_inout_mode", + sql: ` + CREATE PROCEDURE p(INOUT a integer) + LANGUAGE sql AS 'SELECT $1'; + DROP PROCEDURE p(INOUT a integer); + `, + }, + { + name: "drop_routine_identity_argument_name", + sql: ` + CREATE FUNCTION r(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + DROP ROUTINE r(a integer); + `, + }, + { + name: "function_identity_schema_qualified_types", + sql: ` + CREATE FUNCTION f(a pg_catalog.int4) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + COMMENT ON FUNCTION f(a pg_catalog.int4) IS 'comment'; + `, + }, + { + name: "function_identity_array_type", + sql: ` + CREATE FUNCTION f(a integer[]) RETURNS integer + LANGUAGE sql AS 'SELECT cardinality($1)'; + COMMENT ON FUNCTION f(a integer[]) IS 'comment'; + `, + }, + { + name: "function_identity_quoted_function_name", + sql: ` + CREATE FUNCTION "select"(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + COMMENT ON FUNCTION "select"(a integer) IS 'comment'; + `, + }, + { + name: "function_identity_schema_qualified_function_name", + sql: ` + CREATE SCHEMA ident_s; + CREATE FUNCTION ident_s.f(a integer) RETURNS integer + LANGUAGE sql AS 'SELECT $1'; + COMMENT ON FUNCTION ident_s.f(a integer) IS 'comment'; + `, + }, + { + name: "returns_table_plpgsql", + sql: ` + CREATE FUNCTION f() + RETURNS TABLE(id integer, name text) + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN QUERY SELECT 1, 'one'::text; + END + $$; + `, + }, + { + name: "returns_table_single_column_plpgsql", + sql: ` + CREATE FUNCTION f() + RETURNS TABLE(id integer) + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN QUERY SELECT 1; + END + $$; + `, + }, + { + name: "returns_table_qualified_column_types", + sql: ` + CREATE FUNCTION f() + RETURNS TABLE(id pg_catalog.int4, name pg_catalog.text) + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN QUERY SELECT 1, 'one'::text; + END + $$; + `, + }, + { + name: "bigint_fk_references_integer_pk", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id bigint REFERENCES parent(id) + ); + `, + }, + { + name: "smallint_fk_references_integer_pk", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id smallint REFERENCES parent(id) + ); + `, + }, + { + name: "integer_fk_references_bigint_pk", + sql: ` + CREATE TABLE parent (id bigint PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) + ); + `, + }, + { + name: "integer_fk_references_smallint_pk", + sql: ` + CREATE TABLE parent (id smallint PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) + ); + `, + }, + { + name: "smallint_fk_references_bigint_pk", + sql: ` + CREATE TABLE parent (id bigint PRIMARY KEY); + CREATE TABLE child ( + parent_id smallint REFERENCES parent(id) + ); + `, + }, + { + name: "bigint_fk_references_smallint_pk", + sql: ` + CREATE TABLE parent (id smallint PRIMARY KEY); + CREATE TABLE child ( + parent_id bigint REFERENCES parent(id) + ); + `, + }, + { + name: "integer_fk_references_numeric_pk", + sql: ` + CREATE TABLE parent (id numeric PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) + ); + `, + }, + { + name: "varchar_fk_references_text_pk", + sql: ` + CREATE TABLE parent (id text PRIMARY KEY); + CREATE TABLE child ( + parent_id varchar REFERENCES parent(id) + ); + `, + }, + { + name: "text_fk_references_varchar_pk", + sql: ` + CREATE TABLE parent (id varchar PRIMARY KEY); + CREATE TABLE child ( + parent_id text REFERENCES parent(id) + ); + `, + }, + { + name: "bpchar_fk_references_text_pk", + sql: ` + CREATE TABLE parent (id text PRIMARY KEY); + CREATE TABLE child ( + parent_id character REFERENCES parent(id) + ); + `, + }, + { + name: "text_fk_references_bpchar_pk", + sql: ` + CREATE TABLE parent (id character PRIMARY KEY); + CREATE TABLE child ( + parent_id text REFERENCES parent(id) + ); + `, + }, + { + name: "domain_fk_references_base_pk", + sql: ` + CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0); + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id positive_int REFERENCES parent(id) + ); + `, + }, + { + name: "base_fk_references_domain_pk", + sql: ` + CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0); + CREATE TABLE parent (id positive_int PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) + ); + `, + }, + { + name: "same_domain_fk", + sql: ` + CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0); + CREATE TABLE parent (id positive_int PRIMARY KEY); + CREATE TABLE child ( + parent_id positive_int REFERENCES parent(id) + ); + `, + }, + { + name: "different_domain_same_base_fk", + sql: ` + CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0); + CREATE DOMAIN other_int AS integer CHECK (VALUE > 0); + CREATE TABLE parent (id positive_int PRIMARY KEY); + CREATE TABLE child ( + parent_id other_int REFERENCES parent(id) + ); + `, + }, + { + name: "bigint_fk_references_integer_pk_composite", + sql: ` + CREATE TABLE parent (id integer, tenant_id integer, PRIMARY KEY (id, tenant_id)); + CREATE TABLE child ( + parent_id bigint, + tenant_id bigint, + FOREIGN KEY (parent_id, tenant_id) REFERENCES parent(id, tenant_id) + ); + `, + }, + { + name: "fk_references_unique_index_cross_type", + sql: ` + CREATE TABLE parent (id integer UNIQUE); + CREATE TABLE child ( + parent_id bigint REFERENCES parent(id) + ); + `, + }, + { + name: "fk_references_unique_index_with_include_columns", + sql: ` + CREATE TABLE parent (id integer, extra text); + CREATE UNIQUE INDEX parent_id_idx ON parent (id) INCLUDE (extra); + CREATE TABLE child ( + parent_id bigint REFERENCES parent(id) + ); + `, + }, + { + name: "temporary_zero_column_table", + sql: `CREATE TEMPORARY TABLE zc ();`, + }, + { + name: "unlogged_zero_column_table", + sql: `CREATE UNLOGGED TABLE zc ();`, + }, + { + name: "create_table_like_zero_column_table", + sql: ` + CREATE TABLE zc (); + CREATE TABLE zc_like (LIKE zc); + `, + }, + { + name: "create_table_as_zero_output_columns", + sql: `CREATE TABLE zc AS SELECT;`, + }, + { + name: "create_view_zero_output_columns", + sql: `CREATE VIEW v_zc AS SELECT;`, + }, + { + name: "inherited_zero_column_parent", + sql: ` + CREATE TABLE parent (); + CREATE TABLE child () INHERITS (parent); + `, + }, + { + name: "alter_zero_column_table_add_column", + sql: ` + CREATE TABLE zc (); + ALTER TABLE zc ADD COLUMN id integer; + `, + }, + { + name: "alter_table_drop_column_to_zero_columns", + sql: ` + CREATE TABLE zc (id integer); + ALTER TABLE zc DROP COLUMN id; + `, + }, + { + name: "partitioned_index_attach_wrong_schema_parent_qualified", + sql: ` + CREATE SCHEMA pidx; + CREATE TABLE pidx.parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE pidx.child PARTITION OF pidx.parent FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_id_idx ON ONLY pidx.parent (id); + CREATE INDEX child_id_idx ON pidx.child (id); + ALTER INDEX pidx.parent_id_idx ATTACH PARTITION pidx.child_id_idx; + `, + }, + { + name: "partitioned_index_attach_parent_index_first", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE INDEX parent_id_idx ON ONLY parent (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + `, + }, + { + name: "partitioned_index_attach_child_table_first", + sql: ` + CREATE TABLE child (id integer); + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_id_idx ON ONLY parent (id); + CREATE INDEX child_id_idx ON child (id); + ALTER INDEX parent_id_idx ATTACH PARTITION child_id_idx; + `, + }, + { + name: "partition_table_attach_records_dependency", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child (id integer); + ALTER TABLE parent ATTACH PARTITION child FOR VALUES FROM (0) TO (10); + `, + }, + { + name: "partitioned_unique_index_attach", + sql: ` + CREATE TABLE parent (id integer NOT NULL) PARTITION BY RANGE (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + CREATE UNIQUE INDEX parent_id_idx ON ONLY parent (id); + CREATE UNIQUE INDEX child_id_idx ON child (id); + ALTER INDEX parent_id_idx ATTACH PARTITION child_id_idx; + `, + }, + { + name: "partitioned_expression_index_attach", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_expr_idx ON ONLY parent ((id + 1)); + CREATE INDEX child_expr_idx ON child ((id + 1)); + ALTER INDEX parent_expr_idx ATTACH PARTITION child_expr_idx; + `, + }, + { + name: "partitioned_partial_index_attach", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_partial_idx ON ONLY parent (id) WHERE id > 0; + CREATE INDEX child_partial_idx ON child (id) WHERE id > 0; + ALTER INDEX parent_partial_idx ATTACH PARTITION child_partial_idx; + `, + }, + { + name: "partition_default_bound", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child_default PARTITION OF parent DEFAULT; + `, + }, + { + name: "partition_table_detach", + sql: ` + CREATE TABLE parent (id integer) PARTITION BY RANGE (id); + CREATE TABLE child PARTITION OF parent FOR VALUES FROM (0) TO (10); + ALTER TABLE parent DETACH PARTITION child; + `, + }, + { + name: "generated_column_expression", + sql: ` + CREATE TABLE t ( + a integer, + b integer GENERATED ALWAYS AS (a + 1) STORED + ); + `, + }, + { + name: "identity_column_dependency", + sql: ` + CREATE TABLE t ( + id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY + ); + `, + }, + { + name: "column_default_function_dependency", + sql: ` + CREATE FUNCTION next_code() RETURNS integer LANGUAGE sql AS 'SELECT 1'; + CREATE TABLE t ( + code integer DEFAULT next_code() + ); + `, + }, + { + name: "column_default_sequence_dependency", + sql: ` + CREATE SEQUENCE s; + CREATE TABLE t ( + id integer DEFAULT nextval('s') + ); + `, + }, + { + name: "check_constraint_function_dependency", + sql: ` + CREATE FUNCTION is_positive(x integer) RETURNS boolean LANGUAGE sql AS 'SELECT $1 > 0'; + CREATE TABLE t ( + x integer CHECK (is_positive(x)) + ); + `, + }, + { + name: "exclusion_constraint_resolves_operator_class", + sql: ` + CREATE TABLE t (r int4range); + ALTER TABLE t ADD CONSTRAINT t_r_excl EXCLUDE USING gist (r WITH &&); + `, + }, + { + name: "unique_nulls_not_distinct", + sql: ` + CREATE TABLE t (x integer); + ALTER TABLE t ADD CONSTRAINT t_x_key UNIQUE NULLS NOT DISTINCT (x); + `, + }, + { + name: "deferrable_unique_constraint", + sql: ` + CREATE TABLE t ( + x integer UNIQUE DEFERRABLE INITIALLY DEFERRED + ); + `, + }, + { + name: "deferrable_foreign_key", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED + ); + `, + }, + { + name: "not_valid_foreign_key", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child (parent_id integer); + ALTER TABLE child ADD CONSTRAINT child_parent_fk + FOREIGN KEY (parent_id) REFERENCES parent(id) NOT VALID; + `, + }, + { + name: "validate_constraint", + sql: ` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child (parent_id integer); + ALTER TABLE child ADD CONSTRAINT child_parent_fk + FOREIGN KEY (parent_id) REFERENCES parent(id) NOT VALID; + ALTER TABLE child VALIDATE CONSTRAINT child_parent_fk; + `, + }, + { + name: "constraint_comment", + sql: ` + CREATE TABLE t (x integer CONSTRAINT x_positive CHECK (x > 0)); + COMMENT ON CONSTRAINT x_positive ON t IS 'positive'; + `, + }, + { + name: "view_concat_ws_variadic_builtin", + sql: ` + CREATE TABLE items (a text, b integer); + CREATE VIEW v_items AS + SELECT concat_ws('-', a, b) AS label + FROM items; + `, + }, + { + name: "view_concat_ws_unknown_literals", + sql: ` + CREATE VIEW v_items AS + SELECT concat_ws('-', 'a', 'b', 'c') AS label; + `, + }, + { + name: "view_concat_ws_mixed_types", + sql: ` + CREATE TABLE items (a text, b integer, c uuid, d timestamp); + CREATE VIEW v_items AS + SELECT concat_ws('-', a, b, c, d) AS label + FROM items; + `, + }, + { + name: "view_jsonb_build_object_variadic_builtin", + sql: ` + CREATE TABLE items (id integer, name text); + CREATE VIEW v_items AS + SELECT jsonb_build_object('id', id, 'name', name) AS payload + FROM items; + `, + }, + { + name: "view_concat_variadic_builtin", + sql: ` + CREATE TABLE items (a text, b integer, c timestamp); + CREATE VIEW v_items AS + SELECT concat(a, b, c) AS label + FROM items; + `, + }, + { + name: "view_format_variadic_builtin", + sql: ` + CREATE TABLE items (a text, b integer); + CREATE VIEW v_items AS + SELECT format('%s %s', a, b) AS label + FROM items; + `, + }, + { + name: "view_json_build_object_variadic_builtin", + sql: ` + CREATE TABLE items (id integer, name text); + CREATE VIEW v_items AS + SELECT json_build_object('id', id, 'name', name) AS payload + FROM items; + `, + }, + { + name: "view_json_build_array_variadic_builtin", + sql: ` + CREATE TABLE items (id integer, name text); + CREATE VIEW v_items AS + SELECT json_build_array(id, name) AS payload + FROM items; + `, + }, + { + name: "view_jsonb_build_array_variadic_builtin", + sql: ` + CREATE TABLE items (id integer, name text); + CREATE VIEW v_items AS + SELECT jsonb_build_array(id, name) AS payload + FROM items; + `, + }, + { + name: "view_user_function_default_argument", + sql: ` + CREATE FUNCTION add_default(a integer, b integer DEFAULT 1) + RETURNS integer + LANGUAGE sql + AS 'SELECT a + b'; + CREATE VIEW v_default_arg AS + SELECT add_default(2) AS value; + `, + }, + { + name: "view_user_variadic_function_expanded_arguments", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_arg AS + SELECT count_variadic(1, 2, 3) AS value; + `, + }, + { + name: "view_user_variadic_function_explicit_array", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_arg AS + SELECT count_variadic(VARIADIC ARRAY[1, 2, 3]::integer[]) AS value; + `, + }, + { + name: "view_user_variadic_function_one_arg", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_one AS + SELECT count_variadic(1) AS value; + `, + }, + { + name: "view_user_variadic_function_mixed_coercible_args", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums bigint[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_mixed AS + SELECT count_variadic(1::smallint, 2::integer, 3::bigint) AS value; + `, + }, + { + name: "view_user_variadic_function_explicit_null_array", + sql: ` + CREATE FUNCTION count_variadic(VARIADIC nums integer[]) + RETURNS integer + LANGUAGE sql + AS 'SELECT cardinality(nums)'; + CREATE VIEW v_variadic_null AS + SELECT count_variadic(VARIADIC NULL::integer[]) AS value; + `, + }, + { + name: "view_jsonb_extract_with_unnest_text_alias_column", + sql: ` + CREATE TABLE docs (data jsonb); + CREATE VIEW v_docs AS + SELECT d.data ->> c.col_name AS value + FROM docs d, + unnest(ARRAY['name', 'status']::text[]) AS c(col_name); + `, + }, + { + name: "view_jsonb_extract_with_unnest_varchar_alias_column", + sql: ` + CREATE TABLE docs (data jsonb); + CREATE VIEW v_docs AS + SELECT d.data ->> c.col_name AS value + FROM docs d, + unnest(ARRAY['name', 'status']::varchar[]) AS c(col_name); + `, + }, + { + name: "view_jsonb_extract_with_cte_alias_column", + sql: ` + CREATE TABLE docs (data jsonb); + CREATE VIEW v_docs AS + WITH c(col_name) AS (SELECT 'name'::text) + SELECT d.data ->> c.col_name AS value + FROM docs d, c; + `, + }, + { + name: "view_unnest_integer_alias_column", + sql: ` + CREATE VIEW v_nums AS + SELECT n.value + 1 AS next_value + FROM unnest(ARRAY[1, 2, 3]::integer[]) AS n(value); + `, + }, + { + name: "view_unnest_bigint_alias_column", + sql: ` + CREATE VIEW v_nums AS + SELECT n.value + 1 AS next_value + FROM unnest(ARRAY[1, 2, 3]::bigint[]) AS n(value); + `, + }, + { + name: "view_unnest_uuid_alias_column", + sql: ` + CREATE VIEW v_ids AS + SELECT n.value::text AS id_text + FROM unnest(ARRAY['00000000-0000-0000-0000-000000000001']::uuid[]) AS n(value); + `, + }, + { + name: "view_unnest_jsonb_alias_column", + sql: ` + CREATE VIEW v_payloads AS + SELECT n.value ->> 'name' AS name + FROM unnest(ARRAY['{"name":"one"}'::jsonb]::jsonb[]) AS n(value); + `, + }, + { + name: "view_unnest_domain_array_alias_column", + sql: ` + CREATE DOMAIN loader_text_domain AS text; + CREATE VIEW v_domain_values AS + SELECT n.value::text AS value + FROM unnest(ARRAY['one']::loader_text_domain[]) AS n(value); + `, + }, + { + name: "view_array_polymorphic_builtins", + sql: ` + CREATE VIEW v_arrays AS + SELECT + array_length(ARRAY[1, 2, 3]::integer[], 1) AS len, + array_position(ARRAY['a', 'b']::text[], 'b') AS text_pos, + array_position(ARRAY[1, 2, 3]::integer[], 2) AS int_pos, + array_append(ARRAY[1, 2]::integer[], 3) AS appended, + array_prepend(0, ARRAY[1, 2]::integer[]) AS prepended; + `, + }, + { + name: "view_anycompatible_common_type_builtins", + sql: ` + CREATE VIEW v_common AS + SELECT + coalesce(NULL, 1, 2::bigint) AS c, + greatest(1, 2::bigint) AS g, + least(1, 2::bigint) AS l; + `, + }, + { + name: "view_record_returning_builtin_alias_columns", + sql: ` + CREATE VIEW v_each AS + SELECT e.key, e.value + FROM jsonb_each_text('{"a":"b"}'::jsonb) AS e(key, value); + `, + }, + { + name: "view_record_returning_user_function_alias_columns", + sql: ` + CREATE FUNCTION loader_pair(OUT id integer, OUT name text) + RETURNS record + LANGUAGE sql + AS 'SELECT 1, ''one'''; + CREATE VIEW v_pair AS + SELECT p.id, p.name + FROM loader_pair() AS p(id, name); + `, + }, + { + name: "view_returns_table_function_alias_columns", + sql: ` + CREATE FUNCTION loader_table() + RETURNS TABLE(id integer, name text) + LANGUAGE sql + AS 'SELECT 1, ''one'''; + CREATE VIEW v_table AS + SELECT t.id, t.name + FROM loader_table() AS t(id, name); + `, + }, + { + name: "view_srf_alias_column_list", + sql: ` + CREATE VIEW v_srf AS + SELECT x.value + 1 AS next_value + FROM generate_series(1, 3) AS x(value); + `, + }, + { + name: "view_unknown_literal_and_default_resolution", + sql: ` + CREATE FUNCTION loader_default_one(a integer, b integer DEFAULT 1) + RETURNS integer LANGUAGE sql AS 'SELECT a + b'; + CREATE FUNCTION loader_default_two(a integer, b integer DEFAULT 1, c integer DEFAULT 2) + RETURNS integer LANGUAGE sql AS 'SELECT a + b + c'; + CREATE FUNCTION loader_text_arg(a text) RETURNS text LANGUAGE sql AS 'SELECT a'; + CREATE FUNCTION loader_int_arg(a integer) RETURNS integer LANGUAGE sql AS 'SELECT a'; + CREATE FUNCTION loader_null_arg(a text, b integer DEFAULT 1) RETURNS text LANGUAGE sql AS 'SELECT a'; + CREATE VIEW v_resolve AS + SELECT + loader_default_one(1) AS d1, + loader_default_two(1) AS d2, + loader_text_arg('x') AS text_value, + loader_int_arg(1) AS int_value, + loader_null_arg(NULL) AS null_value; + `, + }, + { + name: "view_operator_resolution_matrix", + sql: ` + CREATE TABLE op_items ( + j json, + jb jsonb, + t text, + i integer, + b bigint, + n numeric, + d date, + ts timestamp, + ia integer[], + r int4range + ); + CREATE VIEW v_ops AS + SELECT + jb -> 'name' AS jb_obj_text, + jb -> 0 AS jb_obj_int, + jb ->> 'name' AS jb_text_text, + jb ->> 0 AS jb_text_int, + j -> 'name' AS j_obj_text, + j ->> 'name' AS j_text_text, + t || 'x' AS text_unknown_right, + 'x' || t AS text_unknown_left, + i + b AS int_bigint, + b + i AS bigint_int, + n + i AS numeric_int, + d + 1 AS date_plus_int, + ts + interval '1 day' AS ts_plus_interval, + t LIKE 'a%' AS text_like, + t ILIKE 'a%' AS text_ilike, + ia @> ARRAY[1]::integer[] AS array_contains, + jb @> '{"a":1}'::jsonb AS jsonb_contains, + r && int4range(1, 3) AS range_overlaps + FROM op_items; + `, + }, + { + name: "view_cte_range_resolution", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_base_items AS + WITH cte1 AS (SELECT id FROM base_items) + SELECT id FROM cte1; + `, + }, + { + name: "view_cte_column_alias_resolution", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_base_items AS + WITH cte1(item_id) AS (SELECT id FROM base_items) + SELECT item_id FROM cte1; + `, + }, + { + name: "view_cte_shadows_base_table", + sql: ` + CREATE TABLE cte1 (id integer); + CREATE TABLE base_items (id integer); + CREATE VIEW v_base_items AS + WITH cte1 AS (SELECT id FROM base_items) + SELECT id FROM cte1; + `, + }, + { + name: "view_multiple_ctes_reference_prior_cte", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_multi_cte AS + WITH c1 AS (SELECT id FROM base_items), + c2 AS (SELECT id + 1 AS next_id FROM c1) + SELECT next_id FROM c2; + `, + }, + { + name: "view_cte_same_column_names_as_base_table", + sql: ` + CREATE TABLE base_items (id integer, value text); + CREATE VIEW v_cte_same_cols AS + WITH c AS (SELECT id, value FROM base_items) + SELECT id, value FROM c; + `, + }, + { + name: "view_nested_subquery_references_outer_cte", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_nested_outer_cte AS + WITH c AS (SELECT id FROM base_items) + SELECT (SELECT max(id) FROM c) AS max_id; + `, + }, + { + name: "view_nested_with_shadows_outer_cte", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_nested_shadow_cte AS + WITH c AS (SELECT id FROM base_items) + SELECT id + FROM ( + WITH c AS (SELECT 2 AS id) + SELECT id FROM c + ) s; + `, + }, + { + name: "view_nested_with_references_outer_cte", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_nested_outer_ref AS + WITH c AS (SELECT id FROM base_items) + SELECT id + FROM ( + WITH d AS (SELECT id + 1 AS id FROM c) + SELECT id FROM d + ) s; + `, + }, + { + name: "view_recursive_cte", + sql: ` + CREATE VIEW v_recursive AS + WITH RECURSIVE r(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM r WHERE n < 3 + ) + SELECT n FROM r; + `, + }, + { + name: "view_cte_materialized", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_cte_mat AS + WITH c AS MATERIALIZED (SELECT id FROM base_items) + SELECT id FROM c; + `, + }, + { + name: "view_cte_not_materialized", + sql: ` + CREATE TABLE base_items (id integer); + CREATE VIEW v_cte_not_mat AS + WITH c AS NOT MATERIALIZED (SELECT id FROM base_items) + SELECT id FROM c; + `, + }, + { + name: "view_cte_column_aliases_fewer_than_output", + sql: ` + CREATE VIEW v_cte_fewer_aliases AS + WITH c(a) AS (SELECT 1 AS x, 2 AS y) + SELECT a, y FROM c; + `, + }, + { + name: "view_later_lateral_references_previous_lateral_alias", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_lateral_base AS + SELECT s2.next_id + FROM lateral_base b, + LATERAL (SELECT b.id AS id) s1, + LATERAL (SELECT s1.id + 1 AS next_id) s2; + `, + }, + { + name: "view_cross_join_lateral_references_left", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_cross_lateral AS + SELECT s.id + FROM lateral_base b + CROSS JOIN LATERAL (SELECT b.id AS id) s; + `, + }, + { + name: "view_inner_join_lateral_references_left", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_inner_lateral AS + SELECT s.id + FROM lateral_base b + INNER JOIN LATERAL (SELECT b.id AS id) s ON true; + `, + }, + { + name: "view_lateral_references_multiple_prior_items", + sql: ` + CREATE TABLE a (id integer); + CREATE TABLE b (id integer); + CREATE VIEW v_multi_lateral AS + SELECT s.sum_id + FROM a, b, + LATERAL (SELECT a.id + b.id AS sum_id) s; + `, + }, + { + name: "view_third_lateral_references_two_prior_lateral_aliases", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_chain_lateral AS + SELECT s3.total + FROM lateral_base b, + LATERAL (SELECT b.id AS id1) s1, + LATERAL (SELECT s1.id1 + 1 AS id2) s2, + LATERAL (SELECT s1.id1 + s2.id2 AS total) s3; + `, + }, + { + name: "view_lateral_unnest_references_left", + sql: ` + CREATE TABLE array_items (id integer, vals integer[]); + CREATE VIEW v_lateral_unnest AS + SELECT u.val + array_items.id AS value + FROM array_items, + LATERAL unnest(array_items.vals) AS u(val); + `, + }, + { + name: "view_lateral_generate_series_references_left", + sql: ` + CREATE TABLE series_items (n integer); + CREATE VIEW v_lateral_series AS + SELECT g.x + FROM series_items, + LATERAL generate_series(1, series_items.n) AS g(x); + `, + }, + { + name: "view_rows_from_with_ordinality", + sql: ` + CREATE VIEW v_rows_from AS + SELECT x.val, x.ord + FROM ROWS FROM (unnest(ARRAY['a', 'b']::text[])) WITH ORDINALITY AS x(val, ord); + `, + }, + { + name: "view_lateral_record_coldeflist", + sql: ` + CREATE TABLE json_items (payload jsonb); + CREATE VIEW v_lateral_record AS + SELECT r.id, r.name + FROM json_items, + LATERAL jsonb_to_record(json_items.payload) AS r(id integer, name text); + `, + }, + { + name: "view_lateral_join_scope_nested", + sql: ` + CREATE TABLE a (id integer); + CREATE TABLE b (id integer); + CREATE VIEW v_lateral_join_scope AS + SELECT s.sum_id + FROM a + JOIN b ON true + JOIN LATERAL (SELECT a.id + b.id AS sum_id) s ON true; + `, + }, + { + name: "view_correlated_subquery_matrix", + sql: ` + CREATE TABLE parents (id integer); + CREATE TABLE children (parent_id integer, value integer); + CREATE VIEW v_correlated AS + SELECT + p.id, + (SELECT max(c.value) FROM children c WHERE c.parent_id = p.id) AS max_value + FROM parents p + WHERE EXISTS (SELECT 1 FROM children c WHERE c.parent_id = p.id) + AND p.id IN (SELECT c.parent_id FROM children c WHERE c.value > p.id) + AND (SELECT count(*) FROM children c WHERE c.parent_id = p.id) > 0; + `, + }, + { + name: "view_nested_correlated_subquery_levels_up_two", + sql: ` + CREATE TABLE parents (id integer); + CREATE TABLE children (parent_id integer, value integer); + CREATE VIEW v_nested_correlated AS + SELECT p.id + FROM parents p + WHERE EXISTS ( + SELECT 1 + FROM children c + WHERE EXISTS ( + SELECT 1 + WHERE c.parent_id = p.id + ) + ); + `, + }, + { + name: "view_star_from_unnest_alias", + sql: ` + CREATE VIEW v_star_unnest AS + SELECT * + FROM unnest(ARRAY['a', 'b']::text[]) AS u(val); + `, + }, + { + name: "view_star_from_multi_unnest_alias", + sql: ` + CREATE VIEW v_star_multi_unnest AS + SELECT * + FROM unnest(ARRAY[1, 2]::integer[], ARRAY['a', 'b']::text[]) AS u(id, name); + `, + }, + { + name: "view_jsonb_to_record_coldeflist", + sql: ` + CREATE VIEW v_jsonb_record AS + SELECT r.id, r.name + FROM jsonb_to_record('{"id":1,"name":"one"}'::jsonb) AS r(id integer, name text); + `, + }, + { + name: "view_jsonb_to_recordset_coldeflist", + sql: ` + CREATE VIEW v_jsonb_recordset AS + SELECT r.id, r.name + FROM jsonb_to_recordset('[{"id":1,"name":"one"}]'::jsonb) AS r(id integer, name text); + `, + }, + { + name: "view_srf_in_select_target", + sql: ` + CREATE VIEW v_srf_target AS + SELECT generate_series(1, 3) AS value; + `, + }, + { + name: "view_star_expansion_through_cte", + sql: ` + CREATE TABLE star_items (id integer, name text); + CREATE VIEW v_star_cte AS + WITH c AS (SELECT id, name FROM star_items) + SELECT * FROM c; + `, + }, + { + name: "view_star_expansion_through_lateral", + sql: ` + CREATE TABLE star_items (id integer, vals integer[]); + CREATE VIEW v_star_lateral AS + SELECT * + FROM star_items, + LATERAL unnest(star_items.vals) AS u(val); + `, + }, + { + name: "view_column_alias_list_overrides_target_names", + sql: `CREATE VIEW v_alias_override(a, b) AS SELECT 1 AS x, 2 AS y;`, + }, + { + name: "view_column_aliases_fewer_than_output", + sql: `CREATE VIEW v_alias_fewer(a) AS SELECT 1 AS x, 2 AS y;`, + }, + { + name: "object_namespace_alter_rename_matrix", + sql: ` + CREATE TABLE t (id integer); + CREATE INDEX t_id_idx ON t (id); + ALTER INDEX t_id_idx RENAME TO t_id_idx2; + CREATE SEQUENCE s; + ALTER SEQUENCE s RENAME TO s2; + ALTER SEQUENCE s2 OWNER TO CURRENT_USER; + CREATE VIEW v AS SELECT id FROM t; + ALTER VIEW v RENAME TO v2; + CREATE MATERIALIZED VIEW mv AS SELECT id FROM t; + ALTER MATERIALIZED VIEW mv RENAME TO mv2; + CREATE TYPE mood AS ENUM ('sad', 'ok'); + ALTER TYPE mood RENAME TO mood2; + CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0); + ALTER DOMAIN positive_int RENAME TO positive_int2; + CREATE FUNCTION f(a integer) RETURNS integer LANGUAGE sql AS 'SELECT a'; + ALTER FUNCTION f(integer) RENAME TO f2; + CREATE PROCEDURE p(a integer) LANGUAGE sql AS 'SELECT 1'; + ALTER PROCEDURE p(integer) RENAME TO p2; + ALTER ROUTINE f2(integer) OWNER TO CURRENT_USER; + `, + }, + { + name: "object_namespace_comment_drop_matrix", + sql: ` + CREATE TABLE t (id integer); + CREATE INDEX t_id_idx ON t (id); + COMMENT ON INDEX t_id_idx IS 'idx'; + COMMENT ON INDEX t_id_idx IS NULL; + CREATE SEQUENCE s; + COMMENT ON SEQUENCE s IS 'seq'; + COMMENT ON SEQUENCE s IS NULL; + CREATE TYPE mood AS ENUM ('sad', 'ok'); + COMMENT ON TYPE mood IS 'type'; + COMMENT ON TYPE mood IS NULL; + CREATE FUNCTION f(a integer) RETURNS integer LANGUAGE sql AS 'SELECT a'; + COMMENT ON FUNCTION f(a integer) IS 'func'; + COMMENT ON FUNCTION f(integer) IS NULL; + DROP FUNCTION f(integer); + DROP INDEX t_id_idx; + DROP SEQUENCE s; + DROP TYPE mood; + `, + }, + { + name: "object_namespace_grant_revoke_matrix", + sql: ` + CREATE TABLE t (id integer); + CREATE VIEW v AS SELECT id FROM t; + CREATE SEQUENCE s; + CREATE FUNCTION f(a integer) RETURNS integer LANGUAGE sql AS 'SELECT a'; + GRANT SELECT ON TABLE t TO PUBLIC; + REVOKE SELECT ON TABLE t FROM PUBLIC; + GRANT SELECT ON TABLE v TO PUBLIC; + REVOKE SELECT ON TABLE v FROM PUBLIC; + GRANT USAGE ON SEQUENCE s TO PUBLIC; + REVOKE USAGE ON SEQUENCE s FROM PUBLIC; + GRANT EXECUTE ON FUNCTION f(integer) TO PUBLIC; + REVOKE EXECUTE ON FUNCTION f(integer) FROM PUBLIC; + `, + }, + { + name: "object_namespace_set_schema_matrix", + sql: ` + CREATE SCHEMA target_schema; + CREATE TABLE t (id integer); + ALTER TABLE t SET SCHEMA target_schema; + CREATE SEQUENCE s; + ALTER SEQUENCE s SET SCHEMA target_schema; + CREATE TYPE mood AS ENUM ('sad', 'ok'); + ALTER TYPE mood SET SCHEMA target_schema; + CREATE FUNCTION f(a integer) RETURNS integer LANGUAGE sql AS 'SELECT a'; + ALTER FUNCTION f(integer) SET SCHEMA target_schema; + `, + }, + { + name: "alter_index_depends_on_extension", + sql: ` + CREATE TABLE t (id integer); + CREATE INDEX t_id_idx ON t (id); + ALTER INDEX t_id_idx DEPENDS ON EXTENSION plpgsql; + `, + }, + { + name: "drop_owned_by_loaded_objects", + sql: ` + CREATE ROLE loader_owner; + CREATE TABLE t (id integer); + CREATE SEQUENCE s; + ALTER TABLE t OWNER TO loader_owner; + ALTER SEQUENCE s OWNER TO loader_owner; + DROP OWNED BY loader_owner; + DROP ROLE loader_owner; + `, + }, + { + name: "view_left_join_lateral_references_left_relation", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_lateral_left AS + SELECT b.id, s.next_id + FROM lateral_base b + LEFT JOIN LATERAL (SELECT b.id + 1 AS next_id) s ON true; + `, + }, + { + name: "view_chained_lateral_references_base_and_prior_alias", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_lateral_base AS + SELECT s3.value + FROM lateral_base b, + LATERAL (SELECT b.id AS id) s1, + LATERAL (SELECT s1.id + b.id AS id2) s2, + LATERAL (SELECT s2.id2 + s1.id AS value) s3; + `, + }, + { + name: "view_cross_join_lateral_references_left_relation", + sql: ` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_lateral_cross AS + SELECT b.id, s.next_id + FROM lateral_base b + CROSS JOIN LATERAL (SELECT b.id + 1 AS next_id) s; + `, + }, + { + name: "alter_index_attach_partition", + sql: ` + CREATE TABLE parent_idx_attach (id integer) PARTITION BY RANGE (id); + CREATE TABLE child_idx_attach PARTITION OF parent_idx_attach + FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_idx_attach_id_idx ON ONLY parent_idx_attach (id); + CREATE INDEX child_idx_attach_id_idx ON child_idx_attach (id); + ALTER INDEX parent_idx_attach_id_idx ATTACH PARTITION child_idx_attach_id_idx; + `, + }, + { + name: "alter_index_attach_partition_schema_qualified", + sql: ` + CREATE SCHEMA part_s; + CREATE TABLE part_s.parent_idx_attach (id integer) PARTITION BY RANGE (id); + CREATE TABLE part_s.child_idx_attach PARTITION OF part_s.parent_idx_attach + FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_idx_attach_id_idx ON ONLY part_s.parent_idx_attach (id); + CREATE INDEX child_idx_attach_id_idx ON part_s.child_idx_attach (id); + ALTER INDEX part_s.parent_idx_attach_id_idx ATTACH PARTITION part_s.child_idx_attach_id_idx; + `, + }, + } +} + +func TestLoaderCompatZeroColumnTable(t *testing.T) { + c, err := LoadSQL(`CREATE TABLE zc ();`) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } + + _, rel, err := c.findRelation("", "zc") + if err != nil { + t.Fatalf("findRelation failed: %v", err) + } + if got := len(rel.Columns); got != 0 { + t.Fatalf("columns: got %d, want 0", got) + } +} + +func TestLoaderCompatForeignKeyMatchSimple(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id integer, + FOREIGN KEY (parent_id) REFERENCES parent(id) MATCH SIMPLE + ); + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatCommentOnFunctionWithArgumentNames(t *testing.T) { + _, err := LoadSQL(` + CREATE FUNCTION f(a integer, b text) RETURNS integer + LANGUAGE sql AS 'SELECT 1'; + COMMENT ON FUNCTION f(a integer, b text) IS 'comment'; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatReturnsTablePlpgsql(t *testing.T) { + _, err := LoadSQL(` + CREATE FUNCTION f() + RETURNS TABLE(id integer, name text) + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN QUERY SELECT 1, 'one'::text; + END + $$; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatBigintForeignKeyReferencesIntegerPrimaryKey(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE parent (id integer PRIMARY KEY); + CREATE TABLE child ( + parent_id bigint REFERENCES parent(id) + ); + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatViewConcatWSVariadicBuiltin(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE items (a text, b integer); + CREATE VIEW v_items AS + SELECT concat_ws('-', a, b) AS label + FROM items; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatViewJsonbExtractWithUnnestAliasColumn(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE docs (data jsonb); + CREATE VIEW v_docs AS + SELECT d.data ->> c.col_name AS value + FROM docs d, + unnest(ARRAY['name', 'status']::text[]) AS c(col_name); + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatViewCTERangeResolution(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE base_items (id integer); + CREATE VIEW v_base_items AS + WITH cte1 AS (SELECT id FROM base_items) + SELECT id FROM cte1; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatViewLaterLateralReferencesPreviousLateralAlias(t *testing.T) { + _, err := LoadSQL(` + CREATE TABLE lateral_base (id integer); + CREATE VIEW v_lateral_base AS + SELECT s2.next_id + FROM lateral_base b, + LATERAL (SELECT b.id AS id) s1, + LATERAL (SELECT s1.id + 1 AS next_id) s2; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } +} + +func TestLoaderCompatAlterIndexAttachPartition(t *testing.T) { + c, err := LoadSQL(` + CREATE TABLE parent_idx_attach (id integer) PARTITION BY RANGE (id); + CREATE TABLE child_idx_attach PARTITION OF parent_idx_attach + FOR VALUES FROM (0) TO (10); + CREATE INDEX parent_idx_attach_id_idx ON ONLY parent_idx_attach (id); + CREATE INDEX child_idx_attach_id_idx ON child_idx_attach (id); + ALTER INDEX parent_idx_attach_id_idx ATTACH PARTITION child_idx_attach_id_idx; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } + + schema, err := c.resolveTargetSchema("") + if err != nil { + t.Fatalf("resolve schema: %v", err) + } + parentIdx := schema.Indexes["parent_idx_attach_id_idx"] + childIdx := schema.Indexes["child_idx_attach_id_idx"] + if parentIdx == nil || childIdx == nil { + t.Fatalf("expected parent and child indexes to be registered") + } + for _, dep := range c.deps { + if dep.ObjType == 'i' && dep.ObjOID == childIdx.OID && + dep.RefType == 'i' && dep.RefOID == parentIdx.OID && + dep.DepType == DepInternal { + return + } + } + t.Fatalf("missing internal child-index to parent-index dependency") +} + +func TestLoaderCompatPhase3PartitionTableAttachDependency(t *testing.T) { + c, err := LoadSQL(` + CREATE TABLE parent_attach_dep (id integer) PARTITION BY RANGE (id); + CREATE TABLE child_attach_dep (id integer); + ALTER TABLE parent_attach_dep ATTACH PARTITION child_attach_dep + FOR VALUES FROM (0) TO (10); + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } + + schema, err := c.resolveTargetSchema("") + if err != nil { + t.Fatalf("resolve schema: %v", err) + } + parent := schema.Relations["parent_attach_dep"] + child := schema.Relations["child_attach_dep"] + if parent == nil || child == nil { + t.Fatalf("expected parent and child relations to be registered") + } + if !hasDependency(c, 'r', child.OID, 0, 'r', parent.OID, 0, DepAuto) { + t.Fatalf("missing auto child-table to parent-table dependency") + } +} + +func TestLoaderCompatPhase3ExpressionDependencies(t *testing.T) { + c, err := LoadSQL(` + CREATE SEQUENCE loader_dep_seq; + CREATE FUNCTION loader_dep_next_code() RETURNS integer LANGUAGE sql AS 'SELECT 1'; + CREATE FUNCTION loader_dep_is_positive(x integer) RETURNS boolean LANGUAGE sql AS 'SELECT $1 > 0'; + CREATE TABLE loader_dep_t ( + base integer, + generated integer GENERATED ALWAYS AS (base + 1) STORED, + code integer DEFAULT loader_dep_next_code(), + seq_id integer DEFAULT nextval('loader_dep_seq'), + CONSTRAINT loader_dep_positive CHECK (loader_dep_is_positive(base)) + ); + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } + + schema, err := c.resolveTargetSchema("") + if err != nil { + t.Fatalf("resolve schema: %v", err) + } + rel := schema.Relations["loader_dep_t"] + if rel == nil { + t.Fatalf("expected table to be registered") + } + nextCode := c.findUserProcsByName(schema, "loader_dep_next_code") + if len(nextCode) != 1 { + t.Fatalf("expected loader_dep_next_code function, got %d", len(nextCode)) + } + isPositive := c.findUserProcsByName(schema, "loader_dep_is_positive") + if len(isPositive) != 1 { + t.Fatalf("expected loader_dep_is_positive function, got %d", len(isPositive)) + } + seq := schema.Sequences["loader_dep_seq"] + if seq == nil { + t.Fatalf("expected sequence to be registered") + } + + generatedCol := rel.Columns[1] + codeCol := rel.Columns[2] + seqCol := rel.Columns[3] + + if !hasDependency(c, 'r', rel.OID, int32(generatedCol.AttNum), 'r', rel.OID, int32(rel.Columns[0].AttNum), DepNormal) { + t.Fatalf("missing generated column dependency on referenced base column") + } + if !hasDependency(c, 'r', rel.OID, int32(codeCol.AttNum), 'f', nextCode[0].OID, 0, DepNormal) { + t.Fatalf("missing column default dependency on user function") + } + if !hasDependency(c, 'r', rel.OID, int32(seqCol.AttNum), 's', seq.OID, 0, DepNormal) { + t.Fatalf("missing column default dependency on referenced sequence") + } + + var checkOID uint32 + for _, con := range c.ConstraintsOf(rel.OID) { + if con.Name == "loader_dep_positive" { + checkOID = con.OID + break + } + } + if checkOID == 0 { + t.Fatalf("expected check constraint to be registered") + } + if !hasDependency(c, 'c', checkOID, 0, 'f', isPositive[0].OID, 0, DepNormal) { + t.Fatalf("missing check constraint dependency on user function") + } +} + +func TestLoaderCompatPhase5CTEViewDependencies(t *testing.T) { + c, err := LoadSQL(` + CREATE TABLE phase5_base (id integer, value integer); + CREATE FUNCTION phase5_inc(x integer) RETURNS integer LANGUAGE sql AS 'SELECT $1 + 1'; + CREATE VIEW phase5_view AS + WITH c AS ( + SELECT id, phase5_inc(value) AS next_value + FROM phase5_base + ) + SELECT id, next_value FROM c; + `) + if err != nil { + t.Fatalf("LoadSQL failed: %v", err) + } + + schema, err := c.resolveTargetSchema("") + if err != nil { + t.Fatalf("resolve schema: %v", err) + } + base := schema.Relations["phase5_base"] + view := schema.Relations["phase5_view"] + if base == nil || view == nil { + t.Fatalf("expected base table and view to be registered") + } + procs := c.findUserProcsByName(schema, "phase5_inc") + if len(procs) != 1 { + t.Fatalf("expected phase5_inc function, got %d", len(procs)) + } + if !hasDependency(c, 'r', view.OID, 0, 'r', base.OID, 0, DepNormal) { + t.Fatalf("missing view dependency on base table through CTE") + } + if !hasDependency(c, 'r', view.OID, 0, 'r', base.OID, int32(base.Columns[0].AttNum), DepNormal) { + t.Fatalf("missing view dependency on base column through CTE") + } + if !hasDependency(c, 'r', view.OID, 0, 'f', procs[0].OID, 0, DepNormal) { + t.Fatalf("missing view dependency on function used inside CTE") + } +} + +func hasDependency(c *Catalog, objType byte, objOID uint32, objSubID int32, refType byte, refOID uint32, refSubID int32, depType DepType) bool { + for _, dep := range c.deps { + if dep.ObjType == objType && dep.ObjOID == objOID && dep.ObjSubID == objSubID && + dep.RefType == refType && dep.RefOID == refOID && dep.RefSubID == refSubID && + dep.DepType == depType { + return true + } + } + return false +} + +func TestLoaderCompatAcceptCorpus(t *testing.T) { + for _, tc := range loaderCompatAcceptCases() { + t.Run(tc.name, func(t *testing.T) { + if _, err := LoadSQL(tc.sql); err != nil { + t.Fatalf("LoadSQL failed: %v\nSQL:\n%s", err, tc.sql) + } + }) + } +} + +func TestLoaderCompatRejectCorpus(t *testing.T) { + for _, tc := range loaderCompatRejectCases() { + t.Run(tc.name, func(t *testing.T) { + if _, err := LoadSQL(tc.sql); err == nil { + t.Fatalf("LoadSQL unexpectedly succeeded\nSQL:\n%s", tc.sql) + } + }) + } +} diff --git a/pg/catalog/query.go b/pg/catalog/query.go index 375d1e4c..bd5c8a0b 100644 --- a/pg/catalog/query.go +++ b/pg/catalog/query.go @@ -758,6 +758,11 @@ func (c *Catalog) resolveReturnType(proc *BuiltinProc, argTypes []uint32) uint32 switch retType { case ANYELEMENTOID, ANYOID, ANYNONARRAYOID, ANYCOMPATIBLEOID: + if paramOID == ANYARRAYOID || paramOID == ANYCOMPATIBLEARRAYOID { + if t := c.typeByOID[actualType]; t != nil && t.Elem != 0 { + return t.Elem + } + } return actualType case ANYARRAYOID, ANYCOMPATIBLEARRAYOID: // Return the array type of the actual arg type. diff --git a/pg/catalog/relation_test.go b/pg/catalog/relation_test.go index 847c1646..36ab5207 100644 --- a/pg/catalog/relation_test.go +++ b/pg/catalog/relation_test.go @@ -198,5 +198,14 @@ func TestCreateTableNoColumns(t *testing.T) { c := New() err := c.DefineRelation(makeCreateTableStmt("", "t", nil, nil, false), 'r') - assertErrorCode(t, err, CodeInvalidParameterValue) + if err != nil { + t.Fatalf("DefineRelation failed: %v", err) + } + rel := c.GetRelation("", "t") + if rel == nil { + t.Fatal("expected relation t") + } + if got := len(rel.Columns); got != 0 { + t.Fatalf("columns: got %d, want 0", got) + } } diff --git a/pg/catalog/sdl_deps_test.go b/pg/catalog/sdl_deps_test.go index 24f1e556..8a215ee9 100644 --- a/pg/catalog/sdl_deps_test.go +++ b/pg/catalog/sdl_deps_test.go @@ -56,7 +56,7 @@ func TestSDLExprDepsColumnDefault(t *testing.T) { }, }, { - name: "CHECK with subquery referencing table creates dependency", + name: "CHECK with subquery is rejected like PostgreSQL", sql: ` CREATE TABLE t ( val integer, @@ -64,14 +64,7 @@ func TestSDLExprDepsColumnDefault(t *testing.T) { ); CREATE TABLE lookup (id integer); `, - check: func(t *testing.T, c *Catalog) { - if c.GetRelation("public", "t") == nil { - t.Fatal("table t not found") - } - if c.GetRelation("public", "lookup") == nil { - t.Fatal("table lookup not found") - } - }, + wantErr: "cannot use subquery in check constraint", }, { name: "DEFAULT with type cast to user type creates dependency", diff --git a/pg/catalog/tablecmds.go b/pg/catalog/tablecmds.go index 0f050537..ed9de51b 100644 --- a/pg/catalog/tablecmds.go +++ b/pg/catalog/tablecmds.go @@ -377,10 +377,6 @@ func (c *Catalog) DefineRelation(stmt *nodes.CreateStmt, relkind byte) error { } } - if len(colDefs) == 0 && relkind != 'c' { - return errInvalidParameterValue("tables must have at least one column") - } - // Expand SERIAL columns (not applicable to composite types). type serialInfo struct { colIdx int @@ -606,6 +602,10 @@ func (c *Catalog) DefineRelation(stmt *nodes.CreateStmt, relkind byte) error { continue } if pe.Name == "" { + if pe.Expr == nil { + return &Error{Code: CodeSyntaxError, + Message: "syntax error at or near \")\""} + } // Expression partition key — store attnum=0 (PG convention). keyAttNums = append(keyAttNums, 0) continue @@ -759,17 +759,26 @@ func (c *Catalog) DefineRelation(stmt *nodes.CreateStmt, relkind byte) error { analyzed = coerced } columns[i].DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(columns[i].AttNum), analyzed, rel.OID, + DepNormal, DepNormal) rte := c.buildRelationRTE(rel) columns[i].Default = c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) } } if cd.RawGenExpr != nil && columns[i].Generated == 's' { + if rawExprContainsSubLink(cd.RawGenExpr) { + c.removeRelation(schema, relName, rel) + return &Error{Code: CodeFeatureNotSupported, + Message: "cannot use subquery in column generation expression"} + } if analyzed, err := c.AnalyzeStandaloneExpr(cd.RawGenExpr, rel); err == nil && analyzed != nil { coerced, cerr := c.coerceToTargetType(analyzed, analyzed.exprType(), columns[i].TypeOID, 'i') if cerr == nil && coerced != nil { analyzed = coerced } columns[i].DefaultAnalyzed = analyzed + c.recordDependencyOnSingleRelExprForObject('r', rel.OID, int32(columns[i].AttNum), analyzed, rel.OID, + DepNormal, DepNormal) rte := c.buildRelationRTE(rel) genExpr := c.DeparseExpr(analyzed, []*RangeTableEntry{rte}, false) columns[i].GenerationExpr = genExpr @@ -1770,3 +1779,18 @@ func (c *Catalog) cloneLikeComments(src, dst *Relation) { } } } + +func rawExprContainsSubLink(expr nodes.Node) bool { + found := false + nodes.Inspect(expr, func(n nodes.Node) bool { + if found || n == nil { + return false + } + if _, ok := n.(*nodes.SubLink); ok { + found = true + return false + } + return true + }) + return found +} diff --git a/pg/catalog/view.go b/pg/catalog/view.go index 77e65245..868d1844 100644 --- a/pg/catalog/view.go +++ b/pg/catalog/view.go @@ -92,7 +92,7 @@ func (c *Catalog) DefineView(stmt *nodes.ViewStmt) error { // Apply explicit column names if provided. if len(columnNames) > 0 { - if len(columnNames) != len(cols) { + if len(columnNames) > len(cols) { return &Error{ Code: CodeDatatypeMismatch, Message: fmt.Sprintf("CREATE VIEW specifies %d column names, but query produces %d columns", len(columnNames), len(cols)), @@ -415,4 +415,122 @@ func (c *Catalog) recordViewDepsQ(viewOID uint32, q *Query) { for _, cte := range q.CTEList { c.recordViewDepsQ(viewOID, cte.Query) } + + for _, te := range q.TargetList { + if te.ResJunk { + continue + } + c.recordDependencyOnExpr('r', viewOID, te.Expr, DepNormal) + c.recordViewVarDepsQ(viewOID, q, te.Expr) + } + if q.JoinTree != nil && q.JoinTree.Quals != nil { + c.recordDependencyOnExpr('r', viewOID, q.JoinTree.Quals, DepNormal) + c.recordViewVarDepsQ(viewOID, q, q.JoinTree.Quals) + } +} + +func (c *Catalog) recordViewVarDepsQ(viewOID uint32, q *Query, expr AnalyzedExpr) { + if q == nil || expr == nil { + return + } + switch v := expr.(type) { + case *VarExpr: + c.recordViewVarDep(viewOID, q, v) + case *FuncCallExpr: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *AggExpr: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *OpExpr: + c.recordViewVarDepsQ(viewOID, q, v.Left) + c.recordViewVarDepsQ(viewOID, q, v.Right) + case *DistinctExprQ: + c.recordViewVarDepsQ(viewOID, q, v.Left) + c.recordViewVarDepsQ(viewOID, q, v.Right) + case *ScalarArrayOpExpr: + c.recordViewVarDepsQ(viewOID, q, v.Left) + c.recordViewVarDepsQ(viewOID, q, v.Right) + case *NullIfExprQ: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *CoalesceExprQ: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *MinMaxExprQ: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *BoolExprQ: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *NullTestExpr: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + case *BooleanTestExpr: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + case *CaseExprQ: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + for _, w := range v.When { + c.recordViewVarDepsQ(viewOID, q, w.Condition) + c.recordViewVarDepsQ(viewOID, q, w.Result) + } + c.recordViewVarDepsQ(viewOID, q, v.Default) + case *ArrayExprQ: + for _, elem := range v.Elements { + c.recordViewVarDepsQ(viewOID, q, elem) + } + case *RowExprQ: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + case *RelabelExpr: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + case *CoerceViaIOExpr: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + case *CoerceToDomainExpr: + c.recordViewVarDepsQ(viewOID, q, v.Arg) + case *SubscriptingRefExpr: + c.recordViewVarDepsQ(viewOID, q, v.ContainerExpr) + for _, idx := range v.SubscriptExprs { + c.recordViewVarDepsQ(viewOID, q, idx) + } + for _, idx := range v.LowerExprs { + c.recordViewVarDepsQ(viewOID, q, idx) + } + case *SubLinkExpr: + c.recordViewDepsQ(viewOID, v.SubQuery) + case *WindowFuncExpr: + for _, arg := range v.Args { + c.recordViewVarDepsQ(viewOID, q, arg) + } + c.recordViewVarDepsQ(viewOID, q, v.AggFilter) + } +} + +func (c *Catalog) recordViewVarDep(viewOID uint32, q *Query, v *VarExpr) { + if v == nil || v.LevelsUp != 0 || v.AttNum <= 0 || v.RangeIdx < 0 || v.RangeIdx >= len(q.RangeTable) { + return + } + rte := q.RangeTable[v.RangeIdx] + colIdx := int(v.AttNum - 1) + switch rte.Kind { + case RTERelation: + c.recordDependency('r', viewOID, 0, 'r', rte.RelOID, int32(v.AttNum), DepNormal) + case RTESubquery: + if rte.Subquery != nil && colIdx < len(rte.Subquery.TargetList) { + c.recordViewVarDepsQ(viewOID, rte.Subquery, rte.Subquery.TargetList[colIdx].Expr) + } + case RTECTE: + if rte.CTEIndex >= 0 && rte.CTEIndex < len(q.CTEList) { + cte := q.CTEList[rte.CTEIndex] + if cte.Query != nil && colIdx < len(cte.Query.TargetList) { + c.recordViewVarDepsQ(viewOID, cte.Query, cte.Query.TargetList[colIdx].Expr) + } + } + } } diff --git a/pg/catalog/view_test.go b/pg/catalog/view_test.go index f1c0fd83..363cd7e3 100644 --- a/pg/catalog/view_test.go +++ b/pg/catalog/view_test.go @@ -373,7 +373,7 @@ func TestCreateViewColumnCountMismatch(t *testing.T) { {Val: &Expr{Kind: ExprColumnRef, ColumnName: "name"}}, }, From: []*FromItem{{Kind: FromTable, Table: "t"}}, - }, []string{"a"})) // only 1, but query produces 2 + }, []string{"a", "b", "c"})) // 3 names, but query produces 2 assertErrorCode(t, err, CodeDatatypeMismatch) } diff --git a/pg/parser/alter_misc.go b/pg/parser/alter_misc.go index 14c5fca0..9318136a 100644 --- a/pg/parser/alter_misc.go +++ b/pg/parser/alter_misc.go @@ -74,6 +74,9 @@ func (p *Parser) parseAlterFunctionStmt() (nodes.Node, error) { } // Otherwise it's alterfunc_opt_list (e.g., SET search_path ...) actions := p.parseAlterfuncOptList() + if actions == nil { + return nil, p.syntaxErrorAtCur() + } p.parseOptRestrict() return &nodes.AlterFunctionStmt{ Objtype: objtype, @@ -106,6 +109,9 @@ func (p *Parser) parseAlterFunctionStmt() (nodes.Node, error) { default: // alterfunc_opt_list opt_restrict (e.g., IMMUTABLE, STABLE, etc.) actions := p.parseAlterfuncOptList() + if actions == nil { + return nil, p.syntaxErrorAtCur() + } p.parseOptRestrict() return &nodes.AlterFunctionStmt{ Objtype: objtype, diff --git a/pg/parser/alter_table.go b/pg/parser/alter_table.go index d619fc76..34100b37 100644 --- a/pg/parser/alter_table.go +++ b/pg/parser/alter_table.go @@ -147,7 +147,13 @@ func (p *Parser) parseAlterIndex(alterLoc int) (nodes.Node, error) { if _, err := p.expect(PARTITION); err != nil { return nil, err } - partNames, _ := p.parseQualifiedName() + partNames, err := p.parseQualifiedName() + if err != nil { + return nil, err + } + if !p.collectMode() && partNames == nil { + return nil, p.syntaxErrorAtCur() + } partRv := makeRangeVarFromAnyName(partNames) cmd := &nodes.AlterTableCmd{ Subtype: int(nodes.AT_AttachPartition), @@ -746,7 +752,7 @@ func (p *Parser) parseAlterTableAdd() *nodes.AlterTableCmd { if p.isTableConstraintStart() { // ADD TableConstraint - constr := p.parseTableConstraint() + constr, _ := p.parseTableConstraint() return &nodes.AlterTableCmd{ Subtype: int(nodes.AT_AddConstraint), Def: constr, diff --git a/pg/parser/create_table.go b/pg/parser/create_table.go index 1259a64b..14159aa0 100644 --- a/pg/parser/create_table.go +++ b/pg/parser/create_table.go @@ -82,7 +82,11 @@ func (p *Parser) parseCreateStmt() (*nodes.CreateStmt, error) { stmt.InhRelations = p.parseOptInherit() // OptPartitionSpec - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec // OptAccessMethod (USING name) -- but only for table AM, not constraint/index stmt.AccessMethod = p.parseOptAccessMethod() @@ -146,7 +150,11 @@ func (p *Parser) parseCreateStmtPartitionOf(stmt *nodes.CreateStmt, relpersisten stmt.Partbound = p.parseForValues() // OptPartitionSpec - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec // OptAccessMethod stmt.AccessMethod = p.parseOptAccessMethod() @@ -245,7 +253,7 @@ func (p *Parser) parseTableElementList() (*nodes.List, error) { // TableElement: columnDef | TableConstraint | TableLikeClause func (p *Parser) parseTableElement() (nodes.Node, error) { if p.isTableConstraintStart() { - return p.parseTableConstraint(), nil + return p.parseTableConstraint() } if p.cur.Type == LIKE { return p.parseTableLikeClause() @@ -532,7 +540,10 @@ func (p *Parser) parseColConstraintElem() (nodes.Node, error) { } refRv := makeRangeVarFromNames(refNames) pkAttrs, _ := p.parseOptColumnList() - matchType := p.parseKeyMatch() + matchType, err := p.parseKeyMatch() + if err != nil { + return nil, err + } updAction, delAction, delSetCols := p.parseKeyActions() attrs := p.parseConstraintAttributeSpec() n := &nodes.Constraint{ @@ -653,15 +664,18 @@ func makeDefElem(name string, arg nodes.Node) *nodes.DefElem { // TableConstraint: // CONSTRAINT name ConstraintElem // | ConstraintElem -func (p *Parser) parseTableConstraint() *nodes.Constraint { +func (p *Parser) parseTableConstraint() (*nodes.Constraint, error) { if p.cur.Type == CONSTRAINT { p.advance() name, _ := p.parseName() - c := p.parseConstraintElem() + c, err := p.parseConstraintElem() + if err != nil { + return nil, err + } if c != nil { c.Conname = name } - return c + return c, nil } return p.parseConstraintElem() } @@ -676,7 +690,7 @@ func (p *Parser) parseTableConstraint() *nodes.Constraint { // | CHECK '(' a_expr ')' ConstraintAttributeSpec // | FOREIGN KEY '(' columnList ')' REFERENCES qualified_name opt_column_list key_match key_actions ConstraintAttributeSpec // | EXCLUDE ... -func (p *Parser) parseConstraintElem() *nodes.Constraint { +func (p *Parser) parseConstraintElem() (*nodes.Constraint, error) { switch p.cur.Type { case UNIQUE: p.advance() @@ -690,7 +704,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { p.advance() // INDEX if p.collectMode() { p.addRuleCandidate("qualified_name") - return nil + return nil, nil } idxName, _ := p.parseName() attrs := p.parseConstraintAttributeSpec() @@ -701,7 +715,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil } } @@ -723,7 +737,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil case PRIMARY: p.advance() // PRIMARY @@ -737,7 +751,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { p.advance() // INDEX if p.collectMode() { p.addRuleCandidate("qualified_name") - return nil + return nil, nil } idxName, _ := p.parseName() attrs := p.parseConstraintAttributeSpec() @@ -748,7 +762,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil } } @@ -769,7 +783,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil case CHECK: p.advance() @@ -784,7 +798,7 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil case FOREIGN: p.advance() // FOREIGN @@ -796,7 +810,10 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { refNames, _ := p.parseQualifiedName() refRv := makeRangeVarFromNames(refNames) pkAttrs, _ := p.parseOptColumnList() - matchType := p.parseKeyMatch() + matchType, err := p.parseKeyMatch() + if err != nil { + return nil, err + } updAction, delAction, delSetCols := p.parseKeyActions() attrs := p.parseConstraintAttributeSpec() n := &nodes.Constraint{ @@ -812,12 +829,12 @@ func (p *Parser) parseConstraintElem() *nodes.Constraint { InitiallyValid: true, } applyConstraintAttrs(n, attrs) - return n + return n, nil case EXCLUDE: - return p.parseExclusionConstraint() + return p.parseExclusionConstraint(), nil } - return nil + return nil, nil } // parseExclusionConstraint parses EXCLUDE constraint. @@ -1049,7 +1066,8 @@ func (p *Parser) parseTypedTableElementList() *nodes.List { // TypedTableElement: columnOptions | TableConstraint func (p *Parser) parseTypedTableElement() nodes.Node { if p.isTableConstraintStart() { - return p.parseTableConstraint() + c, _ := p.parseTableConstraint() + return c } return p.parseColumnOptions() } @@ -1163,24 +1181,28 @@ func (p *Parser) parseColumnList() *nodes.List { // parseKeyMatch parses key_match. // // key_match: MATCH FULL | MATCH PARTIAL | MATCH SIMPLE | /* EMPTY */ -func (p *Parser) parseKeyMatch() byte { +func (p *Parser) parseKeyMatch() (byte, error) { if p.cur.Type != MATCH { - return 's' // default SIMPLE + return 's', nil // default SIMPLE } p.advance() switch p.cur.Type { case FULL: p.advance() - return 'f' + return 'f', nil case PARTIAL: p.advance() - return 'p' + return 'p', nil + case SIMPLE: + p.advance() + return 's', nil default: // SIMPLE if p.cur.Type == IDENT && strings.EqualFold(p.cur.Str, "simple") { p.advance() + return 's', nil } - return 's' + return 0, p.syntaxErrorAtCur() } } @@ -1420,28 +1442,40 @@ func (p *Parser) parseOptInherit() *nodes.List { // parseOptPartitionSpec parses OptPartitionSpec. // // OptPartitionSpec: PartitionSpec | /* EMPTY */ -func (p *Parser) parseOptPartitionSpec() *nodes.PartitionSpec { +func (p *Parser) parseOptPartitionSpec() (*nodes.PartitionSpec, error) { if p.cur.Type == PARTITION { return p.parsePartitionSpec() } - return nil + return nil, nil } // parsePartitionSpec parses PartitionSpec. // // PartitionSpec: PARTITION BY ColId '(' part_params ')' -func (p *Parser) parsePartitionSpec() *nodes.PartitionSpec { +func (p *Parser) parsePartitionSpec() (*nodes.PartitionSpec, error) { p.advance() // PARTITION - p.expect(BY) - strategy, _ := p.parseColId() - p.expect('(') - params := p.parsePartParams() - p.expect(')') + if _, err := p.expect(BY); err != nil { + return nil, err + } + strategy, err := p.parseColId() + if err != nil { + return nil, err + } + if _, err := p.expect('('); err != nil { + return nil, err + } + params, err := p.parsePartParams() + if err != nil { + return nil, err + } + if _, err := p.expect(')'); err != nil { + return nil, err + } return &nodes.PartitionSpec{ Strategy: parsePartitionStrategy(strategy), PartParams: params, Loc: nodes.NoLoc(), - } + }, nil } // parsePartitionStrategy converts partition strategy name to internal code. @@ -1461,15 +1495,21 @@ func parsePartitionStrategy(strategy string) string { // parsePartParams parses part_params. // // part_params: part_elem | part_params ',' part_elem -func (p *Parser) parsePartParams() *nodes.List { - elem := p.parsePartElem() +func (p *Parser) parsePartParams() (*nodes.List, error) { + elem, err := p.parsePartElem() + if err != nil { + return nil, err + } items := []nodes.Node{elem} for p.cur.Type == ',' { p.advance() - elem = p.parsePartElem() + elem, err = p.parsePartElem() + if err != nil { + return nil, err + } items = append(items, elem) } - return &nodes.List{Items: items} + return &nodes.List{Items: items}, nil } // parsePartElem parses a single partition key element. @@ -1478,11 +1518,19 @@ func (p *Parser) parsePartParams() *nodes.List { // ColId opt_collate opt_qualified_name // | func_expr_windowless opt_collate opt_qualified_name // | '(' a_expr ')' opt_collate opt_qualified_name -func (p *Parser) parsePartElem() *nodes.PartitionElem { +func (p *Parser) parsePartElem() (*nodes.PartitionElem, error) { if p.cur.Type == '(' { p.advance() - expr, _ := p.parseAExpr(0) - p.expect(')') + expr, err := p.parseAExpr(0) + if err != nil { + return nil, err + } + if expr == nil { + return nil, p.syntaxErrorAtCur() + } + if _, err := p.expect(')'); err != nil { + return nil, err + } collation := p.parseOptCollate() opclass := p.parseOptQualifiedName() return &nodes.PartitionElem{ @@ -1490,14 +1538,17 @@ func (p *Parser) parsePartElem() *nodes.PartitionElem { Collation: collation, Opclass: opclass, Loc: nodes.NoLoc(), - } + }, nil } // Try as ColId first (checking if next is not '(' which would make it a function) if p.isColId() { next := p.peekNext() if next.Type != '(' { - name, _ := p.parseColId() + name, err := p.parseColId() + if err != nil { + return nil, err + } collation := p.parseOptCollate() opclass := p.parseOptQualifiedName() return &nodes.PartitionElem{ @@ -1505,12 +1556,18 @@ func (p *Parser) parsePartElem() *nodes.PartitionElem { Collation: collation, Opclass: opclass, Loc: nodes.NoLoc(), - } + }, nil } } // func_expr_windowless - expr, _ := p.parseFuncExprWindowless() + expr, err := p.parseFuncExprWindowless() + if err != nil { + return nil, err + } + if expr == nil { + return nil, p.syntaxErrorAtCur() + } collation := p.parseOptCollate() opclass := p.parseOptQualifiedName() return &nodes.PartitionElem{ @@ -1518,7 +1575,7 @@ func (p *Parser) parsePartElem() *nodes.PartitionElem { Collation: collation, Opclass: opclass, Loc: nodes.NoLoc(), - } + }, nil } // parseOptAccessMethod parses OptAccessMethod. diff --git a/pg/parser/grant.go b/pg/parser/grant.go index 35010dae..9996a710 100644 --- a/pg/parser/grant.go +++ b/pg/parser/grant.go @@ -159,7 +159,7 @@ func (p *Parser) finishRevokeOnObject(loc int, grantOptionFor bool, privs *nodes IsGrant: false, Targtype: targtype, Objtype: objtype, Objects: objects, Privileges: privs, Grantees: grantees, GrantOption: grantOptionFor, Behavior: nodes.DropBehavior(behavior), - Loc: nodes.Loc{Start: loc, End: p.prev.End}, + Loc: nodes.Loc{Start: loc, End: p.prev.End}, }, nil } @@ -361,7 +361,7 @@ func (p *Parser) parseGrantFunctionWithArgtypesList() (*nodes.List, error) { func (p *Parser) parseGrantFunctionWithArgtypes() (*nodes.ObjectWithArgs, error) { funcName, err := p.parseFuncName() if err != nil { - return nil, nil + return nil, err } owa := &nodes.ObjectWithArgs{Objname: funcName} if p.cur.Type == '(' { @@ -374,7 +374,9 @@ func (p *Parser) parseGrantFunctionWithArgtypes() (*nodes.ObjectWithArgs, error) if err != nil { return nil, err } - p.expect(')') + if _, err := p.expect(')'); err != nil { + return nil, err + } owa.Objargs = args } } else { @@ -407,11 +409,11 @@ func (p *Parser) parseGrantFuncArgsList() (*nodes.List, error) { } func (p *Parser) parseGrantFuncArg() (nodes.Node, error) { - switch p.cur.Type { - case IN_P, OUT_P, INOUT, VARIADIC: - p.advance() + arg := p.parseFuncArg() + if arg == nil { + return nil, nil } - return p.parseTypename() + return arg.ArgType, nil } func (p *Parser) parsePrivileges() (*nodes.List, error) { diff --git a/pg/parser/loader_compat_parser_test.go b/pg/parser/loader_compat_parser_test.go new file mode 100644 index 00000000..cfd158ee --- /dev/null +++ b/pg/parser/loader_compat_parser_test.go @@ -0,0 +1,47 @@ +package parser + +import "testing" + +func TestLoaderCompatParserRejectsInvalidTails(t *testing.T) { + tests := []string{ + `DROP FUNCTION ();`, + `ALTER FUNCTION f();`, + `COMMENT ON FUNCTION f(integer IS 'comment';`, + `GRANT EXECUTE ON FUNCTION f(integer TO role_name;`, + `CREATE TABLE t (x integer, FOREIGN KEY (x) REFERENCES p MATCH);`, + `ALTER INDEX i ATTACH PARTITION;`, + `WITH c AS SELECT 1 SELECT * FROM c;`, + `SELECT * FROM t, LATERAL ();`, + `SELECT * FROM t, LATERAL relation_name;`, + } + + for _, sql := range tests { + t.Run(sql, func(t *testing.T) { + if _, err := Parse(sql); err == nil { + t.Fatalf("Parse(%q): expected error, got nil", sql) + } + }) + } +} + +func TestLoaderCompatParserAcceptsKeywordIdentifiers(t *testing.T) { + tests := []string{ + `CREATE TABLE "select" (id integer);`, + `CREATE TABLE t ("select" integer);`, + `CREATE FUNCTION "select"(a integer) RETURNS integer LANGUAGE sql AS 'SELECT $1';`, + `CREATE TYPE "select" AS ENUM ('a');`, + `CREATE SCHEMA "select";`, + `COMMENT ON TABLE "select" IS 'comment';`, + `GRANT SELECT ON "select" TO PUBLIC;`, + `DROP TABLE "select";`, + `ALTER TABLE "select" RENAME TO select_new;`, + } + + for _, sql := range tests { + t.Run(sql, func(t *testing.T) { + if _, err := Parse(sql); err != nil { + t.Fatalf("Parse(%q): %v", sql, err) + } + }) + } +} diff --git a/pg/parser/parser.go b/pg/parser/parser.go index 58826d7d..ed6019b2 100644 --- a/pg/parser/parser.go +++ b/pg/parser/parser.go @@ -854,7 +854,11 @@ func (p *Parser) parseCreateTableOrCTASAfterParen(names *nodes.List, relpersiste if p.cur.Type == ')' { p.advance() stmt.InhRelations = p.parseOptInherit() - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec stmt.AccessMethod = p.parseOptAccessMethod() stmt.Options = p.parseOptWith() stmt.OnCommit = p.parseOnCommitOption() @@ -876,7 +880,11 @@ func (p *Parser) parseCreateTableOrCTASAfterParen(names *nodes.List, relpersiste stmt.TableElts = tableElts p.expect(')') stmt.InhRelations = p.parseOptInherit() - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec stmt.AccessMethod = p.parseOptAccessMethod() stmt.Options = p.parseOptWith() stmt.OnCommit = p.parseOnCommitOption() @@ -931,7 +939,11 @@ func (p *Parser) parseCreateTableOrCTASAfterParen(names *nodes.List, relpersiste stmt.TableElts = tableElts p.expect(')') stmt.InhRelations = p.parseOptInherit() - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec stmt.AccessMethod = p.parseOptAccessMethod() stmt.Options = p.parseOptWith() stmt.OnCommit = p.parseOnCommitOption() @@ -947,7 +959,11 @@ func (p *Parser) parseCreateTableOrCTASAfterParen(names *nodes.List, relpersiste stmt.TableElts = tableElts p.expect(')') stmt.InhRelations = p.parseOptInherit() - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec stmt.AccessMethod = p.parseOptAccessMethod() stmt.Options = p.parseOptWith() stmt.OnCommit = p.parseOnCommitOption() @@ -1050,7 +1066,11 @@ func (p *Parser) finishCreateStmt(names *nodes.List, relpersistence byte, ifNotE stmt.TableElts = tableElts p.expect(')') stmt.InhRelations = p.parseOptInherit() - stmt.Partspec = p.parseOptPartitionSpec() + partspec, err := p.parseOptPartitionSpec() + if err != nil { + return nil, err + } + stmt.Partspec = partspec stmt.AccessMethod = p.parseOptAccessMethod() stmt.Options = p.parseOptWith() stmt.OnCommit = p.parseOnCommitOption() diff --git a/pg/parser/schema.go b/pg/parser/schema.go index c012389e..05f9eecd 100644 --- a/pg/parser/schema.go +++ b/pg/parser/schema.go @@ -573,7 +573,11 @@ func (p *Parser) parseCommentStmt() (nodes.Node, error) { case FUNCTION: p.advance() stmt.Objtype = nodes.OBJECT_FUNCTION - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } @@ -583,7 +587,11 @@ func (p *Parser) parseCommentStmt() (nodes.Node, error) { case PROCEDURE: p.advance() stmt.Objtype = nodes.OBJECT_PROCEDURE - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } @@ -593,7 +601,11 @@ func (p *Parser) parseCommentStmt() (nodes.Node, error) { case ROUTINE: p.advance() stmt.Objtype = nodes.OBJECT_ROUTINE - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } @@ -790,8 +802,11 @@ func appendNodeToList(list *nodes.List, n nodes.Node) *nodes.List { } // parseFunctionWithArgtypesForComment parses function_with_argtypes for COMMENT ON. -func (p *Parser) parseFunctionWithArgtypesForComment() nodes.Node { - funcName, _ := p.parseFuncName() +func (p *Parser) parseFunctionWithArgtypesForComment() (nodes.Node, error) { + funcName, err := p.parseFuncName() + if err != nil { + return nil, err + } owa := &nodes.ObjectWithArgs{Objname: funcName} if p.cur.Type == '(' { p.advance() @@ -799,21 +814,23 @@ func (p *Parser) parseFunctionWithArgtypesForComment() nodes.Node { p.advance() } else { owa.Objargs = p.parseCommentFuncArgsList() - p.expect(')') + if _, err := p.expect(')'); err != nil { + return nil, err + } } } else { owa.ArgsUnspecified = true } - return owa + return owa, nil } // parseCommentFuncArgsList parses a comma-separated list of function argument types. func (p *Parser) parseCommentFuncArgsList() *nodes.List { var items []nodes.Node for { - typeName, _ := p.parseTypename() - if typeName != nil { - items = append(items, typeName) + arg := p.parseFuncArg() + if arg != nil && arg.ArgType != nil { + items = append(items, arg.ArgType) } if p.cur.Type != ',' { break @@ -956,7 +973,11 @@ func (p *Parser) parseSecLabelStmt() (nodes.Node, error) { case FUNCTION: p.advance() stmt.Objtype = nodes.OBJECT_FUNCTION - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } @@ -966,7 +987,11 @@ func (p *Parser) parseSecLabelStmt() (nodes.Node, error) { case PROCEDURE: p.advance() stmt.Objtype = nodes.OBJECT_PROCEDURE - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } @@ -976,7 +1001,11 @@ func (p *Parser) parseSecLabelStmt() (nodes.Node, error) { case ROUTINE: p.advance() stmt.Objtype = nodes.OBJECT_ROUTINE - stmt.Object = p.parseFunctionWithArgtypesForComment() + obj, err := p.parseFunctionWithArgtypesForComment() + if err != nil { + return nil, err + } + stmt.Object = obj if _, err := p.expect(IS); err != nil { return nil, err } diff --git a/pg/pgregress/known_failures.json b/pg/pgregress/known_failures.json index 720aedb8..16a4644f 100644 --- a/pg/pgregress/known_failures.json +++ b/pg/pgregress/known_failures.json @@ -102,8 +102,7 @@ 199 ], "foreign_key.sql": [ - 227, - 586 + 227 ], "json.sql": [ 65, @@ -240,4 +239,4 @@ 299, 300 ] -} \ No newline at end of file +}