Skip to content

feat: parse PostgreSQL CREATE CAST, CREATE TRANSFORM, CREATE CONVERSION and ALTER CONVERSION - #2918

Merged
manticore-projects merged 2 commits into
JSQLParser:masterfrom
minleejae:feat/postgresql-cast-transform-conversion-20261113
Nov 14, 2026
Merged

manticore-projects merged 2 commits into
JSQLParser:masterfrom
minleejae:feat/postgresql-cast-transform-conversion-20261113

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Follows #2896, which typed DROP CAST and DROP TRANSFORM. This adds the statements that create those objects, and the conversion statements, on the same signature models.

Purpose

PostgreSQL defines casts, transforms and encoding conversions with their own statements:

CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT;
CREATE CAST (int AS text) WITH INOUT AS IMPLICIT;
CREATE CAST (a.t AS b.u) WITHOUT FUNCTION;
CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plpython3u (
    FROM SQL WITH FUNCTION hstore_to_plpython(internal),
    TO SQL WITH FUNCTION plpython_to_hstore(internal));
CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM latin1_to_utf8;
ALTER CONVERSION myconv OWNER TO CURRENT_USER;

On master each of them comes back as an UnsupportedStatement: the text is kept, but the types, functions, encodings and names cannot be inspected, validators report only allowUnsupportedStatements, and with the PostgreSQL dialect any text after CREATE CAST is accepted. pg_dump writes CREATE CAST and CREATE TRANSFORM for every user-defined cast and transform, and the matching DROP CAST, DROP TRANSFORM and COMMENT ON CAST are already typed.

Syntax reference: PostgreSQL 18 CREATE CAST, CREATE TRANSFORM, CREATE CONVERSION, ALTER CONVERSION, and the CreateCastStmt, CreateTransformStmt, CreateConversionStmt, RenameStmt, AlterOwnerStmt and AlterObjectSchemaStmt productions of gram.y.

Current design (As-is)

  • Create() has no branch for CAST, TRANSFORM or CONVERSION. A CREATE statement that names none of TABLE, INDEX, VIEW and POLICY falls back to UnsupportedStatement, in every dialect.
  • Alter() has no branch for CONVERSION, so ALTER CONVERSION … ends in the captureRest() fallback, also as an UnsupportedStatement.
  • Drop already names a cast by a CastSignature ((source AS target)) and a transform by a TransformSignature (FOR type LANGUAGE lang), read by the CastSignature() and TransformSignature() productions, and function signatures are RoutineReference objects read by RoutineReference() (DROP FUNCTION, ALTER FUNCTION, COMMENT ON FUNCTION).

Target design (To-be)

  • Four statements, each with appendTo() shared by toString() and StatementDeParser:
    • CreateCast (statement.create.cast): getCast() is the CastSignature of DROP CAST; getMethod() is WITH_FUNCTION, WITHOUT_FUNCTION or WITH_INOUT; getFunction() is the RoutineReference of WITH FUNCTION, with null arguments when the function is named without an argument list; getContext() is ASSIGNMENT, IMPLICIT or null. Method and function stay consistent: setting a function selects WITH_FUNCTION, another method clears the function.
    • CreateTransform (statement.create.transform): isOrReplace(), getTransform() is the TransformSignature of DROP TRANSFORM, getFromSqlFunction() and getToSqlFunction() are RoutineReference objects, and isToSqlFirst() keeps the order of the two items. The grammar follows transform_element_list: one item, or one FROM SQL and one TO SQL item in either order.
    • CreateConversion (statement.create.conversion): isDefaultConversion(), getName(), getSourceEncoding() and getDestinationEncoding() as StringValue, getFunction().
    • AlterConversion (statement.alter): getName(), getAction() (RENAME, OWNER, SET_SCHEMA) and getNewName(), getOwner() or getSchemaName(), like AlterCollation.
  • The grammar reuses CastSignature(), TransformSignature() and RoutineReference(); no production is duplicated and no keyword is added. The words TRANSFORM, CONVERSION, INOUT, ASSIGNMENT, IMPLICIT, SQL and OWNER are matched contextually.
  • With Dialect.POSTGRESQL, CREATE CAST, CREATE [OR REPLACE] TRANSFORM, CREATE [DEFAULT] CONVERSION, CREATE DEFAULT and ALTER CONVERSION start only these statements, so the forms PostgreSQL reports as syntax errors are parse errors: CREATE OR REPLACE CAST, CREATE OR REPLACE CONVERSION, two FROM SQL items, an encoding that is not a string constant (N'…', B'…', an identifier), a reserved word as the conversion, schema or new name, or as the owner other than CURRENT_ROLE, CURRENT_USER and SESSION_USER.
  • In another dialect and without one, a statement is read this way when it has PostgreSQL's form: CAST (…) followed by WITH FUNCTION, WITH INOUT or WITHOUT FUNCTION, TRANSFORM FOR type LANGUAGE, [DEFAULT] CONVERSION name FOR, and ALTER CONVERSION name followed by RENAME, OWNER or SET. Statements of other products that start with the same words keep today's result, an UnsupportedStatement: CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f, Informix's CREATE CAST (percent AS decimal), Db2's CREATE TRANSFORM FOR type group (…), CREATE OR REPLACE CAST ….
  • Wiring: StatementVisitor default methods, StatementVisitorAdapter, TablesNamesFinder (no tables), StatementFeatureVisitor (schema change), the features createCast, createTransform, createConversion and alterConversion in FeaturesAllowed.CREATE / ALTER and PostgresqlVersion, StatementValidator, README, index.rst and usage.rst. The statements and their signatures carry AST nodes.

