@intentius/chant-lexicon-sql 0.115.0 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +35 -0
- package/dist/okf/index.md +7 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +10 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/access/acl.d.ts +119 -0
- package/dist/postgres/access/acl.d.ts.map +1 -0
- package/dist/postgres/access/import.d.ts +13 -0
- package/dist/postgres/access/import.d.ts.map +1 -0
- package/dist/postgres/access/live.d.ts +24 -0
- package/dist/postgres/access/live.d.ts.map +1 -0
- package/dist/postgres/apply/apply.d.ts +6 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +12 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +212 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +7 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +4 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts +6 -0
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts +2 -0
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +25 -4
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +15 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/op.d.ts +29 -0
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +155 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/commands.d.ts +12 -0
- package/dist/postgres/plan/commands.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +11 -2
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +28 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +14 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +12 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts +4 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +10 -4
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +7 -0
- package/src/config.ts +10 -0
- package/src/generated/lexicon-sql.json +35 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +10 -4
- package/src/lsp/postgres-template.ts +8 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.ts +7 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +6 -0
- package/src/postgres/access/access.e2e.test.ts +191 -0
- package/src/postgres/access/access.test.ts +147 -0
- package/src/postgres/access/acl.ts +400 -0
- package/src/postgres/access/import.ts +64 -0
- package/src/postgres/access/live.ts +123 -0
- package/src/postgres/apply/apply.ts +135 -6
- package/src/postgres/apply/statements.ts +104 -16
- package/src/postgres/entities.ts +613 -14
- package/src/postgres/entity-types.ts +7 -0
- package/src/postgres/import/generator.ts +18 -5
- package/src/postgres/import/live-export.ts +16 -2
- package/src/postgres/live/bind.ts +3 -0
- package/src/postgres/live/catalog.ts +155 -5
- package/src/postgres/live/describe-resources.ts +43 -9
- package/src/postgres/live/routines.e2e.test.ts +173 -0
- package/src/postgres/migrate/batches.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.test.ts +24 -1
- package/src/postgres/migrate/op.ts +80 -35
- package/src/postgres/migrate/outer.e2e.test.ts +163 -0
- package/src/postgres/parser.ts +554 -9
- package/src/postgres/plan/commands.ts +61 -8
- package/src/postgres/plan/deep.ts +79 -18
- package/src/postgres/plan/diff.ts +114 -9
- package/src/postgres/plan/normalize.ts +272 -5
- package/src/postgres/plan/rules.ts +80 -0
- package/src/postgres/plan/schema.ts +27 -6
- package/src/postgres/plan/server-normalize.ts +53 -1
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +22 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres.md +25 -2
- package/src/validate.ts +7 -0
package/src/postgres/parser.ts
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* A hand-written, lossless parser for the Postgres statements the dialect
|
|
3
3
|
* declares (chant #3278, #3279): `CREATE SCHEMA`, `TABLE`, `INDEX`, `VIEW`,
|
|
4
4
|
* `MATERIALIZED VIEW`, `SEQUENCE`, `TYPE ... AS ENUM`, `DOMAIN`, `EXTENSION`,
|
|
5
|
-
* and `
|
|
5
|
+
* `FUNCTION`, `PROCEDURE` and `TRIGGER` (#3680), `POLICY` and `ROLE`, `GRANT`,
|
|
6
|
+
* `REVOKE` and `ALTER DEFAULT PRIVILEGES` on schemas, tables, columns,
|
|
7
|
+
* sequences and routines, `ALTER TABLE ... ROW LEVEL SECURITY` (#3681), and
|
|
8
|
+
* `COMMENT ON` for those objects.
|
|
9
|
+
*
|
|
10
|
+
* A function's or procedure's body is a string (`AS $$ ... $$`), kept
|
|
11
|
+
* verbatim: Postgres stores it as written and prints it back unchanged. A
|
|
12
|
+
* SQL-standard body (`RETURN expr`, `BEGIN ATOMIC ... END`) is refused, since
|
|
13
|
+
* the server stores it parsed and prints it back rewritten.
|
|
6
14
|
*
|
|
7
15
|
* As for ClickHouse (#3196), statement structure is parsed and expressions are
|
|
8
16
|
* not: a default, a check, a generated expression, an index element, a
|
|
@@ -22,7 +30,8 @@
|
|
|
22
30
|
* Grammar source: the Postgres 18 reference pages for each statement
|
|
23
31
|
* (sql-createtable, sql-createindex, sql-createview,
|
|
24
32
|
* sql-creatematerializedview, sql-createsequence, sql-createtype,
|
|
25
|
-
* sql-createdomain, sql-createextension, sql-createschema, sql-
|
|
33
|
+
* sql-createdomain, sql-createextension, sql-createschema, sql-createfunction,
|
|
34
|
+
* sql-createprocedure, sql-createtrigger, sql-comment) and
|
|
26
35
|
* `src/backend/parser/gram.y` at `REL_18_6` where a page is silent.
|
|
27
36
|
*/
|
|
28
37
|
|
|
@@ -205,13 +214,115 @@ export interface ExtensionNode {
|
|
|
205
214
|
cascade: boolean;
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
/** One parameter of a function or procedure: `[mode] [name] type [DEFAULT expr]`. */
|
|
218
|
+
export interface RoutineArgNode {
|
|
219
|
+
mode?: "IN" | "OUT" | "INOUT" | "VARIADIC";
|
|
220
|
+
name?: string;
|
|
221
|
+
nameSpan?: Span;
|
|
222
|
+
type: Span;
|
|
223
|
+
default?: Span;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface RoutineNode {
|
|
227
|
+
statement: "function" | "procedure";
|
|
228
|
+
orReplace: boolean;
|
|
229
|
+
name: NameNode;
|
|
230
|
+
args: RoutineArgNode[];
|
|
231
|
+
/** `RETURNS [SETOF] type`: the type, `SETOF` excluded. */
|
|
232
|
+
returns?: { setof: boolean; type: Span };
|
|
233
|
+
/** `RETURNS TABLE (name type, ...)`. */
|
|
234
|
+
returnsTable?: Array<{ name: string; nameSpan: Span; type: Span }>;
|
|
235
|
+
language?: Span;
|
|
236
|
+
/** The attributes after the signature, by their key word: `VOLATILITY` (IMMUTABLE, STABLE, VOLATILE), `STRICT`, `SECURITY`, `LEAKPROOF`, `PARALLEL`, `COST`, `ROWS`, `SUPPORT`, `WINDOW`, `TRANSFORM`. */
|
|
237
|
+
options: Array<{ option: string; value?: Span; flag?: boolean }>;
|
|
238
|
+
/** `SET name { TO | = } value`: the parameter, folded, and its value as written. */
|
|
239
|
+
set: Array<{ name: string; value: Span }>;
|
|
240
|
+
/** `AS 'definition'`, or `AS 'obj_file', 'link_symbol'`: the string constants. */
|
|
241
|
+
body: Span[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface TriggerNode {
|
|
245
|
+
statement: "trigger";
|
|
246
|
+
orReplace: boolean;
|
|
247
|
+
constraint: boolean;
|
|
248
|
+
name: NameNode;
|
|
249
|
+
timing: "BEFORE" | "AFTER" | "INSTEAD OF";
|
|
250
|
+
events: Array<{ event: "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE"; columns: Array<{ name: string; span: Span }> }>;
|
|
251
|
+
table: NameNode;
|
|
252
|
+
/** A constraint trigger's `FROM referenced_table`. */
|
|
253
|
+
from?: NameNode;
|
|
254
|
+
deferrable?: boolean;
|
|
255
|
+
initiallyDeferred?: boolean;
|
|
256
|
+
/** `REFERENCING OLD TABLE AS o NEW TABLE AS n`. */
|
|
257
|
+
referencing: Array<{ which: "OLD" | "NEW"; name: string }>;
|
|
258
|
+
forEach?: "ROW" | "STATEMENT";
|
|
259
|
+
/** The `WHEN (...)` condition, parentheses excluded. */
|
|
260
|
+
when?: Span;
|
|
261
|
+
function: NameNode;
|
|
262
|
+
args: Span[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface PolicyNode {
|
|
266
|
+
statement: "policy";
|
|
267
|
+
name: NameNode;
|
|
268
|
+
table: NameNode;
|
|
269
|
+
/** `AS PERMISSIVE` (true, the default) or `AS RESTRICTIVE` (false). */
|
|
270
|
+
permissive?: boolean;
|
|
271
|
+
command?: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
272
|
+
/** `TO role, ...`: role names, `public` for PUBLIC. */
|
|
273
|
+
roles: Array<{ name: string; span: Span }>;
|
|
274
|
+
/** `USING (...)`, parentheses excluded. */
|
|
275
|
+
using?: Span;
|
|
276
|
+
/** `WITH CHECK (...)`, parentheses excluded. */
|
|
277
|
+
check?: Span;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface RoleNode {
|
|
281
|
+
statement: "role";
|
|
282
|
+
name: NameNode;
|
|
283
|
+
/** The options, each by its key word (`LOGIN`, `NOINHERIT`, `CONNECTION LIMIT`, `VALID UNTIL`), with a value where it takes one. */
|
|
284
|
+
options: Array<{ option: string; value?: Span }>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** What a GRANT or REVOKE is on: an object kind, or (for default privileges) a kind of object created later. */
|
|
288
|
+
export type GrantTarget = "TABLE" | "SEQUENCE" | "SCHEMA" | "FUNCTION" | "PROCEDURE" | "ROUTINE" | "TABLES" | "SEQUENCES" | "FUNCTIONS" | "ROUTINES" | "TYPES" | "SCHEMAS";
|
|
289
|
+
|
|
290
|
+
export interface GrantNode {
|
|
291
|
+
statement: "grant";
|
|
292
|
+
revoke: boolean;
|
|
293
|
+
/** `REVOKE GRANT OPTION FOR ...`. */
|
|
294
|
+
grantOptionFor: boolean;
|
|
295
|
+
/** Each privilege, upper case (`ALL` for ALL [PRIVILEGES]), with the columns it is limited to. */
|
|
296
|
+
privileges: Array<{ privilege: string; columns: Array<{ name: string; span: Span }> }>;
|
|
297
|
+
on: GrantTarget;
|
|
298
|
+
/** The objects, each with a routine's parameter list as written (parentheses excluded). Empty for default privileges. */
|
|
299
|
+
objects: Array<{ name: NameNode; args?: Span }>;
|
|
300
|
+
/** Role names, `public` for PUBLIC. */
|
|
301
|
+
grantees: Array<{ name: string; span: Span }>;
|
|
302
|
+
withGrantOption: boolean;
|
|
303
|
+
/** `ALTER DEFAULT PRIVILEGES FOR ROLE ...`. */
|
|
304
|
+
forRoles?: Array<{ name: string; span: Span }>;
|
|
305
|
+
/** `ALTER DEFAULT PRIVILEGES IN SCHEMA ...`. */
|
|
306
|
+
inSchemas?: Array<{ name: string; span: Span }>;
|
|
307
|
+
/** True for `ALTER DEFAULT PRIVILEGES`. */
|
|
308
|
+
defaults: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface RowSecurityNode {
|
|
312
|
+
statement: "rowSecurity";
|
|
313
|
+
table: NameNode;
|
|
314
|
+
action: "ENABLE" | "DISABLE" | "FORCE" | "NO FORCE";
|
|
315
|
+
}
|
|
316
|
+
|
|
208
317
|
export interface CommentNode {
|
|
209
318
|
statement: "comment";
|
|
210
|
-
/** TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, CONSTRAINT, DOMAIN CONSTRAINT. */
|
|
319
|
+
/** TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, CONSTRAINT, DOMAIN CONSTRAINT. */
|
|
211
320
|
objectType: string;
|
|
212
321
|
target: NameNode;
|
|
213
|
-
/** For `COMMENT ON CONSTRAINT c ON [DOMAIN] t`: the table or domain. */
|
|
322
|
+
/** For `COMMENT ON CONSTRAINT c ON [DOMAIN] t` and `COMMENT ON TRIGGER t ON table`: the table or domain. */
|
|
214
323
|
on?: NameNode;
|
|
324
|
+
/** For `COMMENT ON FUNCTION f(...)`: the parameter list, parentheses excluded. */
|
|
325
|
+
args?: Span;
|
|
215
326
|
/** The comment, a string, or undefined for `IS NULL`. */
|
|
216
327
|
text?: Span;
|
|
217
328
|
}
|
|
@@ -225,6 +336,12 @@ export type StatementNode =
|
|
|
225
336
|
| EnumNode
|
|
226
337
|
| DomainNode
|
|
227
338
|
| ExtensionNode
|
|
339
|
+
| RoutineNode
|
|
340
|
+
| TriggerNode
|
|
341
|
+
| PolicyNode
|
|
342
|
+
| RoleNode
|
|
343
|
+
| GrantNode
|
|
344
|
+
| RowSecurityNode
|
|
228
345
|
| CommentNode;
|
|
229
346
|
|
|
230
347
|
const isNameToken = (t: Token | undefined): boolean =>
|
|
@@ -746,8 +863,19 @@ class PgParser extends SqlCursor {
|
|
|
746
863
|
|
|
747
864
|
parse(): StatementNode {
|
|
748
865
|
if (this.accept("COMMENT")) return this.comment();
|
|
866
|
+
if (this.accept("GRANT")) return this.grant(false, false);
|
|
867
|
+
if (this.accept("REVOKE")) return this.grant(true, false);
|
|
868
|
+
if (this.acceptSeq("ALTER", "DEFAULT", "PRIVILEGES")) return this.defaultPrivileges();
|
|
869
|
+
if (this.acceptSeq("ALTER", "TABLE")) return this.rowSecurity();
|
|
749
870
|
this.expect("CREATE");
|
|
750
871
|
const orReplace = this.acceptSeq("OR", "REPLACE");
|
|
872
|
+
if (!orReplace && this.accept("POLICY")) return this.policy();
|
|
873
|
+
if (!orReplace && this.accept("ROLE")) return this.role();
|
|
874
|
+
if (!orReplace && kw(this.peek(), "USER", "GROUP")) this.fail("expected ROLE (declare a role with CREATE ROLE; LOGIN is an option of it)");
|
|
875
|
+
if (this.accept("FUNCTION")) return this.routine("function", orReplace);
|
|
876
|
+
if (this.accept("PROCEDURE")) return this.routine("procedure", orReplace);
|
|
877
|
+
if (this.accept("TRIGGER")) return this.trigger(orReplace, false);
|
|
878
|
+
if (this.acceptSeq("CONSTRAINT", "TRIGGER")) return this.trigger(orReplace, true);
|
|
751
879
|
if (!orReplace) {
|
|
752
880
|
if (this.accept("SCHEMA")) return this.schema();
|
|
753
881
|
if (this.accept("EXTENSION")) return this.extension();
|
|
@@ -762,7 +890,7 @@ class PgParser extends SqlCursor {
|
|
|
762
890
|
const recursive = this.accept("RECURSIVE");
|
|
763
891
|
if (!orReplace && !recursive && persistence === undefined && this.acceptSeq("MATERIALIZED", "VIEW")) return this.view(true, false, false, false);
|
|
764
892
|
if (this.accept("VIEW")) return this.view(false, orReplace, persistence === "temporary", recursive);
|
|
765
|
-
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN or
|
|
893
|
+
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, POLICY or ROLE");
|
|
766
894
|
}
|
|
767
895
|
|
|
768
896
|
private schema(): SchemaNode {
|
|
@@ -1007,26 +1135,431 @@ class PgParser extends SqlCursor {
|
|
|
1007
1135
|
}
|
|
1008
1136
|
}
|
|
1009
1137
|
|
|
1138
|
+
/** Whether `a b` continues one type name (`double precision`, `character varying`, `timestamp with time zone`) rather than being a name and a type. */
|
|
1139
|
+
private continuesType(a: Token | undefined, b: Token | undefined): boolean {
|
|
1140
|
+
if (a?.kind !== "ident" || b?.kind !== "ident") return false;
|
|
1141
|
+
const x = a.text.toUpperCase();
|
|
1142
|
+
const y = b.text.toUpperCase();
|
|
1143
|
+
if (x === "DOUBLE") return y === "PRECISION";
|
|
1144
|
+
if (["CHARACTER", "CHAR", "NCHAR", "BIT", "VARCHAR"].includes(x)) return y === "VARYING";
|
|
1145
|
+
if (x === "NATIONAL") return y === "CHARACTER" || y === "CHAR";
|
|
1146
|
+
if (x === "TIME" || x === "TIMESTAMP") return y === "WITH" || y === "WITHOUT";
|
|
1147
|
+
if (x === "INTERVAL") return ["YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND"].includes(y);
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** `[IN | OUT | INOUT | VARIADIC] [name] type [{DEFAULT | =} expr]`. */
|
|
1152
|
+
private routineArg(): RoutineArgNode {
|
|
1153
|
+
const arg: RoutineArgNode = { type: { from: 0, to: 0, refs: [] } };
|
|
1154
|
+
// A mode word is a mode unless it is the whole parameter (a type of that name) or the parameter's type follows nothing else.
|
|
1155
|
+
if (kw(this.peek(), "IN", "OUT", "INOUT", "VARIADIC") && !(this.isPunct(",", 1) || this.isPunct(")", 1))) {
|
|
1156
|
+
arg.mode = this.next().text.toUpperCase() as RoutineArgNode["mode"];
|
|
1157
|
+
}
|
|
1158
|
+
const a = this.peek();
|
|
1159
|
+
const b = this.peek(1);
|
|
1160
|
+
const named =
|
|
1161
|
+
a !== undefined &&
|
|
1162
|
+
(a.kind === "ident" || a.kind === "qident") &&
|
|
1163
|
+
b !== undefined &&
|
|
1164
|
+
(b.kind === "ident" || b.kind === "qident" || b.kind === "ref") &&
|
|
1165
|
+
!(b.kind === "ident" && keywordCategory(b.text) === "R") &&
|
|
1166
|
+
!kw(b, "DEFAULT") &&
|
|
1167
|
+
!this.continuesType(a, b);
|
|
1168
|
+
if (named) {
|
|
1169
|
+
const { text, span } = this.nameTok("a parameter name");
|
|
1170
|
+
arg.name = text;
|
|
1171
|
+
arg.nameSpan = span;
|
|
1172
|
+
}
|
|
1173
|
+
if (kw(this.peek(), "SETOF")) this.fail("expected a parameter type (SETOF is a return type)");
|
|
1174
|
+
arg.type = this.typeName();
|
|
1175
|
+
if (this.isOp("%")) this.fail("expected the end of the parameter (write the column's type: %TYPE is resolved once, when the function is created)");
|
|
1176
|
+
if (this.accept("DEFAULT") || (this.isOp("=") && (this.p++, true))) arg.default = this.checkedExpr([], true);
|
|
1177
|
+
return arg;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private routine(kind: "function" | "procedure", orReplace: boolean): RoutineNode {
|
|
1181
|
+
const node: RoutineNode = { statement: kind, orReplace, name: this.qualifiedName(`a ${kind} name`), args: [], options: [], set: [], body: [] };
|
|
1182
|
+
this.expectPunct("(");
|
|
1183
|
+
if (!this.acceptPunct(")")) {
|
|
1184
|
+
for (;;) {
|
|
1185
|
+
node.args.push(this.routineArg());
|
|
1186
|
+
if (this.acceptPunct(",")) continue;
|
|
1187
|
+
this.expectPunct(")");
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (kind === "function" && this.accept("RETURNS")) {
|
|
1192
|
+
if (this.accept("TABLE")) {
|
|
1193
|
+
this.expectPunct("(");
|
|
1194
|
+
node.returnsTable = [];
|
|
1195
|
+
for (;;) {
|
|
1196
|
+
const { text, span } = this.nameTok("a column name");
|
|
1197
|
+
node.returnsTable.push({ name: text, nameSpan: span, type: this.typeName() });
|
|
1198
|
+
if (this.acceptPunct(",")) continue;
|
|
1199
|
+
this.expectPunct(")");
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
const setof = this.accept("SETOF");
|
|
1204
|
+
node.returns = { setof, type: this.typeName() };
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const word = (what: string): Span => {
|
|
1208
|
+
const from = this.idx();
|
|
1209
|
+
const t = this.next();
|
|
1210
|
+
if (!["ident", "qident", "string", "ref", "number"].includes(t.kind)) this.fail(`expected ${what}`, t);
|
|
1211
|
+
return this.span(from, t.kind === "ref" ? [t.part] : []);
|
|
1212
|
+
};
|
|
1213
|
+
const option = (option: string, value?: Span, flag?: boolean) => node.options.push({ option, ...(value ? { value } : {}), ...(flag !== undefined ? { flag } : {}) });
|
|
1214
|
+
const forFunction = (what: string) => {
|
|
1215
|
+
if (kind === "procedure") this.fail(`expected LANGUAGE, SECURITY, SET or AS (${what} is not an attribute of a procedure)`, this.lastSeen());
|
|
1216
|
+
};
|
|
1217
|
+
for (;;) {
|
|
1218
|
+
if (this.accept("LANGUAGE")) node.language = word("a language name");
|
|
1219
|
+
else if (this.accept("TRANSFORM")) {
|
|
1220
|
+
const from = this.idx();
|
|
1221
|
+
const refs: number[] = [];
|
|
1222
|
+
do {
|
|
1223
|
+
this.expect("FOR");
|
|
1224
|
+
this.expect("TYPE");
|
|
1225
|
+
refs.push(...this.typeName().refs);
|
|
1226
|
+
} while (this.acceptPunct(","));
|
|
1227
|
+
option("TRANSFORM", this.span(from, refs));
|
|
1228
|
+
} else if (kw(this.peek(), "IMMUTABLE", "STABLE", "VOLATILE")) {
|
|
1229
|
+
forFunction(this.peek()!.text.toUpperCase());
|
|
1230
|
+
option("VOLATILITY", word("a volatility"));
|
|
1231
|
+
} else if (this.acceptSeq("NOT", "LEAKPROOF")) {
|
|
1232
|
+
forFunction("LEAKPROOF");
|
|
1233
|
+
option("LEAKPROOF", undefined, false);
|
|
1234
|
+
} else if (this.accept("LEAKPROOF")) {
|
|
1235
|
+
forFunction("LEAKPROOF");
|
|
1236
|
+
option("LEAKPROOF", undefined, true);
|
|
1237
|
+
} else if (this.acceptSeq("CALLED", "ON", "NULL", "INPUT")) {
|
|
1238
|
+
forFunction("CALLED ON NULL INPUT");
|
|
1239
|
+
option("STRICT", undefined, false);
|
|
1240
|
+
} else if (this.acceptSeq("RETURNS", "NULL", "ON", "NULL", "INPUT") || this.accept("STRICT")) {
|
|
1241
|
+
forFunction("STRICT");
|
|
1242
|
+
option("STRICT", undefined, true);
|
|
1243
|
+
} else if (this.acceptSeq("EXTERNAL", "SECURITY") || this.accept("SECURITY")) {
|
|
1244
|
+
if (this.accept("DEFINER")) option("SECURITY", undefined, true);
|
|
1245
|
+
else {
|
|
1246
|
+
this.expect("INVOKER");
|
|
1247
|
+
option("SECURITY", undefined, false);
|
|
1248
|
+
}
|
|
1249
|
+
} else if (this.accept("PARALLEL")) {
|
|
1250
|
+
forFunction("PARALLEL");
|
|
1251
|
+
if (!kw(this.peek(), "UNSAFE", "RESTRICTED", "SAFE")) this.fail("expected UNSAFE, RESTRICTED or SAFE");
|
|
1252
|
+
option("PARALLEL", word("UNSAFE, RESTRICTED or SAFE"));
|
|
1253
|
+
} else if (kw(this.peek(), "COST", "ROWS")) {
|
|
1254
|
+
const o = this.next().text.toUpperCase();
|
|
1255
|
+
forFunction(o);
|
|
1256
|
+
const from = this.idx();
|
|
1257
|
+
const t = this.next();
|
|
1258
|
+
if (t.kind !== "number" && t.kind !== "ref") this.fail("expected a number", t);
|
|
1259
|
+
option(o, this.span(from, t.kind === "ref" ? [t.part] : []));
|
|
1260
|
+
} else if (this.accept("SUPPORT")) {
|
|
1261
|
+
forFunction("SUPPORT");
|
|
1262
|
+
option("SUPPORT", this.qualifiedName("a support function").span);
|
|
1263
|
+
} else if (this.accept("WINDOW")) {
|
|
1264
|
+
forFunction("WINDOW");
|
|
1265
|
+
option("WINDOW", undefined, true);
|
|
1266
|
+
} else if (this.accept("SET")) {
|
|
1267
|
+
const param = this.qualifiedName("a configuration parameter");
|
|
1268
|
+
if (this.acceptSeq("FROM", "CURRENT")) {
|
|
1269
|
+
this.fail("expected TO or = (SET ... FROM CURRENT takes the value of the session that runs the CREATE; write the value)", this.lastSeen());
|
|
1270
|
+
}
|
|
1271
|
+
if (!this.accept("TO")) {
|
|
1272
|
+
if (!this.isOp("=")) this.fail("expected TO or =");
|
|
1273
|
+
this.p++;
|
|
1274
|
+
}
|
|
1275
|
+
const from = this.idx();
|
|
1276
|
+
const refs: number[] = [];
|
|
1277
|
+
for (;;) {
|
|
1278
|
+
if (this.isOp("-") || this.isOp("+")) this.p++;
|
|
1279
|
+
const t = this.next();
|
|
1280
|
+
if (t.kind === "ref") refs.push(t.part);
|
|
1281
|
+
else if (!["ident", "qident", "string", "number"].includes(t.kind)) this.fail("expected a value", t);
|
|
1282
|
+
if (this.acceptPunct(",")) continue;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
node.set.push({ name: param.pieces.join("."), value: this.span(from, refs) });
|
|
1286
|
+
} else if (this.accept("AS")) {
|
|
1287
|
+
if (node.body.length > 0) this.fail("expected one AS clause", this.lastSeen());
|
|
1288
|
+
for (;;) {
|
|
1289
|
+
const from = this.idx();
|
|
1290
|
+
const t = this.next();
|
|
1291
|
+
if (t.kind !== "string") this.fail("expected the definition as a string constant (AS $$ ... $$)", t);
|
|
1292
|
+
node.body.push(this.span(from, []));
|
|
1293
|
+
if (this.acceptPunct(",")) continue;
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
} else if (kw(this.peek(), "RETURN") || this.is("BEGIN", "ATOMIC")) {
|
|
1297
|
+
this.fail("expected AS (a SQL-standard body is stored parsed and printed back rewritten, so it cannot be compared with what was written; write the body as a string: AS $$ ... $$)");
|
|
1298
|
+
} else break;
|
|
1299
|
+
}
|
|
1300
|
+
if (node.body.length === 0) this.fail("expected AS and the body, as a string constant (AS $$ ... $$)");
|
|
1301
|
+
return node;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/** The last significant token read. */
|
|
1305
|
+
private lastSeen(): Token | undefined {
|
|
1306
|
+
return this.tokens[this.idx(-1)];
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private trigger(orReplace: boolean, constraint: boolean): TriggerNode {
|
|
1310
|
+
const { text, span } = this.nameTok("a trigger name");
|
|
1311
|
+
if (this.isPunct(".")) this.fail("expected BEFORE, AFTER or INSTEAD OF (a trigger name is not schema-qualified; the trigger is its table's)");
|
|
1312
|
+
const name: NameNode = { pieces: [text], span };
|
|
1313
|
+
let timing: TriggerNode["timing"];
|
|
1314
|
+
if (this.accept("BEFORE")) timing = "BEFORE";
|
|
1315
|
+
else if (this.accept("AFTER")) timing = "AFTER";
|
|
1316
|
+
else if (this.acceptSeq("INSTEAD", "OF")) timing = "INSTEAD OF";
|
|
1317
|
+
else return this.fail("expected BEFORE, AFTER or INSTEAD OF");
|
|
1318
|
+
const events: TriggerNode["events"] = [];
|
|
1319
|
+
do {
|
|
1320
|
+
if (this.accept("INSERT", "DELETE", "TRUNCATE")) events.push({ event: this.lastSeen()!.text.toUpperCase() as "INSERT", columns: [] });
|
|
1321
|
+
else if (this.accept("UPDATE")) {
|
|
1322
|
+
const columns: Array<{ name: string; span: Span }> = [];
|
|
1323
|
+
if (this.accept("OF")) {
|
|
1324
|
+
do columns.push((({ text, span }) => ({ name: text, span }))(this.nameTok("a column name")));
|
|
1325
|
+
while (this.acceptPunct(","));
|
|
1326
|
+
}
|
|
1327
|
+
events.push({ event: "UPDATE", columns });
|
|
1328
|
+
} else this.fail("expected INSERT, UPDATE, DELETE or TRUNCATE");
|
|
1329
|
+
} while (this.accept("OR"));
|
|
1330
|
+
this.expect("ON");
|
|
1331
|
+
const node: TriggerNode = { statement: "trigger", orReplace, constraint, name, timing, events, table: this.qualifiedName("a table name"), referencing: [], function: { pieces: [], span: { from: 0, to: 0, refs: [] } }, args: [] };
|
|
1332
|
+
for (;;) {
|
|
1333
|
+
if (this.accept("FROM")) node.from = this.qualifiedName("a referenced table");
|
|
1334
|
+
else if (this.acceptSeq("NOT", "DEFERRABLE")) node.deferrable = false;
|
|
1335
|
+
else if (this.accept("DEFERRABLE")) node.deferrable = true;
|
|
1336
|
+
else if (this.acceptSeq("INITIALLY", "DEFERRED")) node.initiallyDeferred = true;
|
|
1337
|
+
else if (this.acceptSeq("INITIALLY", "IMMEDIATE")) node.initiallyDeferred = false;
|
|
1338
|
+
else if (this.accept("REFERENCING")) {
|
|
1339
|
+
do {
|
|
1340
|
+
if (!this.accept("OLD", "NEW")) this.fail("expected OLD or NEW");
|
|
1341
|
+
const which = this.lastSeen()!.text.toUpperCase() as "OLD" | "NEW";
|
|
1342
|
+
this.expect("TABLE");
|
|
1343
|
+
this.accept("AS");
|
|
1344
|
+
node.referencing.push({ which, name: this.nameTok("a transition table name").text });
|
|
1345
|
+
} while (kw(this.peek(), "OLD", "NEW"));
|
|
1346
|
+
} else if (this.accept("FOR")) {
|
|
1347
|
+
this.accept("EACH");
|
|
1348
|
+
if (!this.accept("ROW", "STATEMENT")) this.fail("expected ROW or STATEMENT");
|
|
1349
|
+
node.forEach = this.lastSeen()!.text.toUpperCase() as "ROW" | "STATEMENT";
|
|
1350
|
+
} else if (this.accept("WHEN")) {
|
|
1351
|
+
if (!this.isPunct("(")) this.fail("expected '(' (a WHEN condition is parenthesized)");
|
|
1352
|
+
node.when = this.parenthesized();
|
|
1353
|
+
} else break;
|
|
1354
|
+
}
|
|
1355
|
+
this.expect("EXECUTE");
|
|
1356
|
+
this.expect("FUNCTION", "PROCEDURE");
|
|
1357
|
+
node.function = this.qualifiedName("a trigger function");
|
|
1358
|
+
this.expectPunct("(");
|
|
1359
|
+
if (!this.acceptPunct(")")) {
|
|
1360
|
+
for (;;) {
|
|
1361
|
+
const from = this.idx();
|
|
1362
|
+
if (this.isOp("-")) this.p++;
|
|
1363
|
+
const t = this.next();
|
|
1364
|
+
if (!["string", "number", "ident", "qident", "ref"].includes(t.kind)) this.fail("expected a trigger argument (a string, a number or a name)", t);
|
|
1365
|
+
node.args.push(this.span(from, t.kind === "ref" ? [t.part] : []));
|
|
1366
|
+
if (this.acceptPunct(",")) continue;
|
|
1367
|
+
this.expectPunct(")");
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return node;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** A role name where one is granted to or named: an identifier, an interpolation, or PUBLIC (`public`). */
|
|
1375
|
+
private roleSpec(what = "a role name"): { name: string; span: Span } {
|
|
1376
|
+
const t = this.peek();
|
|
1377
|
+
if (kw(t, "CURRENT_USER", "CURRENT_ROLE", "SESSION_USER")) this.fail(`expected ${what} (name the role: ${t!.text.toUpperCase()} is whichever role runs the statement)`);
|
|
1378
|
+
if (kw(t, "PUBLIC")) {
|
|
1379
|
+
const i = this.idx();
|
|
1380
|
+
this.p++;
|
|
1381
|
+
return { name: "public", span: { from: i, to: i + 1, refs: [] } };
|
|
1382
|
+
}
|
|
1383
|
+
const n = this.next();
|
|
1384
|
+
if (!isNameToken(n)) this.fail(`expected ${what}`, n);
|
|
1385
|
+
const i = this.idx(-1);
|
|
1386
|
+
return { name: n.kind === "ref" ? "" : identValue(n), span: { from: i, to: i + 1, refs: n.kind === "ref" ? [n.part] : [] } };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private roleList(what?: string): Array<{ name: string; span: Span }> {
|
|
1390
|
+
const out = [this.roleSpec(what)];
|
|
1391
|
+
while (this.acceptPunct(",")) out.push(this.roleSpec(what));
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private policy(): PolicyNode {
|
|
1396
|
+
const { text, span } = this.nameTok("a policy name");
|
|
1397
|
+
this.expect("ON");
|
|
1398
|
+
const node: PolicyNode = { statement: "policy", name: { pieces: [text], span }, table: this.qualifiedName("a table name"), roles: [] };
|
|
1399
|
+
if (this.accept("AS")) {
|
|
1400
|
+
if (this.accept("PERMISSIVE")) node.permissive = true;
|
|
1401
|
+
else {
|
|
1402
|
+
this.expect("RESTRICTIVE");
|
|
1403
|
+
node.permissive = false;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (this.accept("FOR")) {
|
|
1407
|
+
if (!this.accept("ALL", "SELECT", "INSERT", "UPDATE", "DELETE")) this.fail("expected ALL, SELECT, INSERT, UPDATE or DELETE");
|
|
1408
|
+
node.command = this.lastSeen()!.text.toUpperCase() as PolicyNode["command"];
|
|
1409
|
+
}
|
|
1410
|
+
if (this.accept("TO")) node.roles = this.roleList();
|
|
1411
|
+
if (this.accept("USING")) {
|
|
1412
|
+
if (!this.isPunct("(")) this.fail("expected '('");
|
|
1413
|
+
node.using = this.parenthesized();
|
|
1414
|
+
}
|
|
1415
|
+
if (this.acceptSeq("WITH", "CHECK")) {
|
|
1416
|
+
if (!this.isPunct("(")) this.fail("expected '('");
|
|
1417
|
+
node.check = this.parenthesized();
|
|
1418
|
+
}
|
|
1419
|
+
return node;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private role(): RoleNode {
|
|
1423
|
+
const { text, span } = this.nameTok("a role name");
|
|
1424
|
+
const node: RoleNode = { statement: "role", name: { pieces: [text], span }, options: [] };
|
|
1425
|
+
this.accept("WITH");
|
|
1426
|
+
const FLAGS = ["SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS"];
|
|
1427
|
+
for (;;) {
|
|
1428
|
+
if (this.atEnd()) return node;
|
|
1429
|
+
if (kw(this.peek(), ...FLAGS)) node.options.push({ option: this.next().text.toUpperCase() });
|
|
1430
|
+
else if (this.acceptSeq("CONNECTION", "LIMIT")) {
|
|
1431
|
+
const from = this.idx();
|
|
1432
|
+
if (this.isOp("-")) this.p++;
|
|
1433
|
+
const t = this.next();
|
|
1434
|
+
if (t.kind !== "number" && t.kind !== "ref") this.fail("expected a number", t);
|
|
1435
|
+
node.options.push({ option: "CONNECTION LIMIT", value: this.span(from, t.kind === "ref" ? [t.part] : []) });
|
|
1436
|
+
} else if (kw(this.peek(), "PASSWORD", "ENCRYPTED", "UNENCRYPTED") || this.is("VALID", "UNTIL")) {
|
|
1437
|
+
this.fail("expected a role option (a password is the environment's secret, never a declaration's: set it where the environment's credentials are kept)");
|
|
1438
|
+
} else if (kw(this.peek(), "IN", "ROLE", "ADMIN", "USER", "SYSID")) {
|
|
1439
|
+
this.fail("expected a role option (who is a member of which role is the environment's: grant memberships where the roles are provisioned)");
|
|
1440
|
+
} else return this.fail("expected a role option");
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/** `GRANT|REVOKE privileges ON target TO|FROM roles`, after GRANT or REVOKE; for default privileges, `on` is a plural kind and there are no objects. */
|
|
1445
|
+
private grant(revoke: boolean, defaults: boolean): GrantNode {
|
|
1446
|
+
const grantOptionFor = revoke && this.acceptSeq("GRANT", "OPTION", "FOR");
|
|
1447
|
+
const privileges: GrantNode["privileges"] = [];
|
|
1448
|
+
if (this.accept("ALL")) {
|
|
1449
|
+
this.accept("PRIVILEGES");
|
|
1450
|
+
const columns = this.isPunct("(") ? this.nameList() : [];
|
|
1451
|
+
privileges.push({ privilege: "ALL", columns });
|
|
1452
|
+
} else {
|
|
1453
|
+
const KNOWN = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", "MAINTAIN", "USAGE", "CREATE", "EXECUTE"];
|
|
1454
|
+
do {
|
|
1455
|
+
const t = this.next();
|
|
1456
|
+
if (!kw(t, ...KNOWN)) {
|
|
1457
|
+
if (t.kind === "ident" || t.kind === "qident" || t.kind === "ref") {
|
|
1458
|
+
this.fail("expected a privilege (a role granted to a role is membership, which is the environment's: grant it where the roles are provisioned)", t);
|
|
1459
|
+
}
|
|
1460
|
+
this.fail("expected a privilege", t);
|
|
1461
|
+
}
|
|
1462
|
+
privileges.push({ privilege: t.text.toUpperCase(), columns: this.isPunct("(") ? this.nameList() : [] });
|
|
1463
|
+
} while (this.acceptPunct(","));
|
|
1464
|
+
}
|
|
1465
|
+
this.expect("ON");
|
|
1466
|
+
const node: GrantNode = { statement: "grant", revoke, grantOptionFor, privileges, on: "TABLE", objects: [], grantees: [], withGrantOption: false, defaults };
|
|
1467
|
+
if (defaults) {
|
|
1468
|
+
if (!this.accept("TABLES", "SEQUENCES", "FUNCTIONS", "ROUTINES", "TYPES", "SCHEMAS")) this.fail("expected TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES or SCHEMAS");
|
|
1469
|
+
node.on = this.lastSeen()!.text.toUpperCase() as GrantTarget;
|
|
1470
|
+
} else {
|
|
1471
|
+
if (this.accept("ALL")) {
|
|
1472
|
+
this.fail("expected an object (ALL ... IN SCHEMA grants on what exists when it runs; grant on each declared object, and use ALTER DEFAULT PRIVILEGES for what is created later)", this.lastSeen());
|
|
1473
|
+
}
|
|
1474
|
+
if (kw(this.peek(), "DATABASE", "DOMAIN", "TYPE", "LANGUAGE", "LARGE", "TABLESPACE", "FOREIGN", "PARAMETER")) {
|
|
1475
|
+
this.fail("expected TABLE, SEQUENCE, SCHEMA, FUNCTION, PROCEDURE or ROUTINE (privileges on this kind of object are not declared)");
|
|
1476
|
+
}
|
|
1477
|
+
if (this.accept("TABLE", "SEQUENCE", "SCHEMA", "FUNCTION", "PROCEDURE", "ROUTINE")) node.on = this.lastSeen()!.text.toUpperCase() as GrantTarget;
|
|
1478
|
+
const routine = node.on === "FUNCTION" || node.on === "PROCEDURE" || node.on === "ROUTINE";
|
|
1479
|
+
do {
|
|
1480
|
+
const name = this.qualifiedName(node.on === "SCHEMA" ? "a schema name" : "an object name");
|
|
1481
|
+
node.objects.push(routine && this.isPunct("(") ? { name, args: this.parenthesized() } : { name });
|
|
1482
|
+
} while (this.acceptPunct(","));
|
|
1483
|
+
}
|
|
1484
|
+
if (revoke) this.expect("FROM");
|
|
1485
|
+
else this.expect("TO");
|
|
1486
|
+
this.accept("GROUP");
|
|
1487
|
+
node.grantees = this.roleList();
|
|
1488
|
+
if (!revoke && this.acceptSeq("WITH", "GRANT", "OPTION")) node.withGrantOption = true;
|
|
1489
|
+
if (this.acceptSeq("GRANTED", "BY")) this.fail("expected the end of the statement (GRANTED BY names another grantor; chant grants as the role it runs as)", this.lastSeen());
|
|
1490
|
+
if (revoke && this.accept("CASCADE", "RESTRICT")) {
|
|
1491
|
+
if (this.lastSeen()!.text.toUpperCase() === "CASCADE") this.fail("expected the end of the statement (CASCADE revokes what others granted from it; revoke each grant)", this.lastSeen());
|
|
1492
|
+
}
|
|
1493
|
+
return node;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private defaultPrivileges(): GrantNode {
|
|
1497
|
+
let forRoles: GrantNode["forRoles"];
|
|
1498
|
+
let inSchemas: GrantNode["inSchemas"];
|
|
1499
|
+
for (;;) {
|
|
1500
|
+
if (this.accept("FOR")) {
|
|
1501
|
+
this.expect("ROLE", "USER");
|
|
1502
|
+
forRoles = this.roleList();
|
|
1503
|
+
} else if (this.acceptSeq("IN", "SCHEMA")) {
|
|
1504
|
+
inSchemas = [];
|
|
1505
|
+
do inSchemas.push((({ text, span }) => ({ name: text, span }))(this.nameTok("a schema name")));
|
|
1506
|
+
while (this.acceptPunct(","));
|
|
1507
|
+
} else break;
|
|
1508
|
+
}
|
|
1509
|
+
let node: GrantNode;
|
|
1510
|
+
if (this.accept("GRANT")) node = this.grant(false, true);
|
|
1511
|
+
else {
|
|
1512
|
+
this.expect("REVOKE");
|
|
1513
|
+
node = this.grant(true, true);
|
|
1514
|
+
}
|
|
1515
|
+
return { ...node, ...(forRoles ? { forRoles } : {}), ...(inSchemas ? { inSchemas } : {}) };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private rowSecurity(): RowSecurityNode {
|
|
1519
|
+
this.accept("ONLY");
|
|
1520
|
+
const table = this.qualifiedName("a table name");
|
|
1521
|
+
let action: RowSecurityNode["action"];
|
|
1522
|
+
if (this.accept("ENABLE")) action = "ENABLE";
|
|
1523
|
+
else if (this.accept("DISABLE")) action = "DISABLE";
|
|
1524
|
+
else if (this.accept("FORCE")) action = "FORCE";
|
|
1525
|
+
else if (this.acceptSeq("NO", "FORCE")) action = "NO FORCE";
|
|
1526
|
+
else return this.fail("expected ENABLE, DISABLE, FORCE or NO FORCE ROW LEVEL SECURITY (a table's other changes are its CREATE TABLE's)");
|
|
1527
|
+
this.expect("ROW");
|
|
1528
|
+
this.expect("LEVEL");
|
|
1529
|
+
this.expect("SECURITY");
|
|
1530
|
+
return { statement: "rowSecurity", table, action };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1010
1533
|
private comment(): CommentNode {
|
|
1011
1534
|
this.expect("ON");
|
|
1012
1535
|
let objectType: string;
|
|
1013
1536
|
let on: NameNode | undefined;
|
|
1014
1537
|
let target: NameNode;
|
|
1538
|
+
let args: Span | undefined;
|
|
1015
1539
|
if (this.accept("CONSTRAINT")) {
|
|
1016
1540
|
objectType = "CONSTRAINT";
|
|
1017
1541
|
target = this.qualifiedName("a constraint name");
|
|
1018
1542
|
this.expect("ON");
|
|
1019
1543
|
if (this.accept("DOMAIN")) objectType = "DOMAIN CONSTRAINT";
|
|
1020
1544
|
on = this.qualifiedName("a table name");
|
|
1545
|
+
} else if (this.accept("TRIGGER", "POLICY")) {
|
|
1546
|
+
objectType = this.lastSeen()!.text.toUpperCase();
|
|
1547
|
+
target = this.qualifiedName(`a ${objectType.toLowerCase()} name`);
|
|
1548
|
+
this.expect("ON");
|
|
1549
|
+
on = this.qualifiedName("a table name");
|
|
1550
|
+
} else if (this.accept("ROLE")) {
|
|
1551
|
+
objectType = "ROLE";
|
|
1552
|
+
target = this.qualifiedName("a role name");
|
|
1021
1553
|
} else {
|
|
1022
1554
|
if (this.acceptSeq("MATERIALIZED", "VIEW")) objectType = "MATERIALIZED VIEW";
|
|
1023
|
-
else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION")) {
|
|
1555
|
+
else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION", "FUNCTION", "PROCEDURE")) {
|
|
1024
1556
|
objectType = this.tokens[this.idx(-1)]!.text.toUpperCase();
|
|
1025
|
-
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION or CONSTRAINT");
|
|
1557
|
+
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, POLICY, ROLE or CONSTRAINT");
|
|
1026
1558
|
target = this.qualifiedName("an object name");
|
|
1559
|
+
if ((objectType === "FUNCTION" || objectType === "PROCEDURE") && this.isPunct("(")) args = this.parenthesized();
|
|
1027
1560
|
}
|
|
1028
1561
|
this.expect("IS");
|
|
1029
|
-
const node: CommentNode = { statement: "comment", objectType, target, on };
|
|
1562
|
+
const node: CommentNode = { statement: "comment", objectType, target, on, ...(args ? { args } : {}) };
|
|
1030
1563
|
if (!this.accept("NULL")) {
|
|
1031
1564
|
const t = this.peek();
|
|
1032
1565
|
if (t?.kind !== "string" && t?.kind !== "ref") this.fail("expected a string or NULL");
|
|
@@ -1051,7 +1584,19 @@ class PgParser extends SqlCursor {
|
|
|
1051
1584
|
}
|
|
1052
1585
|
|
|
1053
1586
|
const describe = (n: StatementNode): string =>
|
|
1054
|
-
n.statement === "comment"
|
|
1587
|
+
n.statement === "comment"
|
|
1588
|
+
? "COMMENT ON"
|
|
1589
|
+
: n.statement === "enum"
|
|
1590
|
+
? "type definition"
|
|
1591
|
+
: n.statement === "grant"
|
|
1592
|
+
? n.defaults
|
|
1593
|
+
? "ALTER DEFAULT PRIVILEGES"
|
|
1594
|
+
: n.revoke
|
|
1595
|
+
? "REVOKE"
|
|
1596
|
+
: "GRANT"
|
|
1597
|
+
: n.statement === "rowSecurity"
|
|
1598
|
+
? "ALTER TABLE"
|
|
1599
|
+
: `${n.statement} definition`;
|
|
1055
1600
|
|
|
1056
1601
|
/**
|
|
1057
1602
|
* Parse a template: one CREATE, optionally followed by COMMENT ON statements,
|