Skip to content

Commit a0033bd

Browse files
authored
Merge pull request #304 from constructive-io/feat/deparser-merge-seclabel
fix(deparser): MergeWhenClause support and SecLabelStmt qualified object names
2 parents 2d63bfc + df814d2 commit a0033bd

4 files changed

Lines changed: 120 additions & 1 deletion

File tree

‎__fixtures__/generated/generated.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21294,6 +21294,14 @@
2129421294
"misc/missing-types-12.sql": "ALTER SUBSCRIPTION my_subscription OWNER TO new_owner",
2129521295
"misc/missing-types-13.sql": "ALTER STATISTICS my_statistics OWNER TO new_owner",
2129621296
"misc/missing-types-14.sql": "ALTER TABLESPACE my_tablespace OWNER TO new_owner",
21297+
"misc/merge-seclabel-1.sql": "MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name)",
21298+
"misc/merge-seclabel-2.sql": "MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED AND s.deleted THEN DELETE WHEN MATCHED THEN UPDATE SET name = s.name, updated_at = now() WHEN NOT MATCHED THEN DO NOTHING",
21299+
"misc/merge-seclabel-3.sql": "MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN NOT MATCHED BY SOURCE THEN DELETE WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)",
21300+
"misc/merge-seclabel-4.sql": "MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN NOT MATCHED THEN INSERT DEFAULT VALUES",
21301+
"misc/merge-seclabel-5.sql": "MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name RETURNING u.id, u.name",
21302+
"misc/merge-seclabel-6.sql": "SECURITY LABEL FOR anon ON COLUMN myschema.users.name IS 'MASKED WITH FUNCTION anon.fake_name()'",
21303+
"misc/merge-seclabel-7.sql": "SECURITY LABEL FOR anon ON TABLE myschema.users IS 'sensitive'",
21304+
"misc/merge-seclabel-8.sql": "SECURITY LABEL ON FUNCTION myschema.fn() IS 'labeled'",
2129721305
"misc/launchql-ext-types-1.sql": "CREATE DOMAIN attachment AS jsonb CHECK ( value ?& ARRAY['url', 'mime'] AND (value->>'url') ~ '^(https?)://[^\\s/$.?#].[^\\s]*$' )",
2129821306
"misc/launchql-ext-types-2.sql": "COMMENT ON DOMAIN attachment IS E'@name launchqlInternalTypeAttachment'",
2129921307
"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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- MergeWhenClause deparse support (previously: "Deparser does not handle node type: MergeWhenClause")
2+
MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
3+
MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED AND s.deleted THEN DELETE WHEN MATCHED THEN UPDATE SET name = s.name, updated_at = now() WHEN NOT MATCHED THEN DO NOTHING;
4+
MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN NOT MATCHED BY SOURCE THEN DELETE WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id);
5+
MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN NOT MATCHED THEN INSERT DEFAULT VALUES;
6+
MERGE INTO tgt.users u USING src.staged s ON u.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name RETURNING u.id, u.name;
7+
8+
-- SecLabelStmt qualified object names must be dot-joined (previously emitted "schema", table, column)
9+
SECURITY LABEL FOR anon ON COLUMN myschema.users.name IS 'MASKED WITH FUNCTION anon.fake_name()';
10+
SECURITY LABEL FOR anon ON TABLE myschema.users IS 'sensitive';
11+
SECURITY LABEL ON FUNCTION myschema.fn() IS 'labeled';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
import { FixtureTestUtils } from '../../test-utils';
3+
const fixtures = new FixtureTestUtils();
4+
5+
it('misc-merge-seclabel', async () => {
6+
await fixtures.runFixtureTests([
7+
"misc/merge-seclabel-1.sql",
8+
"misc/merge-seclabel-2.sql",
9+
"misc/merge-seclabel-3.sql",
10+
"misc/merge-seclabel-4.sql",
11+
"misc/merge-seclabel-5.sql",
12+
"misc/merge-seclabel-6.sql",
13+
"misc/merge-seclabel-7.sql",
14+
"misc/merge-seclabel-8.sql"
15+
]);
16+
});

