Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions __fixtures__/generated/generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -21580,6 +21580,27 @@
"misc/literal-escaping-30.sql": "SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x' AS x, DEFAULT 'http://d'), '/x:r' PASSING (SELECT doc FROM t) COLUMNS a int) AS xt",
"misc/literal-escaping-31.sql": "SELECT * FROM t, LATERAL XMLTABLE('/r' PASSING t.doc COLUMNS a int PATH 'a') xt",
"misc/literal-escaping-32.sql": "SELECT 1_000.000_1, 1_000.5e1_0, 0x_FFFF_FFFF_FFFF_FFFF",
"misc/literal-edge-cases-1.sql": "SELECT X''",
"misc/literal-edge-cases-2.sql": "SELECT B''",
"misc/literal-edge-cases-3.sql": "SELECT X'', B'', X'1F', B'101'",
"misc/literal-edge-cases-4.sql": "DO $x$ BEGIN RAISE NOTICE '$_PGFN_$'; END $x$",
"misc/literal-edge-cases-5.sql": "DO $x$ BEGIN RAISE NOTICE '$$'; END $x$",
"misc/literal-edge-cases-6.sql": "DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$'; END $x$",
"misc/literal-edge-cases-7.sql": "DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$ $EOFCODE1$'; END $x$",
"misc/literal-edge-cases-8.sql": "DO $x$BEGIN NULL; END; --$$x$",
"misc/literal-edge-cases-9.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$_PGFN_$') $x$",
"misc/literal-edge-cases-10.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$$ $EOFCODE$') $x$",
"misc/literal-edge-cases-11.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$SELECT 1 --$$x$",
"misc/literal-edge-cases-12.sql": "CREATE VIEW v WITH (security_barrier = 'Foo') AS SELECT 1",
"misc/literal-edge-cases-13.sql": "CREATE VIEW v WITH (security_barrier = 'true') AS SELECT 1",
"misc/literal-edge-cases-14.sql": "CREATE INDEX i ON t (a) WITH (fillfactor = 'int')",
"misc/literal-edge-cases-15.sql": "CREATE INDEX i ON t (a) WITH (opt = 'Foo')",
"misc/literal-edge-cases-16.sql": "CREATE TABLE t (a int) WITH (opt = 'Foo')",
"misc/literal-edge-cases-17.sql": "CREATE TABLE t (a int) WITH (opt = 'int')",
"misc/literal-edge-cases-18.sql": "CREATE TABLE t (a int) WITH (opt = 'select')",
"misc/literal-edge-cases-19.sql": "ALTER TABLE t SET (opt = 'Foo')",
"misc/literal-edge-cases-20.sql": "ALTER TABLE t SET (opt = 'int')",
"misc/literal-edge-cases-21.sql": "ALTER INDEX i SET (opt = 'Foo')",
"misc/launchql-ext-types-1.sql": "CREATE DOMAIN attachment AS jsonb CHECK ( value ?& ARRAY['url', 'mime'] AND (value->>'url') ~ '^(https?)://[^\\s/$.?#].[^\\s]*$' )",
"misc/launchql-ext-types-2.sql": "COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment'",
"misc/launchql-ext-types-3.sql": "CREATE DOMAIN email AS citext CHECK ( value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' )",
Expand Down
29 changes: 29 additions & 0 deletions __fixtures__/kitchen-sink/misc/literal-edge-cases.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Edge-case literals the deparser must keep valid
-- Ref: constructive-io/constructive-planning#2175

-- Empty bit-string literals
SELECT X'';
SELECT B'';
SELECT X'', B'', X'1F', B'101';

-- Dollar-quoted bodies that contain the default or fallback delimiter
DO $x$ BEGIN RAISE NOTICE '$_PGFN_$'; END $x$;
DO $x$ BEGIN RAISE NOTICE '$$'; END $x$;
DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$'; END $x$;
DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$ $EOFCODE1$'; END $x$;
DO $x$BEGIN NULL; END; --$$x$;
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$_PGFN_$') $x$;
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$$ $EOFCODE$') $x$;
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$SELECT 1 --$$x$;

-- String option values must keep their case and stay strings
CREATE VIEW v WITH (security_barrier = 'Foo') AS SELECT 1;
CREATE VIEW v WITH (security_barrier = 'true') AS SELECT 1;
CREATE INDEX i ON t (a) WITH (fillfactor = 'int');
CREATE INDEX i ON t (a) WITH (opt = 'Foo');
CREATE TABLE t (a int) WITH (opt = 'Foo');
CREATE TABLE t (a int) WITH (opt = 'int');
CREATE TABLE t (a int) WITH (opt = 'select');
ALTER TABLE t SET (opt = 'Foo');
ALTER TABLE t SET (opt = 'int');
ALTER INDEX i SET (opt = 'Foo');
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

