Skip to content

Commit cf81016

Browse files
authored
Merge pull request #359 from constructive-io/fix/edge-case-literals
fix(deparser): keep empty bit strings, collision-free dollar tags and case-sensitive option strings valid
2 parents 253de85 + e5373c5 commit cf81016

4 files changed

Lines changed: 102 additions & 6 deletions

File tree

‎__fixtures__/generated/generated.json‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21580,6 +21580,27 @@
2158021580
"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",
2158121581
"misc/literal-escaping-31.sql": "SELECT * FROM t, LATERAL XMLTABLE('/r' PASSING t.doc COLUMNS a int PATH 'a') xt",
2158221582
"misc/literal-escaping-32.sql": "SELECT 1_000.000_1, 1_000.5e1_0, 0x_FFFF_FFFF_FFFF_FFFF",
21583+
"misc/literal-edge-cases-1.sql": "SELECT X''",
21584+
"misc/literal-edge-cases-2.sql": "SELECT B''",
21585+
"misc/literal-edge-cases-3.sql": "SELECT X'', B'', X'1F', B'101'",
21586+
"misc/literal-edge-cases-4.sql": "DO $x$ BEGIN RAISE NOTICE '$_PGFN_$'; END $x$",
21587+
"misc/literal-edge-cases-5.sql": "DO $x$ BEGIN RAISE NOTICE '$$'; END $x$",
21588+
"misc/literal-edge-cases-6.sql": "DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$'; END $x$",
21589+
"misc/literal-edge-cases-7.sql": "DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$ $EOFCODE1$'; END $x$",
21590+
"misc/literal-edge-cases-8.sql": "DO $x$BEGIN NULL; END; --$$x$",
21591+
"misc/literal-edge-cases-9.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$_PGFN_$') $x$",
21592+
"misc/literal-edge-cases-10.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$$ $EOFCODE$') $x$",
21593+
"misc/literal-edge-cases-11.sql": "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$SELECT 1 --$$x$",
21594+
"misc/literal-edge-cases-12.sql": "CREATE VIEW v WITH (security_barrier = 'Foo') AS SELECT 1",
21595+
"misc/literal-edge-cases-13.sql": "CREATE VIEW v WITH (security_barrier = 'true') AS SELECT 1",
21596+
"misc/literal-edge-cases-14.sql": "CREATE INDEX i ON t (a) WITH (fillfactor = 'int')",
21597+
"misc/literal-edge-cases-15.sql": "CREATE INDEX i ON t (a) WITH (opt = 'Foo')",
21598+
"misc/literal-edge-cases-16.sql": "CREATE TABLE t (a int) WITH (opt = 'Foo')",
21599+
"misc/literal-edge-cases-17.sql": "CREATE TABLE t (a int) WITH (opt = 'int')",
21600+
"misc/literal-edge-cases-18.sql": "CREATE TABLE t (a int) WITH (opt = 'select')",
21601+
"misc/literal-edge-cases-19.sql": "ALTER TABLE t SET (opt = 'Foo')",
21602+
"misc/literal-edge-cases-20.sql": "ALTER TABLE t SET (opt = 'int')",
21603+
"misc/literal-edge-cases-21.sql": "ALTER INDEX i SET (opt = 'Foo')",
2158321604
"misc/launchql-ext-types-1.sql": "CREATE DOMAIN attachment AS jsonb CHECK ( value ?& ARRAY['url', 'mime'] AND (value->>'url') ~ '^(https?)://[^\\s/$.?#].[^\\s]*$' )",
2158421605
"misc/launchql-ext-types-2.sql": "COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment'",
2158521606
"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])?)*$' )",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- Edge-case literals the deparser must keep valid
2+
-- Ref: constructive-io/constructive-planning#2175
3+
4+
-- Empty bit-string literals
5+
SELECT X'';
6+
SELECT B'';
7+
SELECT X'', B'', X'1F', B'101';
8+
9+
-- Dollar-quoted bodies that contain the default or fallback delimiter
10+
DO $x$ BEGIN RAISE NOTICE '$_PGFN_$'; END $x$;
11+
DO $x$ BEGIN RAISE NOTICE '$$'; END $x$;
12+
DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$'; END $x$;
13+
DO $x$ BEGIN RAISE NOTICE '$$ $EOFCODE$ $EOFCODE1$'; END $x$;
14+
DO $x$BEGIN NULL; END; --$$x$;
15+
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$_PGFN_$') $x$;
16+
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$ SELECT length('$$ $EOFCODE$') $x$;
17+
CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $x$SELECT 1 --$$x$;
18+
19+
-- String option values must keep their case and stay strings
20+
CREATE VIEW v WITH (security_barrier = 'Foo') AS SELECT 1;
21+
CREATE VIEW v WITH (security_barrier = 'true') AS SELECT 1;
22+
CREATE INDEX i ON t (a) WITH (fillfactor = 'int');
23+
CREATE INDEX i ON t (a) WITH (opt = 'Foo');
24+
CREATE TABLE t (a int) WITH (opt = 'Foo');
25+
CREATE TABLE t (a int) WITH (opt = 'int');
26+
CREATE TABLE t (a int) WITH (opt = 'select');
27+
ALTER TABLE t SET (opt = 'Foo');
28+
ALTER TABLE t SET (opt = 'int');
29+
ALTER INDEX i SET (opt = 'Foo');
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('misc-literal-edge-cases', async () => {
6+
await fixtures.runFixtureTests([
7+
"misc/literal-edge-cases-1.sql",
8+
"misc/literal-edge-cases-2.sql",
9+
"misc/literal-edge-cases-3.sql",
10+
"misc/literal-edge-cases-4.sql",
11+
"misc/literal-edge-cases-5.sql",
12+
"misc/literal-edge-cases-6.sql",
13+
"misc/literal-edge-cases-7.sql",
14+
"misc/literal-edge-cases-8.sql",
15+
"misc/literal-edge-cases-9.sql",
16+
"misc/literal-edge-cases-10.sql",
17+
"misc/literal-edge-cases-11.sql",
18+
"misc/literal-edge-cases-12.sql",
19+
"misc/literal-edge-cases-13.sql",
20+
"misc/literal-edge-cases-14.sql",
21+
"misc/literal-edge-cases-15.sql",
22+
"misc/literal-edge-cases-16.sql",
23+
"misc/literal-edge-cases-17.sql",
24+
"misc/literal-edge-cases-18.sql",
25+
"misc/literal-edge-cases-19.sql",
26+
"misc/literal-edge-cases-20.sql",
27+
"misc/literal-edge-cases-21.sql"
28+
]);
29+
});

