Skip to content

Commit 6a8a6f7

Browse files
authored
Merge pull request #358 from constructive-io/devin/1791320803-deparser-literal-escaping
fix(deparser): escape string literals and validate numeric literals from hand-built ASTs
2 parents 506aa3b + f190948 commit 6a8a6f7

5 files changed

Lines changed: 269 additions & 65 deletions

File tree

‎__fixtures__/generated/generated.json‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21548,6 +21548,38 @@
2154821548
"misc/merge-seclabel-6.sql": "SECURITY LABEL FOR anon ON COLUMN myschema.users.name IS 'MASKED WITH FUNCTION anon.fake_name()'",
2154921549
"misc/merge-seclabel-7.sql": "SECURITY LABEL FOR anon ON TABLE myschema.users IS 'sensitive'",
2155021550
"misc/merge-seclabel-8.sql": "SECURITY LABEL ON FUNCTION myschema.fn() IS 'labeled'",
21551+
"misc/literal-escaping-1.sql": "PREPARE TRANSACTION 'it''s'",
21552+
"misc/literal-escaping-2.sql": "COMMIT PREPARED 'it''s'",
21553+
"misc/literal-escaping-3.sql": "ROLLBACK PREPARED 'it''s'",
21554+
"misc/literal-escaping-4.sql": "NOTIFY ch, 'it''s'",
21555+
"misc/literal-escaping-5.sql": "LOAD 'it''s.so'",
21556+
"misc/literal-escaping-6.sql": "CREATE TABLESPACE ts LOCATION '/tmp/it''s'",
21557+
"misc/literal-escaping-7.sql": "SECURITY LABEL ON TABLE t IS 'it''s'",
21558+
"misc/literal-escaping-8.sql": "CREATE SUBSCRIPTION s CONNECTION 'host=it''s' PUBLICATION p",
21559+
"misc/literal-escaping-9.sql": "CREATE CONVERSION c FOR 'it''s' TO 'UTF8' FROM f",
21560+
"misc/literal-escaping-10.sql": "SET search_path = 'it''s'",
21561+
"misc/literal-escaping-11.sql": "CREATE INDEX i ON t USING gist (c opc (opt = 'it''s'))",
21562+
"misc/literal-escaping-12.sql": "CREATE INDEX i ON t (c) WITH (opt = 'it''s')",
21563+
"misc/literal-escaping-13.sql": "CREATE TABLE t (a int) WITH (opt = 'it''s')",
21564+
"misc/literal-escaping-14.sql": "ALTER TABLE t SET (opt = 'it''s')",
21565+
"misc/literal-escaping-15.sql": "CREATE FOREIGN DATA WRAPPER w OPTIONS (opt 'it''s')",
21566+
"misc/literal-escaping-16.sql": "ALTER FOREIGN DATA WRAPPER w OPTIONS (ADD opt 'it''s')",
21567+
"misc/literal-escaping-17.sql": "CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS (host 'it''s')",
21568+
"misc/literal-escaping-18.sql": "CREATE USER MAPPING FOR u SERVER s OPTIONS (password 'it''s')",
21569+
"misc/literal-escaping-19.sql": "CREATE FOREIGN TABLE ft (a int OPTIONS (col 'it''s')) SERVER s",
21570+
"misc/literal-escaping-20.sql": "CREATE ROLE r PASSWORD 'it''s'",
21571+
"misc/literal-escaping-21.sql": "CREATE ROLE r VALID UNTIL 'it''s'",
21572+
"misc/literal-escaping-22.sql": "ALTER EXTENSION e UPDATE TO 'it''s'",
21573+
"misc/literal-escaping-23.sql": "CREATE EVENT TRIGGER e ON ddl_command_start WHEN TAG IN ('it''s', 'CREATE TABLE') EXECUTE FUNCTION f()",
21574+
"misc/literal-escaping-24.sql": "CREATE TYPE ty (input = i, output = o, category = '''')",
21575+
"misc/literal-escaping-25.sql": "CREATE AGGREGATE agg (int) (sfunc = f, stype = int, initcond = 'it''s')",
21576+
"misc/literal-escaping-26.sql": "CREATE COLLATION c (locale = 'it''s')",
21577+
"misc/literal-escaping-27.sql": "CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION f('it''s', 'b')",
21578+
"misc/literal-escaping-28.sql": "SELECT * FROM XMLTABLE('/r' PASSING doc COLUMNS a int PATH 'it''s')",
21579+
"misc/literal-escaping-29.sql": "SELECT * FROM XMLTABLE('/r' PASSING doc COLUMNS a int PATH 'a', b text PATH 'b''c' DEFAULT 'd' NOT NULL, n FOR ORDINALITY)",
21580+
"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",
21581+
"misc/literal-escaping-31.sql": "SELECT * FROM t, LATERAL XMLTABLE('/r' PASSING t.doc COLUMNS a int PATH 'a') xt",
21582+
"misc/literal-escaping-32.sql": "SELECT 1_000.000_1, 1_000.5e1_0, 0x_FFFF_FFFF_FFFF_FFFF",
2155121583
"misc/launchql-ext-types-1.sql": "CREATE DOMAIN attachment AS jsonb CHECK ( value ?& ARRAY['url', 'mime'] AND (value->>'url') ~ '^(https?)://[^\\s/$.?#].[^\\s]*$' )",
2155221584
"misc/launchql-ext-types-2.sql": "COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment'",
2155321585
"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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- String values containing single quotes must stay inside one literal
2+
-- Ref: constructive-io/pgsql-parser#355
3+
PREPARE TRANSACTION 'it''s';
4+
COMMIT PREPARED 'it''s';
5+
ROLLBACK PREPARED 'it''s';
6+
NOTIFY ch, 'it''s';
7+
LOAD 'it''s.so';
8+
CREATE TABLESPACE ts LOCATION '/tmp/it''s';
9+
SECURITY LABEL ON TABLE t IS 'it''s';
10+
CREATE SUBSCRIPTION s CONNECTION 'host=it''s' PUBLICATION p;
11+
CREATE CONVERSION c FOR 'it''s' TO 'UTF8' FROM f;
12+
SET search_path = 'it''s';
13+
CREATE INDEX i ON t USING gist (c opc (opt = 'it''s'));
14+
CREATE INDEX i ON t (c) WITH (opt = 'it''s');
15+
CREATE TABLE t (a int) WITH (opt = 'it''s');
16+
ALTER TABLE t SET (opt = 'it''s');
17+
CREATE FOREIGN DATA WRAPPER w OPTIONS (opt 'it''s');
18+
ALTER FOREIGN DATA WRAPPER w OPTIONS (ADD opt 'it''s');
19+
CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS (host 'it''s');
20+
CREATE USER MAPPING FOR u SERVER s OPTIONS (password 'it''s');
21+
CREATE FOREIGN TABLE ft (a int OPTIONS (col 'it''s')) SERVER s;
22+
CREATE ROLE r PASSWORD 'it''s';
23+
CREATE ROLE r VALID UNTIL 'it''s';
24+
ALTER EXTENSION e UPDATE TO 'it''s';
25+
CREATE EVENT TRIGGER e ON ddl_command_start WHEN TAG IN ('it''s', 'CREATE TABLE') EXECUTE FUNCTION f();
26+
CREATE TYPE ty (input = i, output = o, category = '''');
27+
CREATE AGGREGATE agg (int) (sfunc = f, stype = int, initcond = 'it''s');
28+
CREATE COLLATION c (locale = 'it''s');
29+
CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION f('it''s', 'b');
30+
31+
-- XMLTABLE
32+
SELECT * FROM XMLTABLE('/r' PASSING doc COLUMNS a int PATH 'it''s');
33+
SELECT * FROM XMLTABLE('/r' PASSING doc COLUMNS a int PATH 'a', b text PATH 'b''c' DEFAULT 'd' NOT NULL, n FOR ORDINALITY);
34+
SELECT * FROM XMLTABLE(XMLNAMESPACES('http://x' AS x, DEFAULT 'http://d'), '/x:r' PASSING (SELECT doc FROM t) COLUMNS a int) AS xt;
35+
SELECT * FROM t, LATERAL XMLTABLE('/r' PASSING t.doc COLUMNS a int PATH 'a') xt;
36+
37+
-- Numeric literals with underscores
38+
SELECT 1_000.000_1, 1_000.5e1_0, 0x_FFFF_FFFF_FFFF_FFFF;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('misc-literal-escaping', async () => {
6+
await fixtures.runFixtureTests([
7+
"misc/literal-escaping-1.sql",
8+
"misc/literal-escaping-2.sql",
9+
"misc/literal-escaping-3.sql",
10+
"misc/literal-escaping-4.sql",
11+
"misc/literal-escaping-5.sql",
12+
"misc/literal-escaping-6.sql",
13+
"misc/literal-escaping-7.sql",
14+
"misc/literal-escaping-8.sql",
15+
"misc/literal-escaping-9.sql",
16+
"misc/literal-escaping-10.sql",
17+
"misc/literal-escaping-11.sql",
18+
"misc/literal-escaping-12.sql",
19+
"misc/literal-escaping-13.sql",
20+
"misc/literal-escaping-14.sql",
21+
"misc/literal-escaping-15.sql",
22+
"misc/literal-escaping-16.sql",
23+
"misc/literal-escaping-17.sql",
24+
"misc/literal-escaping-18.sql",
25+
"misc/literal-escaping-19.sql",
26+
"misc/literal-escaping-20.sql",
27+
"misc/literal-escaping-21.sql",
28+
"misc/literal-escaping-22.sql",
29+
"misc/literal-escaping-23.sql",
30+
"misc/literal-escaping-24.sql",
31+
"misc/literal-escaping-25.sql",
32+
"misc/literal-escaping-26.sql",
33+
"misc/literal-escaping-27.sql",
34+
"misc/literal-escaping-28.sql",
35+
"misc/literal-escaping-29.sql",
36+
"misc/literal-escaping-30.sql",
37+
"misc/literal-escaping-31.sql",
38+
"misc/literal-escaping-32.sql"
39+
]);
40+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Deparser } from '../../src';
2+
3+
// Values that parsed SQL can never produce, so they can't live in a
4+
// kitchen-sink fixture (see __fixtures__/kitchen-sink/misc/literal-escaping.sql).
5+
6+
describe('numeric literals in hand-built ASTs', () => {
7+
const selectConst = (aConst: any) => ({
8+
SelectStmt: {
9+
targetList: [{ ResTarget: { val: { A_Const: aConst } } }],
10+
limitOption: 'LIMIT_OPTION_DEFAULT',
11+
op: 'SETOP_NONE'
12+
}
13+
});
14+
15+
it.each([
16+
['wrapped fval', { fval: { fval: '1; DROP TABLE t' } }],
17+
['unwrapped fval', { fval: '1 OR 1=1' }],
18+
['val.Float', { val: { Float: { fval: '1)--' } } }],
19+
['wrapped ival', { ival: { ival: '1; DROP TABLE t' } }],
20+
['unwrapped ival', { ival: '1 OR 1=1' }],
21+
['val.Integer', { val: { Integer: { ival: '1)--' } } }],
22+
['doubled underscore', { fval: { fval: '1__0.5' } }],
23+
['trailing underscore', { fval: { fval: '1_.5' } }],
24+
['trailing hex underscore', { fval: { fval: '0xFF_' } }]
25+
])('rejects non-numeric %s', (_label, aConst) => {
26+
expect(() => Deparser.deparse(selectConst(aConst) as any)).toThrow(/Invalid (numeric|integer) literal/);
27+
});
28+
29+
it('rejects non-numeric Float and Integer nodes', () => {
30+
const deparser = new Deparser([]);
31+
expect(() => deparser.Float({ fval: '1; DROP TABLE t' }, {} as any)).toThrow(/Invalid numeric literal/);
32+
expect(() => deparser.Integer({ ival: '1; DROP TABLE t' as any }, {} as any)).toThrow(/Invalid integer literal/);
33+
});
34+
});
35+
36+
describe('TableFunc (JSON_TABLE)', () => {
37+
it('does not re-quote the row path', () => {
38+
const deparser = new Deparser([]);
39+
const sql = deparser.TableFunc({
40+
functype: 'TFT_JSON_TABLE',
41+
docexpr: { ColumnRef: { fields: [{ String: { sval: 'doc' } }] } },
42+
rowexpr: { A_Const: { sval: { sval: "$.a' OR '1'='1" } } }
43+
} as any, {} as any);
44+
expect(sql).toBe("JSON_TABLE (doc) , '$.a'' OR ''1''=''1'");
45+
});
46+
});

0 commit comments

Comments
 (0)