import { FixtureTestUtils } from '../../test-utils';
const fixtures = new FixtureTestUtils();

it('misc-literal-edge-cases', async () => {
await fixtures.runFixtureTests([
"misc/literal-edge-cases-1.sql",
"misc/literal-edge-cases-2.sql",
"misc/literal-edge-cases-3.sql",
"misc/literal-edge-cases-4.sql",
"misc/literal-edge-cases-5.sql",
"misc/literal-edge-cases-6.sql",
"misc/literal-edge-cases-7.sql",
"misc/literal-edge-cases-8.sql",
"misc/literal-edge-cases-9.sql",
"misc/literal-edge-cases-10.sql",
"misc/literal-edge-cases-11.sql",
"misc/literal-edge-cases-12.sql",
"misc/literal-edge-cases-13.sql",
"misc/literal-edge-cases-14.sql",
"misc/literal-edge-cases-15.sql",
"misc/literal-edge-cases-16.sql",
"misc/literal-edge-cases-17.sql",
"misc/literal-edge-cases-18.sql",
"misc/literal-edge-cases-19.sql",
"misc/literal-edge-cases-20.sql",
"misc/literal-edge-cases-21.sql"
]);
});
29 changes: 23 additions & 6 deletions packages/deparser/src/deparser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QuoteUtils } from '@pgsql/quotes';
import { keywordKindOf, QuoteUtils } from '@pgsql/quotes';
import { Node } from '@pgsql/types';
import * as t from '@pgsql/types';

Expand Down Expand Up @@ -149,7 +149,7 @@ function isWrappedParseResult(obj: any): obj is { ParseResult: t.ParseResult } {
*/
function formatBitString(bsval: string): string {
const quote = (value: string) => value.replace(/'/g, "''");
if (/^x[0-9A-Fa-f]+$/.test(bsval)) {
if (/^x[0-9A-Fa-f]*$/.test(bsval)) {
return `x'${bsval.substring(1)}'`;
}
if (bsval.startsWith('b')) {
Expand All @@ -158,6 +158,12 @@ function formatBitString(bsval: string): string {
return `b'${quote(bsval)}'`;
}

// A dollar-quoted body ends at the first occurrence of its tag, including one
// formed by the body's trailing characters plus the closing tag (`...$` + `$$`).
function dollarTagConflicts(body: string, tag: string): boolean {
return (body + tag).indexOf(tag) < body.length;
}

const NUMERIC_LITERAL = /^[+-]?(?:0[xX](?:_?[0-9A-Fa-f])+|0[oO](?:_?[0-7])+|0[bB](?:_?[01])+|(?:\d(?:_?\d)*(?:\.(?:\d(?:_?\d)*)?)?|\.\d(?:_?\d)*)(?:[eE][+-]?\d(?:_?\d)*)?)$/;

function formatInteger(ival: unknown): string {
Expand Down Expand Up @@ -239,10 +245,19 @@ export class Deparser implements DeparserVisitor {
*/
private getFunctionDelimiter(body: string): string {
const delimiter = this.options.functionDelimiter || '$$';
if (body.includes(delimiter)) {
return this.options.functionDelimiterFallback || '$EOFCODE$';
if (!dollarTagConflicts(body, delimiter)) {
return delimiter;
}
const fallback = this.options.functionDelimiterFallback || '$EOFCODE$';
if (!dollarTagConflicts(body, fallback)) {
return fallback;
}
const base = fallback.slice(0, -1);
let counter = 1;
while (dollarTagConflicts(body, `${base}${counter}$`)) {
counter++;
}
return delimiter;
return `${base}${counter}$`;
}

/**
Expand Down Expand Up @@ -10739,7 +10754,9 @@ export class Deparser implements DeparserVisitor {
private formatOptionValue(arg: any, argValue: any): any {
if (arg && this.getNodeType(arg) === 'String') {
const sval: string = this.getNodeData(arg).sval || '';
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(sval) ? sval : QuoteUtils.escape(sval);
return /^[a-z_][a-z0-9_]*$/.test(sval) && keywordKindOf(sval) === 'NO_KEYWORD'
? sval
: QuoteUtils.escape(sval);
}
return argValue;
}
Expand Down
Loading