Skip to content
Open
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
32 changes: 32 additions & 0 deletions __fixtures__/generated/generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
21 changes: 21 additions & 0 deletions __fixtures__/kitchen-sink/misc/json-query-functions.sql
Original file line number Diff line number Diff line change
@@ -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"', '$'));
11 changes: 11 additions & 0 deletions __fixtures__/kitchen-sink/misc/json-table.sql
Original file line number Diff line number Diff line change
@@ -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;
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-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"
]);
});
19 changes: 19 additions & 0 deletions packages/deparser/__tests__/kitchen-sink/misc-json-table.test.ts
Original file line number Diff line number Diff line change
@@ -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"
]);
});
1 change: 1 addition & 0 deletions packages/deparser/scripts/clean-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
183 changes: 183 additions & 0 deletions packages/deparser/src/deparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Comment on lines +11149 to +11158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

ON SCALAR STRING silently dropped from JSON quote clauses

The quotes field is a bare enum (JS_QUOTES_KEEP/JS_QUOTES_OMIT), so formatJsonQuotes can only emit KEEP QUOTES/OMIT QUOTES and silently drops the ON SCALAR STRING modifier (packages/deparser/src/deparser.ts:11149). Upstream PostgreSQL carries the ON SCALAR STRING flag as a separate boolean on the JSON quotes node and its ruleutils prints it back, but this repo's libpg-query protobuf (JsonQuotes enum in __fixtures__/proto/17-latest.proto:2900) has no such field, so the flag is already lost in the parse tree. JSON_QUERY(x, '$' OMIT QUOTES ON SCALAR STRING) therefore deparses as OMIT QUOTES, which omits quotes unconditionally instead of only for scalar strings — semantically different SQL with no error. The kitchen-sink fixture __fixtures__/kitchen-sink/misc/json-query-functions.sql:12 exercises exactly this clause, and round-trip tests pass only because reparse matches the already-lossy AST.

📋 Prompt for AI Agents

In packages/deparser/src/deparser.ts around lines 11149-11158 (formatJsonQuotes) and its callers JsonFuncExpr (line ~11196) and JsonTableColumn (line ~11245), handle the PostgreSQL ON SCALAR STRING modifier that upstream attaches to the JSON quotes node. First inspect the libpg-query version used at runtime to see whether it exposes a scalar_string/on_scalar field next to the JsonQuotes enum (the fixture protos in fixtures/proto/17-latest.proto lack it); if the runtime parse tree carries it, thread it through the types and emit ON SCALAR STRING when set. If the proto cannot represent the flag, add a test/comment documenting that OMIT QUOTES ON SCALAR STRING round-trips lossily (e.g. in fixtures/kitchen-sink/misc/json-query-functions.sql line 12) instead of silently emitting semantically different SQL.


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[] = [];

Expand Down
Loading