From d30d6c2dad94873db9ccba4a64e756a813aadd20 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 04:10:46 +0000 Subject: [PATCH] feat(deparser): support JSON_QUERY, JSON_VALUE, JSON_EXISTS and JSON_TABLE --- __fixtures__/generated/generated.json | 32 +++ .../misc/json-query-functions.sql | 21 ++ __fixtures__/kitchen-sink/misc/json-table.sql | 11 ++ .../misc-json-query-functions.test.ts | 29 +++ .../kitchen-sink/misc-json-table.test.ts | 19 ++ packages/deparser/scripts/clean-utils.ts | 1 + packages/deparser/src/deparser.ts | 183 ++++++++++++++++++ 7 files changed, 296 insertions(+) create mode 100644 __fixtures__/kitchen-sink/misc/json-query-functions.sql create mode 100644 __fixtures__/kitchen-sink/misc/json-table.sql create mode 100644 packages/deparser/__tests__/kitchen-sink/misc-json-query-functions.test.ts create mode 100644 packages/deparser/__tests__/kitchen-sink/misc-json-table.test.ts diff --git a/__fixtures__/generated/generated.json b/__fixtures__/generated/generated.json index a9ad7570..b129651a 100644 --- a/__fixtures__/generated/generated.json +++ b/__fixtures__/generated/generated.json @@ -21620,6 +21620,38 @@ "misc/launchql-ext-default-roles-1.sql": "DO $$\n BEGIN\n IF NOT EXISTS (\n SELECT\n 1\n FROM\n pg_roles\n WHERE\n rolname = 'anonymous') THEN\n CREATE ROLE anonymous;\n COMMENT ON ROLE anonymous IS 'Anonymous group';\n ALTER USER anonymous WITH NOCREATEDB;\n ALTER USER anonymous WITH NOCREATEROLE;\n ALTER USER anonymous WITH NOLOGIN;\n ALTER USER anonymous WITH NOBYPASSRLS;\nEND IF;\nEND $$", "misc/launchql-ext-default-roles-2.sql": "DO $$\n BEGIN\n IF NOT EXISTS (\n SELECT\n 1\n FROM\n pg_roles\n WHERE\n rolname = 'authenticated') THEN\n CREATE ROLE authenticated;\n COMMENT ON ROLE authenticated IS 'Authenticated group';\n ALTER USER authenticated WITH NOCREATEDB;\n ALTER USER authenticated WITH NOCREATEROLE;\n ALTER USER authenticated WITH NOLOGIN;\n ALTER USER authenticated WITH NOBYPASSRLS;\nEND IF;\nEND $$", "misc/launchql-ext-default-roles-3.sql": "DO $$\n BEGIN\n IF NOT EXISTS (\n SELECT\n 1\n FROM\n pg_roles\n WHERE\n rolname = 'administrator') THEN\n CREATE ROLE administrator;\n COMMENT ON ROLE administrator IS 'Administration group';\n ALTER USER administrator WITH NOCREATEDB;\n ALTER USER administrator WITH NOCREATEROLE;\n ALTER USER administrator WITH NOLOGIN;\n ALTER USER administrator WITH BYPASSRLS;\n GRANT anonymous TO administrator;\n GRANT authenticated TO administrator;\nEND IF;\nEND $$", + "misc/json-table-1.sql": "SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' COLUMNS (a int PATH '$'))", + "misc/json-table-2.sql": "SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' COLUMNS (id FOR ORDINALITY, a int)) jt", + "misc/json-table-3.sql": "SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' AS root COLUMNS (a int PATH '$')) AS jt (x)", + "misc/json-table-4.sql": "SELECT * FROM JSON_TABLE(jsonb '{\"a\": [1]}', '$' PASSING 1 AS x, 'y' AS \"Y\" COLUMNS (a int PATH '$.a[*] ? (@ > $x)' DEFAULT -1 ON EMPTY ERROR ON ERROR) ERROR ON ERROR) jt", + "misc/json-table-5.sql": "SELECT * FROM JSON_TABLE(jsonb '{\"a\": 1}', '$' COLUMNS (e bool EXISTS PATH '$.a', e2 int EXISTS PATH '$.b' TRUE ON ERROR)) jt", + "misc/json-table-6.sql": "SELECT * FROM JSON_TABLE(jsonb '{\"a\": \"x\"}', '$' COLUMNS (js jsonb FORMAT JSON PATH '$.a' WITH CONDITIONAL WRAPPER OMIT QUOTES, js2 text FORMAT JSON ENCODING UTF8 PATH '$' WITHOUT WRAPPER KEEP QUOTES EMPTY OBJECT ON ERROR)) jt", + "misc/json-table-7.sql": "SELECT * FROM JSON_TABLE(jsonb '{\"a\": \"x\"}', '$' COLUMNS (q jsonb PATH '$.a' WITH WRAPPER EMPTY ARRAY ON EMPTY, \"Mixed Col\" text PATH '$.a' NULL ON ERROR)) jt", + "misc/json-table-8.sql": "SELECT * FROM JSON_TABLE(jsonb '{\"a\": [{\"b\": 1}]}', '$' COLUMNS (id FOR ORDINALITY, NESTED PATH '$.a[*]' COLUMNS (b int PATH '$.b', NESTED PATH '$.c[*]' AS c_path COLUMNS (c text)), NESTED '$.d' AS d COLUMNS (d int))) jt", + "misc/json-table-9.sql": "SELECT jt.* FROM t, LATERAL JSON_TABLE(t.js, '$[*]' COLUMNS (a text PATH '$.a')) AS jt", + "misc/json-table-10.sql": "SELECT jt.* FROM t CROSS JOIN JSON_TABLE(t.js FORMAT JSON, '$[*]' COLUMNS (a text)) jt", + "misc/json-table-11.sql": "CREATE VIEW json_table_view AS SELECT * FROM JSON_TABLE(jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '\"foo\"' AS \"b c\" COLUMNS (id FOR ORDINALITY, \"int\" int PATH '$', js json PATH '$', jsb jsonb PATH '$', exists1 bool EXISTS PATH '$.aaa', NESTED PATH '$[1]' AS p1 COLUMNS (a1 int, NESTED PATH '$[*]' AS \"p1 1\" COLUMNS (a11 text)))) jt", + "misc/json-query-functions-1.sql": "SELECT JSON_EXISTS(jsonb '{\"a\": 1}', '$.a')", + "misc/json-query-functions-2.sql": "SELECT JSON_EXISTS(jsonb '{\"a\": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)", + "misc/json-query-functions-3.sql": "SELECT JSON_EXISTS(jsonb '[1,2,3]', '$[*] ? (@ > $x)' PASSING 2 AS x, 'b' AS \"Y\" UNKNOWN ON ERROR)", + "misc/json-query-functions-4.sql": "SELECT JSON_EXISTS(js FORMAT JSON, '$.a' TRUE ON ERROR) FROM t", + "misc/json-query-functions-5.sql": "SELECT JSON_VALUE(jsonb '\"aaa\"', '$')", + "misc/json-query-functions-6.sql": "SELECT JSON_VALUE(jsonb '123', '$' RETURNING int)", + "misc/json-query-functions-7.sql": "SELECT JSON_VALUE(jsonb '\"2017-02-20\"', '$' RETURNING date DEFAULT '2000-01-01' ON EMPTY NULL ON ERROR)", + "misc/json-query-functions-8.sql": "SELECT JSON_VALUE(jsonb '[1,2]', 'strict $[*]' DEFAULT 0 + 1 ON ERROR)", + "misc/json-query-functions-9.sql": "SELECT JSON_VALUE(js, '$.a' PASSING x + 1 AS x RETURNING text ERROR ON EMPTY ERROR ON ERROR) FROM t", + "misc/json-query-functions-10.sql": "SELECT JSON_QUERY(jsonb '[1,2]', '$[*]')", + "misc/json-query-functions-11.sql": "SELECT JSON_QUERY(jsonb '\"aaa\"', '$' RETURNING text KEEP QUOTES)", + "misc/json-query-functions-12.sql": "SELECT JSON_QUERY(jsonb '\"aaa\"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING)", + "misc/json-query-functions-13.sql": "SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)", + "misc/json-query-functions-14.sql": "SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH CONDITIONAL ARRAY WRAPPER)", + "misc/json-query-functions-15.sql": "SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH UNCONDITIONAL WRAPPER)", + "misc/json-query-functions-16.sql": "SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITHOUT WRAPPER)", + "misc/json-query-functions-17.sql": "SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY)", + "misc/json-query-functions-18.sql": "SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR)", + "misc/json-query-functions-19.sql": "SELECT JSON_QUERY(js FORMAT JSON, '$.a' PASSING 1 AS x RETURNING bytea FORMAT JSON ENCODING UTF8 WITH CONDITIONAL WRAPPER KEEP QUOTES DEFAULT '\"x\"' ON EMPTY ERROR ON ERROR) FROM t", + "misc/json-query-functions-20.sql": "CREATE VIEW json_query_v AS SELECT JSON_QUERY(jsonb '{\"a\": 1}', '$.a' RETURNING jsonb WITH WRAPPER NULL ON ERROR) AS q, JSON_VALUE(jsonb '1', '$' RETURNING int) AS v, JSON_EXISTS(jsonb '1', '$') AS e", + "misc/json-query-functions-21.sql": "CREATE TABLE json_check (js jsonb CHECK (JSON_EXISTS(js, '$.a')), v text DEFAULT JSON_VALUE(jsonb '\"x\"', '$'))", "misc/issues-1.sql": "select from test_table WHERE status = 'complete'::text", "misc/issues-2.sql": "select from test_table WHERE status = 'complete'", "misc/issues-3.sql": "CREATE TABLE new_style (\n id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n val1 TEXT NOT NULL,\n val2 TEXT NULL,\n CONSTRAINT uq_val1_val2_new UNIQUE NULLS NOT DISTINCT (val1, val2)\n)", diff --git a/__fixtures__/kitchen-sink/misc/json-query-functions.sql b/__fixtures__/kitchen-sink/misc/json-query-functions.sql new file mode 100644 index 00000000..8dc1dd42 --- /dev/null +++ b/__fixtures__/kitchen-sink/misc/json-query-functions.sql @@ -0,0 +1,21 @@ +SELECT JSON_EXISTS(jsonb '{"a": 1}', '$.a'); +SELECT JSON_EXISTS(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR); +SELECT JSON_EXISTS(jsonb '[1,2,3]', '$[*] ? (@ > $x)' PASSING 2 AS x, 'b' AS "Y" UNKNOWN ON ERROR); +SELECT JSON_EXISTS(js FORMAT JSON, '$.a' TRUE ON ERROR) FROM t; +SELECT JSON_VALUE(jsonb '"aaa"', '$'); +SELECT JSON_VALUE(jsonb '123', '$' RETURNING int); +SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date DEFAULT '2000-01-01' ON EMPTY NULL ON ERROR); +SELECT JSON_VALUE(jsonb '[1,2]', 'strict $[*]' DEFAULT 0 + 1 ON ERROR); +SELECT JSON_VALUE(js, '$.a' PASSING x + 1 AS x RETURNING text ERROR ON EMPTY ERROR ON ERROR) FROM t; +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]'); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES); +SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH CONDITIONAL ARRAY WRAPPER); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH UNCONDITIONAL WRAPPER); +SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' WITHOUT WRAPPER); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY); +SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR); +SELECT JSON_QUERY(js FORMAT JSON, '$.a' PASSING 1 AS x RETURNING bytea FORMAT JSON ENCODING UTF8 WITH CONDITIONAL WRAPPER KEEP QUOTES DEFAULT '"x"' ON EMPTY ERROR ON ERROR) FROM t; +CREATE VIEW json_query_v AS SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING jsonb WITH WRAPPER NULL ON ERROR) AS q, JSON_VALUE(jsonb '1', '$' RETURNING int) AS v, JSON_EXISTS(jsonb '1', '$') AS e; +CREATE TABLE json_check (js jsonb CHECK (JSON_EXISTS(js, '$.a')), v text DEFAULT JSON_VALUE(jsonb '"x"', '$')); diff --git a/__fixtures__/kitchen-sink/misc/json-table.sql b/__fixtures__/kitchen-sink/misc/json-table.sql new file mode 100644 index 00000000..f08fa382 --- /dev/null +++ b/__fixtures__/kitchen-sink/misc/json-table.sql @@ -0,0 +1,11 @@ +SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' COLUMNS (a int PATH '$')); +SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' COLUMNS (id FOR ORDINALITY, a int)) jt; +SELECT * FROM JSON_TABLE(jsonb '[1, 2]', '$[*]' AS root COLUMNS (a int PATH '$')) AS jt (x); +SELECT * FROM JSON_TABLE(jsonb '{"a": [1]}', '$' PASSING 1 AS x, 'y' AS "Y" COLUMNS (a int PATH '$.a[*] ? (@ > $x)' DEFAULT -1 ON EMPTY ERROR ON ERROR) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '{"a": 1}', '$' COLUMNS (e bool EXISTS PATH '$.a', e2 int EXISTS PATH '$.b' TRUE ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '{"a": "x"}', '$' COLUMNS (js jsonb FORMAT JSON PATH '$.a' WITH CONDITIONAL WRAPPER OMIT QUOTES, js2 text FORMAT JSON ENCODING UTF8 PATH '$' WITHOUT WRAPPER KEEP QUOTES EMPTY OBJECT ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '{"a": "x"}', '$' COLUMNS (q jsonb PATH '$.a' WITH WRAPPER EMPTY ARRAY ON EMPTY, "Mixed Col" text PATH '$.a' NULL ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '{"a": [{"b": 1}]}', '$' COLUMNS (id FOR ORDINALITY, NESTED PATH '$.a[*]' COLUMNS (b int PATH '$.b', NESTED PATH '$.c[*]' AS c_path COLUMNS (c text)), NESTED '$.d' AS d COLUMNS (d int))) jt; +SELECT jt.* FROM t, LATERAL JSON_TABLE(t.js, '$[*]' COLUMNS (a text PATH '$.a')) AS jt; +SELECT jt.* FROM t CROSS JOIN JSON_TABLE(t.js FORMAT JSON, '$[*]' COLUMNS (a text)) jt; +CREATE VIEW json_table_view AS SELECT * FROM JSON_TABLE(jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" COLUMNS (id FOR ORDINALITY, "int" int PATH '$', js json PATH '$', jsb jsonb PATH '$', exists1 bool EXISTS PATH '$.aaa', NESTED PATH '$[1]' AS p1 COLUMNS (a1 int, NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text)))) jt; diff --git a/packages/deparser/__tests__/kitchen-sink/misc-json-query-functions.test.ts b/packages/deparser/__tests__/kitchen-sink/misc-json-query-functions.test.ts new file mode 100644 index 00000000..cc4dbcf3 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/misc-json-query-functions.test.ts @@ -0,0 +1,29 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('misc-json-query-functions', async () => { + await fixtures.runFixtureTests([ + "misc/json-query-functions-1.sql", + "misc/json-query-functions-2.sql", + "misc/json-query-functions-3.sql", + "misc/json-query-functions-4.sql", + "misc/json-query-functions-5.sql", + "misc/json-query-functions-6.sql", + "misc/json-query-functions-7.sql", + "misc/json-query-functions-8.sql", + "misc/json-query-functions-9.sql", + "misc/json-query-functions-10.sql", + "misc/json-query-functions-11.sql", + "misc/json-query-functions-12.sql", + "misc/json-query-functions-13.sql", + "misc/json-query-functions-14.sql", + "misc/json-query-functions-15.sql", + "misc/json-query-functions-16.sql", + "misc/json-query-functions-17.sql", + "misc/json-query-functions-18.sql", + "misc/json-query-functions-19.sql", + "misc/json-query-functions-20.sql", + "misc/json-query-functions-21.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/misc-json-table.test.ts b/packages/deparser/__tests__/kitchen-sink/misc-json-table.test.ts new file mode 100644 index 00000000..389769ef --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/misc-json-table.test.ts @@ -0,0 +1,19 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('misc-json-table', async () => { + await fixtures.runFixtureTests([ + "misc/json-table-1.sql", + "misc/json-table-2.sql", + "misc/json-table-3.sql", + "misc/json-table-4.sql", + "misc/json-table-5.sql", + "misc/json-table-6.sql", + "misc/json-table-7.sql", + "misc/json-table-8.sql", + "misc/json-table-9.sql", + "misc/json-table-10.sql", + "misc/json-table-11.sql" +]); +}); diff --git a/packages/deparser/scripts/clean-utils.ts b/packages/deparser/scripts/clean-utils.ts index 4d32ebaa..8d91dc7e 100644 --- a/packages/deparser/scripts/clean-utils.ts +++ b/packages/deparser/scripts/clean-utils.ts @@ -64,6 +64,7 @@ export const cleanTree = (tree: any) => { stmt_len: noop, stmt_location: noop, location: noop, + name_location: noop, rexpr_list_start: noop, rexpr_list_end: noop, list_start: noop, diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 94c7f625..5085d4de 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -11098,6 +11098,189 @@ export class Deparser implements DeparserVisitor { return parts.join(' '); } + JsonBehavior(node: t.JsonBehavior, context: DeparserContext): string { + switch (node.btype) { + case 'JSON_BEHAVIOR_NULL': + return 'NULL'; + case 'JSON_BEHAVIOR_ERROR': + return 'ERROR'; + case 'JSON_BEHAVIOR_EMPTY': + return 'EMPTY'; + case 'JSON_BEHAVIOR_TRUE': + return 'TRUE'; + case 'JSON_BEHAVIOR_FALSE': + return 'FALSE'; + case 'JSON_BEHAVIOR_UNKNOWN': + return 'UNKNOWN'; + case 'JSON_BEHAVIOR_EMPTY_ARRAY': + return 'EMPTY ARRAY'; + case 'JSON_BEHAVIOR_EMPTY_OBJECT': + return 'EMPTY OBJECT'; + case 'JSON_BEHAVIOR_DEFAULT': + return `DEFAULT ${this.visit(node.expr as Node, context)}`; + default: + throw new Error(`Unsupported JsonBehavior btype: ${node.btype}`); + } + } + + JsonArgument(node: t.JsonArgument, context: DeparserContext): string { + const val = node.val ? this.JsonValueExpr(node.val, context) : ''; + return `${val} AS ${QuoteUtils.quoteIdentifier(node.name)}`; + } + + formatJsonPassing(passing: t.Node[] | undefined, context: DeparserContext): string | null { + if (!passing || passing.length === 0) return null; + return 'PASSING ' + ListUtils.unwrapList(passing).map(arg => this.visit(arg, context)).join(', '); + } + + formatJsonWrapper(wrapper: t.JsonWrapper | undefined): string | null { + switch (wrapper) { + case 'JSW_NONE': + return 'WITHOUT WRAPPER'; + case 'JSW_CONDITIONAL': + return 'WITH CONDITIONAL WRAPPER'; + case 'JSW_UNCONDITIONAL': + return 'WITH UNCONDITIONAL WRAPPER'; + default: + return null; + } + } + + formatJsonQuotes(quotes: t.JsonQuotes | undefined): string | null { + switch (quotes) { + case 'JS_QUOTES_KEEP': + return 'KEEP QUOTES'; + case 'JS_QUOTES_OMIT': + return 'OMIT QUOTES'; + default: + return null; + } + } + + formatJsonBehaviors(onEmpty: t.JsonBehavior | undefined, onError: t.JsonBehavior | undefined, context: DeparserContext): string[] { + const parts: string[] = []; + if (onEmpty) parts.push(`${this.JsonBehavior(onEmpty, context)} ON EMPTY`); + if (onError) parts.push(`${this.JsonBehavior(onError, context)} ON ERROR`); + return parts; + } + + JsonFuncExpr(node: t.JsonFuncExpr, context: DeparserContext): string { + let name: string; + switch (node.op) { + case 'JSON_EXISTS_OP': + name = 'JSON_EXISTS'; + break; + case 'JSON_QUERY_OP': + name = 'JSON_QUERY'; + break; + case 'JSON_VALUE_OP': + name = 'JSON_VALUE'; + break; + default: + throw new Error(`Unsupported JsonFuncExpr op: ${node.op}`); + } + + const args: string[] = []; + const contextItem = node.context_item ? this.JsonValueExpr(node.context_item, context) : ''; + args.push(`${contextItem}, ${this.visit(node.pathspec as Node, context)}`); + + const passing = this.formatJsonPassing(node.passing, context); + if (passing) args.push(passing); + + const output = this.formatJsonOutput(node.output, context); + if (output) args.push(output); + + const wrapper = this.formatJsonWrapper(node.wrapper); + if (wrapper) args.push(wrapper); + + const quotes = this.formatJsonQuotes(node.quotes); + if (quotes) args.push(quotes); + + args.push(...this.formatJsonBehaviors(node.on_empty, node.on_error, context)); + + return `${name}(${args.join(' ')})`; + } + + JsonTablePathSpec(node: t.JsonTablePathSpec, context: DeparserContext): string { + const path = this.visit(node.string as Node, context); + return node.name ? `${path} AS ${QuoteUtils.quoteIdentifier(node.name)}` : path; + } + + formatJsonTableColumns(columns: t.Node[] | undefined, context: DeparserContext): string { + const cols = ListUtils.unwrapList(columns || []).map(col => this.visit(col, context)); + return `COLUMNS (${cols.join(', ')})`; + } + + JsonTableColumn(node: t.JsonTableColumn, context: DeparserContext): string { + if (node.coltype === 'JTC_NESTED') { + const path = node.pathspec ? this.JsonTablePathSpec(node.pathspec, context) : ''; + return `NESTED PATH ${path} ${this.formatJsonTableColumns(node.columns, context)}`; + } + + const parts: string[] = [QuoteUtils.quoteIdentifier(node.name)]; + + if (node.coltype === 'JTC_FOR_ORDINALITY') { + parts.push('FOR ORDINALITY'); + return parts.join(' '); + } + + if (node.typeName) { + parts.push(this.TypeName(node.typeName, context)); + } + + if (node.coltype === 'JTC_EXISTS') { + parts.push('EXISTS'); + } else if (node.coltype === 'JTC_FORMATTED') { + parts.push(this.formatJsonFormat(node.format) || 'FORMAT JSON'); + } + + if (node.pathspec) { + parts.push('PATH', this.JsonTablePathSpec(node.pathspec, context)); + } + + if (node.coltype !== 'JTC_EXISTS') { + const wrapper = this.formatJsonWrapper(node.wrapper); + if (wrapper) parts.push(wrapper); + + const quotes = this.formatJsonQuotes(node.quotes); + if (quotes) parts.push(quotes); + } + + parts.push(...this.formatJsonBehaviors(node.on_empty, node.on_error, context)); + + return parts.join(' '); + } + + JsonTable(node: t.JsonTable, context: DeparserContext): string { + const output: string[] = []; + + if (node.lateral) { + output.push('LATERAL'); + } + + const args: string[] = []; + const contextItem = node.context_item ? this.JsonValueExpr(node.context_item, context) : ''; + const pathspec = node.pathspec ? this.JsonTablePathSpec(node.pathspec, context) : ''; + args.push(`${contextItem}, ${pathspec}`); + + const passing = this.formatJsonPassing(node.passing, context); + if (passing) args.push(passing); + + args.push(this.formatJsonTableColumns(node.columns, context)); + + if (node.on_error) { + args.push(`${this.JsonBehavior(node.on_error, context)} ON ERROR`); + } + + output.push(`JSON_TABLE(${args.join(' ')})`); + + if (node.alias) { + output.push(this.Alias(node.alias, context)); + } + + return output.join(' '); + } + RangeFunction(node: t.RangeFunction, context: DeparserContext): string { const output: string[] = [];