Behaviour changes:

  • These statements were UnsupportedStatement objects and are now CreateCast, CreateTransform, CreateConversion and AlterConversion. Code that matched them by instanceof UnsupportedStatement has to match the new classes. toString() now renders them in the usual spacing, CREATE CAST (int AS text) WITH INOUT instead of the token list CREATE CAST ( int AS text ) WITH INOUT.
  • With the PostgreSQL dialect, malformed text after these statement starts is a parse error instead of an UnsupportedStatement. Without that dialect, text that has PostgreSQL's form up to the point described above and then differs from it is a parse error too.

Not covered: a type written with SETOF (CREATE CAST (SETOF int AS text) …), which PostgreSQL's grammar takes wherever a type name is allowed. The shared CastSignature, TransformSignature and RoutineReference read a plain type, as DROP CAST and COMMENT ON CAST do on master.

Validation

  • PostgreSqlCastTransformConversionTest adds 148 test cases: 40 statements parsed with the PostgreSQL dialect, without a dialect and with another one, checked against toString() and the deparser, parsed again, validated for PostgreSQL and for FeaturesAllowed.DDL, with no tables, a schema-change feature set and an AST node spanning the statement; the fields of each statement and their edits; lower-case keywords; a script that mixes them with DROP CAST and DROP TRANSFORM; visitors; validation; 84 forms rejected with the PostgreSQL dialect; and 10 statements of other forms that stay UnsupportedStatement without it. ASTNodeCoverageTest walks four of the statements and StatementFeatureVisitorMetadataDdlTest covers the four classes. On master (cf057dd33) every accepted statement is an UnsupportedStatement, and 83 of the 84 rejected forms parse as one.

  • Java 17 Gradle check: 14,618 tests, 0 failures, 25 skipped. Grammar ambiguity, formatting (Spotless ratchet against upstream/master), Checkstyle, PMD, SpotBugs and JaCoCo pass.

  • Compared with master (cf057dd33) on the DDL/DCL survey corpora and the non-DDL corpora: no case is fixed, regressed or changed, since the corpora have none of these statements; the rendered statements and AST dumps are identical.

  • PostgreSQL 18.6: of 141 statements, 45 of the 47 whose syntax the server accepts are typed with Dialect.POSTGRESQL (they stop only at execution, for example with a missing function, type or language), and all 94 it rejects are parse errors; on master 93 of those 94 are an UnsupportedStatement. The two that differ are the SETOF forms listed under "Not covered" (CREATE CAST (setof int AS text) WITH INOUT, DROP CAST (setof int AS text)).

AI assistance: prepared with Claude Code.

minleejae and others added 2 commits November 13, 2026 10:56
…ON and ALTER CONVERSION

CREATE CAST (source AS target) WITH FUNCTION f(args) | WITHOUT FUNCTION |
WITH INOUT [AS ASSIGNMENT | AS IMPLICIT], CREATE [OR REPLACE] TRANSFORM
FOR type LANGUAGE lang (FROM SQL WITH FUNCTION f, TO SQL WITH FUNCTION g),
CREATE [DEFAULT] CONVERSION name FOR 'enc' TO 'enc' FROM function and
ALTER CONVERSION name RENAME TO | OWNER TO | SET SCHEMA were unsupported
statements.

They parse to CreateCast, CreateTransform, CreateConversion and
AlterConversion. A cast and a transform are named by the CastSignature
and TransformSignature of DROP CAST and DROP TRANSFORM, and functions
are RoutineReference objects, read by the existing productions. The
statements are wired into the deparser, visitors, table finder,
validation (createCast, createTransform, createConversion and
alterConversion, enabled for PostgreSQL) and feature analysis.

With the PostgreSQL dialect these words start only these statements, so
forms the server reports as syntax errors are parse errors. In another
dialect and without one, a statement is read this way when it has
PostgreSQL's form, and statements of other products that start with the
same words, such as CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f, stay
unsupported statements.

Signed-off-by: minleejae <mmj9808@gmail.com>
Preserve cast, transform, and conversion support alongside upstream
operator, aggregate, and spatial DDL. Cover both feature groups in the
existing statement-boundary regression test.

Validation: mvn --batch-mode --no-transfer-progress test
15,973 tests; zero failures/errors; 25 existing skips.
JavaCC reports no choice conflicts.
@manticore-projects
manticore-projects merged commit d2769be into JSQLParser:master Nov 14, 2026
1 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants