create-monolayer-app 0.1.2 → 0.2.1
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/package.json +1 -1
- package/template/_gitignore +5 -0
- package/template/lib/db/client.ts +30 -0
- package/template/lib/db/db.ts +4 -0
- package/template/oxlint.config.ts +11 -0
- package/template/package.json +23 -3
- package/template/tsconfig.json +3 -2
- package/template/vendor/kysely-dsql/dsql/adapter.ts +15 -0
- package/template/vendor/kysely-dsql/dsql/alter-column-builder.ts +105 -0
- package/template/vendor/kysely-dsql/dsql/alter-table-builders.ts +195 -0
- package/template/vendor/kysely-dsql/dsql/compatibility-rules.ts +235 -0
- package/template/vendor/kysely-dsql/dsql/create-column-builder.ts +187 -0
- package/template/vendor/kysely-dsql/dsql/create-table-builder.ts +115 -0
- package/template/vendor/kysely-dsql/dsql/dialect.ts +41 -0
- package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +93 -0
- package/template/vendor/kysely-dsql/dsql/dsql-token.ts +29 -0
- package/template/vendor/kysely-dsql/dsql/index-view-builders.ts +206 -0
- package/template/vendor/kysely-dsql/dsql/pool.ts +40 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler-error.ts +18 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler.test.ts +765 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler.ts +259 -0
- package/template/vendor/kysely-dsql/dsql/query-validation.test.ts +518 -0
- package/template/vendor/kysely-dsql/dsql/query-validation.ts +128 -0
- package/template/vendor/kysely-dsql/dsql/raw-statement.test.ts +167 -0
- package/template/vendor/kysely-dsql/dsql/raw-statement.ts +51 -0
- package/template/vendor/kysely-dsql/dsql/schema-module.ts +167 -0
- package/template/vendor/kysely-dsql/dsql/types.ts +4 -0
- package/template/vendor/kysely-dsql/generator/create-migration.ts +86 -0
- package/template/vendor/kysely-dsql/generator/generate-types-from-pglite.ts +40 -0
- package/template/vendor/kysely-dsql/generator/generated-types-writer.ts +73 -0
- package/template/vendor/kysely-dsql/generator/migration-file.ts +73 -0
- package/template/vendor/kysely-dsql/generator/type-generator.ts +330 -0
- package/template/vendor/kysely-dsql/plugins/auto-updated-at.plugin.ts +139 -0
- package/template/vendor/kysely-dsql/plugins/index.ts +10 -0
- package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +217 -0
- package/template/vitest.config.ts +23 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { DSQLValidationCode } from "./query-validation.js";
|
|
4
|
+
import {
|
|
5
|
+
COMPATIBILITY_RULES,
|
|
6
|
+
getCompatibilityIssue,
|
|
7
|
+
validateDSQLStatement,
|
|
8
|
+
} from "./query-validation.js";
|
|
9
|
+
|
|
10
|
+
const TEST_CASES: readonly {
|
|
11
|
+
readonly code: DSQLValidationCode;
|
|
12
|
+
readonly sql: string;
|
|
13
|
+
readonly expectedFeature: string;
|
|
14
|
+
}[] = [
|
|
15
|
+
{
|
|
16
|
+
code: "foreign_key",
|
|
17
|
+
sql: "CREATE TABLE orders (id uuid PRIMARY KEY, user_id uuid REFERENCES users(id))",
|
|
18
|
+
expectedFeature: "Foreign key constraints",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: "trigger",
|
|
22
|
+
sql: "CREATE TRIGGER audit_trigger AFTER INSERT ON users FOR EACH ROW EXECUTE PROCEDURE audit_log()",
|
|
23
|
+
expectedFeature: "Triggers",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: "function_language",
|
|
27
|
+
sql: "CREATE FUNCTION calc_total(qty int, price numeric) RETURNS numeric AS $$ SELECT qty * price $$ LANGUAGE sql",
|
|
28
|
+
expectedFeature: "User-defined functions",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: "procedure",
|
|
32
|
+
sql: "CREATE PROCEDURE do_something() LANGUAGE plpgsql AS $$ BEGIN NULL; END; $$",
|
|
33
|
+
expectedFeature: "Stored procedures",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: "truncate",
|
|
37
|
+
sql: "TRUNCATE TABLE audit_events",
|
|
38
|
+
expectedFeature: "Table truncation",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: "listen_notify",
|
|
42
|
+
sql: "LISTEN cache_invalidation_channel",
|
|
43
|
+
expectedFeature: "LISTEN and NOTIFY",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
code: "extension",
|
|
47
|
+
sql: 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"',
|
|
48
|
+
expectedFeature: "PostgreSQL extensions",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: "postgres_type",
|
|
52
|
+
sql: "CREATE TYPE order_status AS ENUM ('pending', 'completed')",
|
|
53
|
+
expectedFeature: "User-defined types and domains",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
code: "temporary_object",
|
|
57
|
+
sql: "CREATE TEMPORARY TABLE staging_users (id uuid)",
|
|
58
|
+
expectedFeature: "Temporary and unlogged objects",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
code: "materialized_view",
|
|
62
|
+
sql: "CREATE MATERIALIZED VIEW user_counts AS SELECT role, count(*) FROM users GROUP BY role",
|
|
63
|
+
expectedFeature: "Materialized views",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
code: "row_level_security",
|
|
67
|
+
sql: "ALTER TABLE users ENABLE ROW LEVEL SECURITY",
|
|
68
|
+
expectedFeature: "Row-level security",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
code: "database_or_tablespace",
|
|
72
|
+
sql: "CREATE DATABASE tenant_db",
|
|
73
|
+
expectedFeature: "Database and tablespace management",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
code: "synchronous_index",
|
|
77
|
+
sql: "CREATE INDEX idx_users_name ON users (name)",
|
|
78
|
+
expectedFeature: "Synchronous index creation",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: "unsupported_index_clause",
|
|
82
|
+
sql: "CREATE INDEX ASYNC idx_users_name ON users USING btree (name)",
|
|
83
|
+
expectedFeature: "Unsupported index clauses",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
code: "alter_column_type",
|
|
87
|
+
sql: "ALTER TABLE users ALTER COLUMN name TYPE varchar(100)",
|
|
88
|
+
expectedFeature: "Altering column types",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
code: "alter_column_set_not_null",
|
|
92
|
+
sql: "ALTER TABLE users ALTER COLUMN name SET NOT NULL",
|
|
93
|
+
expectedFeature: "Setting NOT NULL on existing columns",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: "alter_add_column_constraint",
|
|
97
|
+
sql: "ALTER TABLE users ADD COLUMN score integer DEFAULT 0",
|
|
98
|
+
expectedFeature: "Adding column constraints during ALTER TABLE",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: "create_table_as",
|
|
102
|
+
sql: "CREATE TABLE users_backup AS SELECT * FROM users",
|
|
103
|
+
expectedFeature: "CREATE TABLE AS",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: "merge",
|
|
107
|
+
sql: "MERGE INTO target_table USING source_table ON target_table.id = source_table.id WHEN MATCHED THEN UPDATE SET value = source_table.value",
|
|
108
|
+
expectedFeature: "MERGE statements",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
code: "collation",
|
|
112
|
+
sql: 'SELECT * FROM users ORDER BY name COLLATE "de_DE"',
|
|
113
|
+
expectedFeature: "Collations",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
code: "vacuum",
|
|
117
|
+
sql: "VACUUM FULL users",
|
|
118
|
+
expectedFeature: "VACUUM and ANALYZE",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
describe(validateDSQLStatement, () => {
|
|
123
|
+
it("exports compatibility rules in declaration order", () => {
|
|
124
|
+
expect(COMPATIBILITY_RULES).toHaveLength(TEST_CASES.length);
|
|
125
|
+
expect(COMPATIBILITY_RULES.map(({ code }) => code)).toStrictEqual(
|
|
126
|
+
TEST_CASES.map(({ code }) => code)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("empty and basic queries", () => {
|
|
131
|
+
it("accepts empty or whitespace statements", () => {
|
|
132
|
+
expect(validateDSQLStatement("")).toStrictEqual({ supported: true });
|
|
133
|
+
expect(validateDSQLStatement(" \n\t ")).toStrictEqual({
|
|
134
|
+
supported: true,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("accepts basic SELECT and INSERT queries", () => {
|
|
139
|
+
expect(
|
|
140
|
+
validateDSQLStatement("SELECT id, name FROM users WHERE id = $1")
|
|
141
|
+
).toStrictEqual({
|
|
142
|
+
supported: true,
|
|
143
|
+
});
|
|
144
|
+
expect(
|
|
145
|
+
validateDSQLStatement("INSERT INTO users (id, name) VALUES ($1, $2)")
|
|
146
|
+
).toStrictEqual({
|
|
147
|
+
supported: true,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("accepts basic UPDATE and DELETE queries", () => {
|
|
152
|
+
expect(
|
|
153
|
+
validateDSQLStatement("UPDATE users SET name = $1 WHERE id = $2")
|
|
154
|
+
).toStrictEqual({
|
|
155
|
+
supported: true,
|
|
156
|
+
});
|
|
157
|
+
expect(
|
|
158
|
+
validateDSQLStatement("DELETE FROM users WHERE id = $1")
|
|
159
|
+
).toStrictEqual({
|
|
160
|
+
supported: true,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("supported views and sequences", () => {
|
|
166
|
+
it("accepts CREATE and DROP VIEW", () => {
|
|
167
|
+
expect(
|
|
168
|
+
validateDSQLStatement(
|
|
169
|
+
"CREATE VIEW active_users AS SELECT id, name FROM users"
|
|
170
|
+
)
|
|
171
|
+
).toStrictEqual({ supported: true });
|
|
172
|
+
expect(
|
|
173
|
+
validateDSQLStatement(
|
|
174
|
+
"CREATE OR REPLACE VIEW active_users AS SELECT id, name FROM users"
|
|
175
|
+
)
|
|
176
|
+
).toStrictEqual({ supported: true });
|
|
177
|
+
expect(validateDSQLStatement("DROP VIEW active_users")).toStrictEqual({
|
|
178
|
+
supported: true,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("accepts CREATE and DROP SEQUENCE", () => {
|
|
183
|
+
expect(
|
|
184
|
+
validateDSQLStatement(
|
|
185
|
+
"CREATE SEQUENCE order_seq START WITH 1 INCREMENT BY 1"
|
|
186
|
+
)
|
|
187
|
+
).toStrictEqual({ supported: true });
|
|
188
|
+
expect(validateDSQLStatement("DROP SEQUENCE order_seq")).toStrictEqual({
|
|
189
|
+
supported: true,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("accepts JSON and JSONB columns", () => {
|
|
194
|
+
expect(
|
|
195
|
+
validateDSQLStatement(
|
|
196
|
+
"CREATE TABLE payload_table (id uuid PRIMARY KEY, config json, data jsonb)"
|
|
197
|
+
)
|
|
198
|
+
).toStrictEqual({ supported: true });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("supported ALTER TABLE and INDEX statements", () => {
|
|
203
|
+
it("accepts DROP and RENAME column and table", () => {
|
|
204
|
+
expect(
|
|
205
|
+
validateDSQLStatement("ALTER TABLE users DROP COLUMN old_col")
|
|
206
|
+
).toStrictEqual({
|
|
207
|
+
supported: true,
|
|
208
|
+
});
|
|
209
|
+
expect(
|
|
210
|
+
validateDSQLStatement(
|
|
211
|
+
"ALTER TABLE users RENAME COLUMN old_name TO new_name"
|
|
212
|
+
)
|
|
213
|
+
).toStrictEqual({ supported: true });
|
|
214
|
+
expect(
|
|
215
|
+
validateDSQLStatement("ALTER TABLE users RENAME TO customer_accounts")
|
|
216
|
+
).toStrictEqual({
|
|
217
|
+
supported: true,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("accepts supported column alterations", () => {
|
|
222
|
+
expect(
|
|
223
|
+
validateDSQLStatement("ALTER TABLE users ADD COLUMN age integer")
|
|
224
|
+
).toStrictEqual({
|
|
225
|
+
supported: true,
|
|
226
|
+
});
|
|
227
|
+
expect(
|
|
228
|
+
validateDSQLStatement(
|
|
229
|
+
"ALTER TABLE users ADD COLUMN IF NOT EXISTS age integer"
|
|
230
|
+
)
|
|
231
|
+
).toStrictEqual({ supported: true });
|
|
232
|
+
expect(
|
|
233
|
+
validateDSQLStatement(
|
|
234
|
+
"ALTER TABLE users ALTER COLUMN status SET DEFAULT 'pending'"
|
|
235
|
+
)
|
|
236
|
+
).toStrictEqual({ supported: true });
|
|
237
|
+
expect(
|
|
238
|
+
validateDSQLStatement(
|
|
239
|
+
"ALTER TABLE users ALTER COLUMN status DROP DEFAULT"
|
|
240
|
+
)
|
|
241
|
+
).toStrictEqual({ supported: true });
|
|
242
|
+
expect(
|
|
243
|
+
validateDSQLStatement(
|
|
244
|
+
"ALTER TABLE users ALTER COLUMN status DROP NOT NULL"
|
|
245
|
+
)
|
|
246
|
+
).toStrictEqual({ supported: true });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("accepts check constraints with NOT VALID", () => {
|
|
250
|
+
expect(
|
|
251
|
+
validateDSQLStatement(
|
|
252
|
+
"ALTER TABLE users ADD CONSTRAINT chk_name CHECK (length(name) > 0) NOT VALID"
|
|
253
|
+
)
|
|
254
|
+
).toStrictEqual({ supported: true });
|
|
255
|
+
expect(
|
|
256
|
+
validateDSQLStatement(
|
|
257
|
+
"ALTER TABLE users ADD CHECK (length(name) > 0) NOT VALID"
|
|
258
|
+
)
|
|
259
|
+
).toStrictEqual({ supported: true });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("accepts supported asynchronous index forms", () => {
|
|
263
|
+
expect(
|
|
264
|
+
validateDSQLStatement(
|
|
265
|
+
"CREATE INDEX ASYNC idx_users_email ON users (email)"
|
|
266
|
+
)
|
|
267
|
+
).toStrictEqual({ supported: true });
|
|
268
|
+
expect(
|
|
269
|
+
validateDSQLStatement(
|
|
270
|
+
"CREATE UNIQUE INDEX ASYNC idx_users_email ON users (email)"
|
|
271
|
+
)
|
|
272
|
+
).toStrictEqual({ supported: true });
|
|
273
|
+
expect(
|
|
274
|
+
validateDSQLStatement(
|
|
275
|
+
"CREATE UNIQUE INDEX ASYNC IF NOT EXISTS idx_users_email ON users (email)"
|
|
276
|
+
)
|
|
277
|
+
).toStrictEqual({ supported: true });
|
|
278
|
+
expect(
|
|
279
|
+
validateDSQLStatement(
|
|
280
|
+
"CREATE INDEX IF NOT EXISTS ASYNC idx_users_email ON users (email)"
|
|
281
|
+
)
|
|
282
|
+
).toStrictEqual({ supported: true });
|
|
283
|
+
expect(
|
|
284
|
+
validateDSQLStatement(
|
|
285
|
+
"CREATE INDEX ASYNC idx_users_email ON users (email) NULLS NOT DISTINCT"
|
|
286
|
+
)
|
|
287
|
+
).toStrictEqual({ supported: true });
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("false-positive resistance", () => {
|
|
292
|
+
it("ignores keywords in single-quoted strings", () => {
|
|
293
|
+
const sql =
|
|
294
|
+
"SELECT * FROM logs WHERE message = 'FOREIGN KEY REFERENCES VACUUM TRUNCATE'";
|
|
295
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("handles escaped single quotes in strings", () => {
|
|
299
|
+
const sql =
|
|
300
|
+
"SELECT * FROM logs WHERE message = 'It''s a FOREIGN KEY REFERENCES test'";
|
|
301
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("ignores keywords in dollar-quoted strings", () => {
|
|
305
|
+
const sql1 =
|
|
306
|
+
"INSERT INTO templates (body) VALUES ($$CREATE MATERIALIZED VIEW TEMPORARY TABLE$$)";
|
|
307
|
+
expect(validateDSQLStatement(sql1)).toStrictEqual({ supported: true });
|
|
308
|
+
|
|
309
|
+
const sql2 =
|
|
310
|
+
"INSERT INTO templates (body) VALUES ($tag$DROP TRIGGER PROCEDURE EXTENSION$tag$)";
|
|
311
|
+
expect(validateDSQLStatement(sql2)).toStrictEqual({ supported: true });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("ignores keywords in double-quoted identifiers", () => {
|
|
315
|
+
const sql = 'SELECT "foreign_key", "vacuum", "trigger" FROM "references"';
|
|
316
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("handles escaped double quotes in identifiers", () => {
|
|
320
|
+
const sql = 'SELECT "my ""foreign_key"" column" FROM "my_table"';
|
|
321
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("ignores keywords in line comments", () => {
|
|
325
|
+
const sql = `
|
|
326
|
+
SELECT id FROM users
|
|
327
|
+
-- Do not use FOREIGN KEY or TRUNCATE TABLE here
|
|
328
|
+
WHERE id = $1
|
|
329
|
+
`;
|
|
330
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("ignores keywords in block comments", () => {
|
|
334
|
+
const sql = `
|
|
335
|
+
SELECT id FROM users
|
|
336
|
+
/*
|
|
337
|
+
* CREATE MATERIALIZED VIEW my_view;
|
|
338
|
+
* ALTER TABLE users ALTER COLUMN x TYPE int;
|
|
339
|
+
*/
|
|
340
|
+
WHERE id = $1
|
|
341
|
+
`;
|
|
342
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("preserves positional parameters", () => {
|
|
346
|
+
const sql =
|
|
347
|
+
"SELECT * FROM users WHERE id = $1 AND tenant_id = $2 AND code = $10";
|
|
348
|
+
expect(validateDSQLStatement(sql)).toStrictEqual({ supported: true });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("handles unclosed string literals and quotes gracefully", () => {
|
|
352
|
+
expect(
|
|
353
|
+
validateDSQLStatement(
|
|
354
|
+
"SELECT * FROM users WHERE note = 'unclosed string"
|
|
355
|
+
)
|
|
356
|
+
).toStrictEqual({
|
|
357
|
+
supported: true,
|
|
358
|
+
});
|
|
359
|
+
expect(
|
|
360
|
+
validateDSQLStatement('SELECT * FROM "unclosed identifier')
|
|
361
|
+
).toStrictEqual({
|
|
362
|
+
supported: true,
|
|
363
|
+
});
|
|
364
|
+
expect(
|
|
365
|
+
validateDSQLStatement("SELECT * FROM $$unclosed dollar quote")
|
|
366
|
+
).toStrictEqual({
|
|
367
|
+
supported: true,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("unsupported rule detection", () => {
|
|
373
|
+
it.each(TEST_CASES)("detects $code", ({ code, sql, expectedFeature }) => {
|
|
374
|
+
const result = validateDSQLStatement(sql);
|
|
375
|
+
expect(result).toStrictEqual({
|
|
376
|
+
supported: false,
|
|
377
|
+
issues: [
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
code,
|
|
380
|
+
feature: expectedFeature,
|
|
381
|
+
documentationUrl: expect.stringMatching(
|
|
382
|
+
/^https:\/\/docs\.aws\.amazon\.com\/aurora-dsql\//u
|
|
383
|
+
),
|
|
384
|
+
}),
|
|
385
|
+
],
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("returns multiple matched issues in stable order", () => {
|
|
390
|
+
const sql = "TRUNCATE TABLE users; VACUUM users;";
|
|
391
|
+
const result = validateDSQLStatement(sql);
|
|
392
|
+
expect(result).toStrictEqual({
|
|
393
|
+
supported: false,
|
|
394
|
+
issues: [
|
|
395
|
+
expect.objectContaining({ code: "truncate" }),
|
|
396
|
+
expect.objectContaining({ code: "vacuum" }),
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("detects unsupported index WHERE clause", () => {
|
|
402
|
+
expect(
|
|
403
|
+
validateDSQLStatement(
|
|
404
|
+
"CREATE INDEX ASYNC idx_user_active ON users (email) WHERE active = true"
|
|
405
|
+
)
|
|
406
|
+
).toStrictEqual({
|
|
407
|
+
supported: false,
|
|
408
|
+
issues: [
|
|
409
|
+
expect.objectContaining({
|
|
410
|
+
code: "unsupported_index_clause",
|
|
411
|
+
}),
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("detects unsupported index ASC clause", () => {
|
|
417
|
+
expect(
|
|
418
|
+
validateDSQLStatement(
|
|
419
|
+
"CREATE INDEX ASYNC idx_user_name ON users (name ASC)"
|
|
420
|
+
)
|
|
421
|
+
).toStrictEqual({
|
|
422
|
+
supported: false,
|
|
423
|
+
issues: [
|
|
424
|
+
expect.objectContaining({
|
|
425
|
+
code: "unsupported_index_clause",
|
|
426
|
+
}),
|
|
427
|
+
],
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("detects unsupported index NULLS FIRST clause", () => {
|
|
432
|
+
expect(
|
|
433
|
+
validateDSQLStatement(
|
|
434
|
+
"CREATE INDEX ASYNC idx_user_name ON users (name NULLS FIRST)"
|
|
435
|
+
)
|
|
436
|
+
).toStrictEqual({
|
|
437
|
+
supported: false,
|
|
438
|
+
issues: [
|
|
439
|
+
expect.objectContaining({
|
|
440
|
+
code: "unsupported_index_clause",
|
|
441
|
+
}),
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("detects alter_add_column_constraint with and without COLUMN keyword", () => {
|
|
447
|
+
expect(
|
|
448
|
+
validateDSQLStatement(
|
|
449
|
+
"ALTER TABLE users ADD COLUMN score integer DEFAULT 0"
|
|
450
|
+
)
|
|
451
|
+
).toStrictEqual({
|
|
452
|
+
supported: false,
|
|
453
|
+
issues: [
|
|
454
|
+
expect.objectContaining({
|
|
455
|
+
code: "alter_add_column_constraint",
|
|
456
|
+
}),
|
|
457
|
+
],
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(
|
|
461
|
+
validateDSQLStatement("ALTER TABLE users ADD score integer DEFAULT 0")
|
|
462
|
+
).toStrictEqual({
|
|
463
|
+
supported: false,
|
|
464
|
+
issues: [
|
|
465
|
+
expect.objectContaining({
|
|
466
|
+
code: "alter_add_column_constraint",
|
|
467
|
+
}),
|
|
468
|
+
],
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(
|
|
472
|
+
validateDSQLStatement(
|
|
473
|
+
"ALTER TABLE users ADD COLUMN is_active boolean NOT NULL"
|
|
474
|
+
)
|
|
475
|
+
).toStrictEqual({
|
|
476
|
+
supported: false,
|
|
477
|
+
issues: [
|
|
478
|
+
expect.objectContaining({
|
|
479
|
+
code: "alter_add_column_constraint",
|
|
480
|
+
}),
|
|
481
|
+
],
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("detects SELECT INTO as create_table_as", () => {
|
|
486
|
+
expect(
|
|
487
|
+
validateDSQLStatement("SELECT * INTO backup_users FROM users")
|
|
488
|
+
).toStrictEqual({
|
|
489
|
+
supported: false,
|
|
490
|
+
issues: [
|
|
491
|
+
expect.objectContaining({
|
|
492
|
+
code: "create_table_as",
|
|
493
|
+
}),
|
|
494
|
+
],
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe(getCompatibilityIssue, () => {
|
|
500
|
+
it("returns the issue for a known code", () => {
|
|
501
|
+
const issue = getCompatibilityIssue("foreign_key");
|
|
502
|
+
expect(issue.code).toBe("foreign_key");
|
|
503
|
+
expect(issue.feature).toBe("Foreign key constraints");
|
|
504
|
+
expect(issue.message).toContain("foreign key");
|
|
505
|
+
expect(issue.documentationUrl).toContain(
|
|
506
|
+
"docs.aws.amazon.com/aurora-dsql"
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("throws for an unknown code", () => {
|
|
511
|
+
// SAFETY: Invalid code is used to test runtime error checking.
|
|
512
|
+
const invalidCode = "unknown_code" as DSQLValidationCode;
|
|
513
|
+
expect(() => getCompatibilityIssue(invalidCode)).toThrow(
|
|
514
|
+
"Unknown compatibility code: unknown_code"
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aurora DSQL query compatibility validation.
|
|
3
|
+
*
|
|
4
|
+
* This module checks SQL statements for known Aurora DSQL restrictions.
|
|
5
|
+
* It operates on raw SQL strings without throwing errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { COMPATIBILITY_RULES } from "./compatibility-rules.js";
|
|
9
|
+
import type { CompatibilityRule } from "./compatibility-rules.js";
|
|
10
|
+
|
|
11
|
+
export { COMPATIBILITY_RULES } from "./compatibility-rules.js";
|
|
12
|
+
|
|
13
|
+
export type DSQLValidationCode =
|
|
14
|
+
| "foreign_key"
|
|
15
|
+
| "trigger"
|
|
16
|
+
| "function_language"
|
|
17
|
+
| "procedure"
|
|
18
|
+
| "truncate"
|
|
19
|
+
| "listen_notify"
|
|
20
|
+
| "extension"
|
|
21
|
+
| "postgres_type"
|
|
22
|
+
| "temporary_object"
|
|
23
|
+
| "materialized_view"
|
|
24
|
+
| "row_level_security"
|
|
25
|
+
| "database_or_tablespace"
|
|
26
|
+
| "synchronous_index"
|
|
27
|
+
| "unsupported_index_clause"
|
|
28
|
+
| "alter_column_type"
|
|
29
|
+
| "alter_column_set_not_null"
|
|
30
|
+
| "alter_add_column_constraint"
|
|
31
|
+
| "create_table_as"
|
|
32
|
+
| "merge"
|
|
33
|
+
| "collation"
|
|
34
|
+
| "vacuum";
|
|
35
|
+
|
|
36
|
+
export interface DSQLValidationIssue {
|
|
37
|
+
readonly code: DSQLValidationCode;
|
|
38
|
+
readonly feature: string;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
readonly documentationUrl: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DSQLValidationResult =
|
|
44
|
+
| { readonly supported: true }
|
|
45
|
+
| {
|
|
46
|
+
readonly supported: false;
|
|
47
|
+
readonly issues: readonly DSQLValidationIssue[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SQL_TOKEN_STRIP_REGEX =
|
|
51
|
+
/--[^\n\r]*|\/\*[\s\S]*?(?:\*\/|$)|'(?:[^']|'')*(?:'|$)|"(?:[^"]|"")*(?:"|$)|(?:\$(?<tag>[A-Za-z_][A-Za-z0-9_]*)?\$[\s\S]*?(?:\$\k<tag>\$|$))/gu;
|
|
52
|
+
|
|
53
|
+
const stripNonCodeSql = (statement: string): string =>
|
|
54
|
+
statement.replaceAll(SQL_TOKEN_STRIP_REGEX, " ");
|
|
55
|
+
|
|
56
|
+
const normalizeSql = (statement: string): string => {
|
|
57
|
+
const stripped = stripNonCodeSql(statement);
|
|
58
|
+
return stripped.replaceAll(/\s+/gu, " ").trim().toLowerCase();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const checkRule = (
|
|
62
|
+
rule: CompatibilityRule,
|
|
63
|
+
normalizedSql: string
|
|
64
|
+
): DSQLValidationIssue | null => {
|
|
65
|
+
if (!rule.check(normalizedSql)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
code: rule.code,
|
|
70
|
+
feature: rule.feature,
|
|
71
|
+
message: rule.message,
|
|
72
|
+
documentationUrl: rule.documentationUrl,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const collectIssues = (
|
|
77
|
+
normalizedSql: string
|
|
78
|
+
): readonly DSQLValidationIssue[] => {
|
|
79
|
+
const issues: DSQLValidationIssue[] = [];
|
|
80
|
+
for (const rule of COMPATIBILITY_RULES) {
|
|
81
|
+
const issue = checkRule(rule, normalizedSql);
|
|
82
|
+
if (issue !== null) {
|
|
83
|
+
issues.push(issue);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Retrieves a predefined validation issue by its error code.
|
|
91
|
+
*
|
|
92
|
+
* @param code - The validation code to look up.
|
|
93
|
+
* @returns The matching DSQLValidationIssue.
|
|
94
|
+
*/
|
|
95
|
+
export function getCompatibilityIssue(
|
|
96
|
+
code: DSQLValidationCode
|
|
97
|
+
): DSQLValidationIssue {
|
|
98
|
+
const rule = COMPATIBILITY_RULES.find((r) => r.code === code);
|
|
99
|
+
if (!rule) {
|
|
100
|
+
throw new Error(`Unknown compatibility code: ${code}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
code: rule.code,
|
|
104
|
+
feature: rule.feature,
|
|
105
|
+
message: rule.message,
|
|
106
|
+
documentationUrl: rule.documentationUrl,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates a SQL statement against known Amazon Aurora DSQL compatibility rules.
|
|
112
|
+
*
|
|
113
|
+
* @param statement - The raw SQL statement string to validate.
|
|
114
|
+
* @returns A result object indicating whether the statement is supported, with any detected issues.
|
|
115
|
+
*/
|
|
116
|
+
export function validateDSQLStatement(statement: string): DSQLValidationResult {
|
|
117
|
+
const normalizedSql = normalizeSql(statement);
|
|
118
|
+
if (normalizedSql.length === 0) {
|
|
119
|
+
return { supported: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const issues = collectIssues(normalizedSql);
|
|
123
|
+
if (issues.length === 0) {
|
|
124
|
+
return { supported: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { supported: false, issues };
|
|
128
|
+
}
|