‎packages/deparser/src/deparser.ts‎

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8720,7 +8720,16 @@ export class Deparser implements DeparserVisitor {
87208720
}
87218721

87228722
if (node.object) {
8723-
output.push(this.visit(node.object, context));
8723+
// Qualified names arrive as a List of String nodes and must be
8724+
// dot-joined (visiting the List directly would comma-join them).
8725+
if (typeof node.object === 'object' && 'List' in node.object) {
8726+
const list = node.object.List as t.List;
8727+
const objectParts = ListUtils.unwrapList(list.items)
8728+
.map(item => this.visit(item, context));
8729+
output.push(objectParts.join('.'));
8730+
} else {
8731+
output.push(this.visit(node.object, context));
8732+
}
87248733
}
87258734

87268735
output.push('IS');
@@ -9318,6 +9327,81 @@ export class Deparser implements DeparserVisitor {
93189327
output.push(whenClauses);
93199328
}
93209329

9330+
if (node.returningList && node.returningList.length > 0) {
9331+
output.push('RETURNING');
9332+
const returningList = ListUtils.unwrapList(node.returningList)
9333+
.map(target => this.visit(target, context))
9334+
.join(', ');
9335+
output.push(returningList);
9336+
}
9337+
9338+
return output.join(' ');
9339+
}
9340+
9341+
MergeWhenClause(node: t.MergeWhenClause, context: DeparserContext): string {
9342+
const output: string[] = ['WHEN'];
9343+
9344+
switch (node.matchKind) {
9345+
case 'MERGE_WHEN_MATCHED':
9346+
output.push('MATCHED');
9347+
break;
9348+
case 'MERGE_WHEN_NOT_MATCHED_BY_SOURCE':
9349+
output.push('NOT MATCHED BY SOURCE');
9350+
break;
9351+
case 'MERGE_WHEN_NOT_MATCHED_BY_TARGET':
9352+
output.push('NOT MATCHED');
9353+
break;
9354+
}
9355+
9356+
if (node.condition) {
9357+
output.push('AND');
9358+
output.push(this.visit(node.condition, context));
9359+
}
9360+
9361+
output.push('THEN');
9362+
9363+
switch (node.commandType) {
9364+
case 'CMD_UPDATE': {
9365+
output.push('UPDATE SET');
9366+
const assignments = ListUtils.unwrapList(node.targetList)
9367+
.map(target => this.visit(target, context.spawn('UpdateStmt', { update: true })))
9368+
.join(', ');
9369+
output.push(assignments);
9370+
break;
9371+
}
9372+
case 'CMD_INSERT': {
9373+
output.push('INSERT');
9374+
const targetList = ListUtils.unwrapList(node.targetList);
9375+
if (targetList.length > 0) {
9376+
const columns = targetList
9377+
.map(target => QuoteUtils.quoteIdentifier(target.ResTarget?.name))
9378+
.join(', ');
9379+
output.push(`(${columns})`);
9380+
}
9381+
if (node.override === 'OVERRIDING_USER_VALUE') {
9382+
output.push('OVERRIDING USER VALUE');
9383+
} else if (node.override === 'OVERRIDING_SYSTEM_VALUE') {
9384+
output.push('OVERRIDING SYSTEM VALUE');
9385+
}
9386+
const values = ListUtils.unwrapList(node.values);
9387+
if (values.length > 0) {
9388+
const valueItems = values
9389+
.map(value => this.visit(value, context))
9390+
.join(', ');
9391+
output.push(`VALUES (${valueItems})`);
9392+
} else {
9393+
output.push('DEFAULT VALUES');
9394+
}
9395+
break;
9396+
}
9397+
case 'CMD_DELETE':
9398+
output.push('DELETE');
9399+
break;
9400+
case 'CMD_NOTHING':
9401+
output.push('DO NOTHING');
9402+
break;
9403+
}
9404+
93219405
return output.join(' ');
93229406
}
93239407

0 commit comments

Comments
 (0)