‎packages/deparser/src/deparser.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QuoteUtils } from '@pgsql/quotes';
1+
import { keywordKindOf, QuoteUtils } from '@pgsql/quotes';
22
import { Node } from '@pgsql/types';
33
import * as t from '@pgsql/types';
44

@@ -149,7 +149,7 @@ function isWrappedParseResult(obj: any): obj is { ParseResult: t.ParseResult } {
149149
*/
150150
function formatBitString(bsval: string): string {
151151
const quote = (value: string) => value.replace(/'/g, "''");
152-
if (/^x[0-9A-Fa-f]+$/.test(bsval)) {
152+
if (/^x[0-9A-Fa-f]*$/.test(bsval)) {
153153
return `x'${bsval.substring(1)}'`;
154154
}
155155
if (bsval.startsWith('b')) {
@@ -158,6 +158,12 @@ function formatBitString(bsval: string): string {
158158
return `b'${quote(bsval)}'`;
159159
}
160160

161+
// A dollar-quoted body ends at the first occurrence of its tag, including one
162+
// formed by the body's trailing characters plus the closing tag (`...$` + `$$`).
163+
function dollarTagConflicts(body: string, tag: string): boolean {
164+
return (body + tag).indexOf(tag) < body.length;
165+
}
166+
161167
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)*)?)$/;
162168

163169
function formatInteger(ival: unknown): string {
@@ -239,10 +245,19 @@ export class Deparser implements DeparserVisitor {
239245
*/
240246
private getFunctionDelimiter(body: string): string {
241247
const delimiter = this.options.functionDelimiter || '$$';
242-
if (body.includes(delimiter)) {
243-
return this.options.functionDelimiterFallback || '$EOFCODE$';
248+
if (!dollarTagConflicts(body, delimiter)) {
249+
return delimiter;
250+
}
251+
const fallback = this.options.functionDelimiterFallback || '$EOFCODE$';
252+
if (!dollarTagConflicts(body, fallback)) {
253+
return fallback;
254+
}
255+
const base = fallback.slice(0, -1);
256+
let counter = 1;
257+
while (dollarTagConflicts(body, `${base}${counter}$`)) {
258+
counter++;
244259
}
245-
return delimiter;
260+
return `${base}${counter}$`;
246261
}
247262

248263
/**
@@ -10739,7 +10754,9 @@ export class Deparser implements DeparserVisitor {
1073910754
private formatOptionValue(arg: any, argValue: any): any {
1074010755
if (arg && this.getNodeType(arg) === 'String') {
1074110756
const sval: string = this.getNodeData(arg).sval || '';
10742-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(sval) ? sval : QuoteUtils.escape(sval);
10757+
return /^[a-z_][a-z0-9_]*$/.test(sval) && keywordKindOf(sval) === 'NO_KEYWORD'
10758+
? sval
10759+
: QuoteUtils.escape(sval);
1074310760
}
1074410761
return argValue;
1074510762
}

0 commit comments

Comments
 (0)