fix(pretty-print): preserve quoted policy names#749
Merged
Conversation
psteinroe
pushed a commit
that referenced
this pull request
Jun 3, 2026
…across all emitters (#755) Several emitters printed identifiers as bare tokens instead of using the quote-aware helper, so names that require quoting (reserved keywords, multi-word, mixed-case) lost their qutes and produced invalid or semantically different SQL. Similar to (#749), routed them through `emit_identifier_maybe_quoted` instead of using bare `TokenKind::IDENT` tokens, and added test cases covering one statement per fixed emitter. ## What kind of change does this PR introduce? Bug fix ## What is the current behavior? Many emitters wrote a dynamic identifier (a user-supplied name read from the AST) straight into a raw `TokenKind::IDENT` token, bypassing the quote aware helper, leading to the quotes always being dropped, which can produce invalid SQL in case of reserved keywords or multi-token identifiers, or change the semantics if the quoted identifier is case-sensitive (e.g. quoted schema names are case-sensitive). **1. `parse-fail`: reserved keyword emitted bare => invalid SQL** ```sql -- input ALTER TABLE t1 SET SCHEMA "primary"; -- formatter output (un-fixed): quotes dropped alter table t1 set schema primary; -- ^^^^^^^ "primary" is a reserved keyword => syntax error at or near "primary" ``` **2. `ast-mismatch`: mixed-case name folded => different meaning** ```sql -- input REINDEX SCHEMA "MySchema"; -- formatter output (un-fixed): quotes dropped, case folded reindex schema myschema; -- ^^^^^^^^ now refers to a different schema (parses fine, wrong semantics) ``` <details> <summary>Full <code>cargo test</code> output against un-fixed code</summary> ``` running 2 tests test test_single__index_elem_reserved_keyword_0 ... FAILED test test_multi__reserved_keyword_idents ... FAILED failures: ---- test_single__index_elem_reserved_keyword_0 stdout ---- Original content: CREATE UNIQUE INDEX index_user_emails_on_user_id_and_primary ON public.user_emails USING btree (user_id, "primary") WHERE "primary"; Parsed AST: IndexStmt( IndexStmt { idxname: "index_user_emails_on_user_id_and_primary", relation: Some( RangeVar { catalogname: "", schemaname: "public", relname: "user_emails", inh: true, relpersistence: "p", alias: None, location: 64, }, ), access_method: "btree", table_space: "", index_params: [ Node { node: Some( IndexElem( IndexElem { name: "user_id", expr: None, indexcolname: "", collation: [], opclass: [], opclassopts: [], ordering: SortbyDefault, nulls_ordering: SortbyNullsDefault, }, ), ), }, Node { node: Some( IndexElem( IndexElem { name: "primary", expr: None, indexcolname: "", collation: [], opclass: [], opclassopts: [], ordering: SortbyDefault, nulls_ordering: SortbyNullsDefault, }, ), ), }, ], index_including_params: [], options: [], where_clause: Some( Node { node: Some( ColumnRef( ColumnRef { fields: [ Node { node: Some( String( String { sval: "primary", }, ), ), }, ], location: 122, }, ), ), }, ), exclude_op_names: [], idxcomment: "", index_oid: 0, old_number: 0, old_create_subid: 0, old_first_relfilelocator_subid: 0, unique: true, nulls_not_distinct: false, primary: false, isconstraint: false, deferrable: false, initdeferred: false, transformed: false, concurrent: false, if_not_exists: false, reset_default_tblspc: false, }, ) Formatted content (width=80): create unique index "index_user_emails_on_user_id_and_primary" on public.user_emails using btree ( user_id, primary ) where "primary"; thread 'test_single__index_elem_reserved_keyword_0' (978946) panicked at crates/pgls_pretty_print/tests/tests.rs:82:56: Failed to parse SQL: Parse("syntax error at or near \"primary\"") ---- test_multi__reserved_keyword_idents stdout ---- Parsed AST: CreateStmt( CreateStmt { relation: Some( RangeVar { catalogname: "", schemaname: "", relname: "primary", inh: true, relpersistence: "p", alias: None, location: 13, }, ), table_elts: [ Node { node: Some( ColumnDef( ColumnDef { colname: "x", type_name: Some( TypeName { names: [ Node { node: Some( String( String { sval: "pg_catalog", }, ), ), }, Node { node: Some( String( String { sval: "int4", }, ), ), }, ], type_oid: 0, setof: false, pct_type: false, typmods: [], typemod: -1, array_bounds: [], location: 26, }, ), compression: "", inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: "", storage_name: "", raw_default: None, cooked_default: None, identity: "", identity_sequence: None, coll_clause: None, coll_oid: 0, constraints: [], fdwoptions: [], location: 24, }, ), ), }, ], inh_relations: [], partbound: None, partspec: None, of_typename: None, constraints: [], options: [], oncommit: OncommitNoop, tablespacename: "", access_method: "", if_not_exists: false, }, ) Parsed AST: CreateStmt( CreateStmt { relation: Some( RangeVar { catalogname: "", schemaname: "", relname: "t1", inh: true, relpersistence: "p", alias: None, location: 13, }, ), table_elts: [ Node { node: Some( ColumnDef( ColumnDef { colname: "primary", type_name: Some( TypeName { names: [ Node { node: Some( String( String { sval: "pg_catalog", }, ), ), }, Node { node: Some( String( String { sval: "int4", }, ), ), }, ], type_oid: 0, setof: false, pct_type: false, typmods: [], typemod: -1, array_bounds: [], location: 27, }, ), compression: "", inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: "", storage_name: "", raw_default: None, cooked_default: None, identity: "", identity_sequence: None, coll_clause: None, coll_oid: 0, constraints: [], fdwoptions: [], location: 17, }, ), ), }, ], inh_relations: [], partbound: None, partspec: None, of_typename: None, constraints: [], options: [], oncommit: OncommitNoop, tablespacename: "", access_method: "", if_not_exists: false, }, ) Parsed AST: CreateStmt( CreateStmt { relation: Some( RangeVar { catalogname: "", schemaname: "", relname: "t2", inh: true, relpersistence: "p", alias: None, location: 13, }, ), table_elts: [ Node { node: Some( ColumnDef( ColumnDef { colname: "x", type_name: Some( TypeName { names: [ Node { node: Some( String( String { sval: "pg_catalog", }, ), ), }, Node { node: Some( String( String { sval: "int4", }, ), ), }, ], type_oid: 0, setof: false, pct_type: false, typmods: [], typemod: -1, array_bounds: [], location: 19, }, ), compression: "", inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: "", storage_name: "", raw_default: None, cooked_default: None, identity: "", identity_sequence: None, coll_clause: None, coll_oid: 0, constraints: [ Node { node: Some( Constraint( Constraint { contype: ConstrCheck, conname: "primary", deferrable: false, initdeferred: false, skip_validation: false, initially_valid: true, is_no_inherit: false, raw_expr: Some( Node { node: Some( AExpr( AExpr { kind: AexprOp, name: [ Node { node: Some( String( String { sval: ">", }, ), ), }, ], lexpr: Some( Node { node: Some( ColumnRef( ColumnRef { fields: [ Node { node: Some( String( String { sval: "x", }, ), ), }, ], location: 51, }, ), ), }, ), rexpr: Some( Node { node: Some( AConst( AConst { isnull: false, location: 55, val: Some( Ival( Integer { ival: 0, }, ), ), }, ), ), }, ), location: 53, }, ), ), }, ), cooked_expr: "", inhcount: 0, nulls_not_distinct: false, keys: [], including: [], exclusions: [], options: [], indexname: "", indexspace: "", reset_default_tblspc: false, access_method: "", where_clause: None, pktable: None, fk_attrs: [], pk_attrs: [], fk_matchtype: "", fk_upd_action: "", fk_del_action: "", fk_del_set_cols: [], old_conpfeqop: [], old_pktable_oid: 0, location: 23, }, ), ), }, ], fdwoptions: [], location: 17, }, ), ), }, ], inh_relations: [], partbound: None, partspec: None, of_typename: None, constraints: [], options: [], oncommit: OncommitNoop, tablespacename: "", access_method: "", if_not_exists: false, }, ) Parsed AST: AlterObjectSchemaStmt( AlterObjectSchemaStmt { object_type: ObjectTable, relation: Some( RangeVar { catalogname: "", schemaname: "", relname: "t1", inh: true, relpersistence: "p", alias: None, location: 12, }, ), object: None, newschema: "primary", missing_ok: false, }, ) Failed to parse formatted SQL. Error: Parse("syntax error at or near \"primary\"") Statement index: 466 Formatted SQL: alter table t1 set schema primary; thread 'test_multi__reserved_keyword_idents' (978945) panicked at crates/pgls_pretty_print/tests/tests.rs:171:17: Failed to parse formatted SQL: Parse("syntax error at or near \"primary\"") failures: test_multi__reserved_keyword_idents test_single__index_elem_reserved_keyword_0 test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 512 filtered out; finished in 0.00s error: test failed, to rerun pass `-p pgls_pretty_print --test tests` ``` </details> > Note: `cargo test` panics on the first broken statement per file, so it > reports one representative failure. The `corpus_sweep` run below enumerates all 46. <details> <summary>Corpus sweep failures</summary> | fixture | category | widths | source | error | |---|---|---|---|---| | parse-fail/alter_database_set_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_event_trig_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_extension_contents_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_object_depends_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_op_family_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_publication_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_publication_stmt_0002 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_role_set_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_table_move_all_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_table_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_table_stmt_0002 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/alter_object_schema_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/close_portal_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/cluster_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_am_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_extension_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_fdw_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_foreign_table_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/index_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_op_class_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_op_family_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_publication_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/rule_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_transform_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_trig_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/index_stmt_0002 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/index_stmt_0003 | parse-fail | 80,100 | /tmp/demo-fixtures/index_elem_reserved_keyword_0.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/create_user_mapping_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/deallocate_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/dropdb_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/drop_table_space_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/drop_user_mapping_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/execute_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/fetch_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/import_foreign_schema_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/listen_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/prepare_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | ast-mismatch/reindex_stmt_0001 | ast-mismatch | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Formatter (beta): This statement type is not fully supported yet. Formatting may alter semantics. | | parse-fail/reindex_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/select_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/select_stmt_0002 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "Case" | | ast-mismatch/select_stmt_0001 | ast-mismatch | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Formatter (beta): This statement type is not fully supported yet. Formatting may alter semantics. | | parse-fail/select_stmt_0003 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/unlisten_stmt_0001 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | | parse-fail/select_stmt_0004 | parse-fail | 80,100 | /tmp/demo-fixtures/reserved_keyword_idents.sql | Failed to parse formatted output: Formatted SQL failed to parse: Invalid statement: syntax error at or near "primary" | </details> ## What is the new behavior? Routed emitters with dynamic identifiers through the `emit_identifier_maybe_quoted` helper. ## Additional context I have a small `corpus_sweep` helper, with which I compared the ASTs of the SQL files I downloaded against the produced SQL of the pretty-print. I wasn't sure whether to include it in the PR, but it allowed me to find the first instance of the bug, from which I looked for similar issues!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Preserve quoting for policy names that require quoted identifiers during pretty-printing. Multi-word policy names in
CREATE POLICYandALTER POLICYnow format to valid SQL instead of being emitted as bare identifier tokens.Policy identifiers
The policy statement emitters now use the shared quote-aware identifier helper, matching other policy operations such as drop and rename. Added snapshot coverage for quoted policy names at both configured pretty-printer line widths.
Fixes #748