create-monolayer-app 0.1.1 → 0.2.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/bin/index.js +1 -1
- 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,765 @@
|
|
|
1
|
+
// oxlint-disable max-classes-per-file typescript/no-explicit-any
|
|
2
|
+
import {
|
|
3
|
+
AddColumnNode,
|
|
4
|
+
AddConstraintNode,
|
|
5
|
+
CheckConstraintNode,
|
|
6
|
+
ColumnDefinitionNode,
|
|
7
|
+
ColumnNode,
|
|
8
|
+
DataTypeNode,
|
|
9
|
+
DefaultValueNode,
|
|
10
|
+
ForeignKeyConstraintNode,
|
|
11
|
+
GeneratedNode,
|
|
12
|
+
IdentifierNode,
|
|
13
|
+
Kysely,
|
|
14
|
+
OrderByItemNode,
|
|
15
|
+
PostgresIntrospector,
|
|
16
|
+
RawNode,
|
|
17
|
+
ReferencesNode,
|
|
18
|
+
SchemableIdentifierNode,
|
|
19
|
+
SelectQueryNode,
|
|
20
|
+
TableNode,
|
|
21
|
+
ValueNode,
|
|
22
|
+
sql,
|
|
23
|
+
} from "kysely";
|
|
24
|
+
import type {
|
|
25
|
+
AlterColumnNode,
|
|
26
|
+
AlterTypeNode,
|
|
27
|
+
CreateIndexNode,
|
|
28
|
+
CreateTableNode,
|
|
29
|
+
CreateTypeNode,
|
|
30
|
+
CreateViewNode,
|
|
31
|
+
DatabaseConnection,
|
|
32
|
+
Dialect,
|
|
33
|
+
DialectAdapter,
|
|
34
|
+
Driver,
|
|
35
|
+
DropTypeNode,
|
|
36
|
+
DropViewNode,
|
|
37
|
+
ModifyColumnNode,
|
|
38
|
+
OperationNode,
|
|
39
|
+
QueryResult,
|
|
40
|
+
RefreshMaterializedViewNode,
|
|
41
|
+
RootOperationNode,
|
|
42
|
+
TransactionSettings,
|
|
43
|
+
} from "kysely";
|
|
44
|
+
import { describe, expect, it, vi } from "vitest";
|
|
45
|
+
|
|
46
|
+
import { DQSLAdapter } from "./adapter.js";
|
|
47
|
+
import { DSQLQueryCompilerError } from "./query-compiler-error.js";
|
|
48
|
+
import { DSQLQueryCompiler } from "./query-compiler.js";
|
|
49
|
+
|
|
50
|
+
const compileNode = (node: OperationNode): string => {
|
|
51
|
+
const compiler = new DSQLQueryCompiler();
|
|
52
|
+
// SAFETY: OperationNode is cast to RootOperationNode for isolated node compilation testing.
|
|
53
|
+
const rootNode = node as RootOperationNode;
|
|
54
|
+
const compiled = compiler.compileQuery(rootNode, {
|
|
55
|
+
queryId: "test-query-id",
|
|
56
|
+
});
|
|
57
|
+
return compiled.sql;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
class SpyDriver implements Driver {
|
|
61
|
+
readonly acquireConnection = vi.fn<() => Promise<DatabaseConnection>>();
|
|
62
|
+
readonly beginTransaction =
|
|
63
|
+
vi.fn<
|
|
64
|
+
(
|
|
65
|
+
connection: DatabaseConnection,
|
|
66
|
+
settings: TransactionSettings
|
|
67
|
+
) => Promise<void>
|
|
68
|
+
>();
|
|
69
|
+
readonly commitTransaction =
|
|
70
|
+
vi.fn<(connection: DatabaseConnection) => Promise<void>>();
|
|
71
|
+
readonly rollbackTransaction =
|
|
72
|
+
vi.fn<(connection: DatabaseConnection) => Promise<void>>();
|
|
73
|
+
readonly releaseConnection =
|
|
74
|
+
vi.fn<(connection: DatabaseConnection) => Promise<void>>();
|
|
75
|
+
readonly destroy = vi.fn<() => Promise<void>>();
|
|
76
|
+
readonly init = vi.fn<() => Promise<void>>().mockResolvedValue();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class SpyDialect implements Dialect {
|
|
80
|
+
readonly driver: SpyDriver;
|
|
81
|
+
|
|
82
|
+
constructor(driver: SpyDriver) {
|
|
83
|
+
this.driver = driver;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
createDriver(): Driver {
|
|
87
|
+
return this.driver;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
91
|
+
createQueryCompiler(): DSQLQueryCompiler {
|
|
92
|
+
return new DSQLQueryCompiler();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
96
|
+
createAdapter(): DialectAdapter {
|
|
97
|
+
return new DQSLAdapter();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
101
|
+
createIntrospector(database: Kysely<any>): PostgresIntrospector {
|
|
102
|
+
return new PostgresIntrospector(database);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const createTestDb = (driver: SpyDriver): Kysely<any> =>
|
|
107
|
+
new Kysely<any>({
|
|
108
|
+
dialect: new SpyDialect(driver),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe(DSQLQueryCompilerError, () => {
|
|
112
|
+
it("constructs an error with issues metadata", () => {
|
|
113
|
+
const error = new DSQLQueryCompilerError([
|
|
114
|
+
{
|
|
115
|
+
code: "foreign_key",
|
|
116
|
+
feature: "Foreign key constraints",
|
|
117
|
+
message: "Aurora DSQL does not support foreign key constraints.",
|
|
118
|
+
documentationUrl:
|
|
119
|
+
"https://docs.aws.amazon.com/aurora-dsql/latest/userguide/create-table-syntax-support.html",
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
expect(error.name).toBe("DSQLQueryCompilerError");
|
|
124
|
+
expect(error.issues).toHaveLength(1);
|
|
125
|
+
expect(error.issues[0]?.code).toBe("foreign_key");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not expose SQL text in the error message", () => {
|
|
129
|
+
const error = new DSQLQueryCompilerError([
|
|
130
|
+
{
|
|
131
|
+
code: "foreign_key",
|
|
132
|
+
feature: "Foreign key constraints",
|
|
133
|
+
message: "Aurora DSQL does not support foreign key constraints.",
|
|
134
|
+
documentationUrl:
|
|
135
|
+
"https://docs.aws.amazon.com/aurora-dsql/latest/userguide/create-table-syntax-support.html",
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
expect(error.message).toContain("[foreign_key]");
|
|
140
|
+
expect(error.message).not.toContain("SELECT");
|
|
141
|
+
expect(error.message).not.toContain("CREATE TABLE");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe(DSQLQueryCompiler, () => {
|
|
146
|
+
describe("compileQuery SQL backstop validation", () => {
|
|
147
|
+
it("throws DSQLQueryCompilerError for unsupported raw SQL", () => {
|
|
148
|
+
const compiler = new DSQLQueryCompiler();
|
|
149
|
+
const node = RawNode.createWithSql("TRUNCATE TABLE users;");
|
|
150
|
+
// SAFETY: RawNode is cast to RootOperationNode for compiler testing.
|
|
151
|
+
const rootNode = node as RootOperationNode;
|
|
152
|
+
|
|
153
|
+
expect(() => compiler.compileQuery(rootNode, { queryId: "q1" })).toThrow(
|
|
154
|
+
DSQLQueryCompilerError
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("does not expose SQL in compiler error message", () => {
|
|
159
|
+
const compiler = new DSQLQueryCompiler();
|
|
160
|
+
const node = RawNode.createWithSql("TRUNCATE TABLE users;");
|
|
161
|
+
// SAFETY: RawNode is cast to RootOperationNode for compiler testing.
|
|
162
|
+
const rootNode = node as RootOperationNode;
|
|
163
|
+
|
|
164
|
+
let caughtError: DSQLQueryCompilerError | null = null;
|
|
165
|
+
try {
|
|
166
|
+
compiler.compileQuery(rootNode, { queryId: "q1" });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof DSQLQueryCompilerError) {
|
|
169
|
+
caughtError = error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
expect(caughtError).not.toBeNull();
|
|
174
|
+
expect(
|
|
175
|
+
caughtError?.issues.some((issue) => issue.code === "truncate")
|
|
176
|
+
).toBeTruthy();
|
|
177
|
+
expect(caughtError?.message).not.toContain("TRUNCATE TABLE users");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("compiles supported raw queries successfully", () => {
|
|
181
|
+
const compiler = new DSQLQueryCompiler();
|
|
182
|
+
const node = RawNode.createWithSql("SELECT 1");
|
|
183
|
+
// SAFETY: RawNode is cast to RootOperationNode for compiler testing.
|
|
184
|
+
const rootNode = node as RootOperationNode;
|
|
185
|
+
const compiled = compiler.compileQuery(rootNode, { queryId: "q2" });
|
|
186
|
+
expect(compiled.sql).toBe("SELECT 1");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("structured CREATE TABLE guards", () => {
|
|
191
|
+
it("rejects temporary tables", () => {
|
|
192
|
+
const node: CreateTableNode = {
|
|
193
|
+
kind: "CreateTableNode",
|
|
194
|
+
table: TableNode.create("temp_users"),
|
|
195
|
+
columns: [
|
|
196
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("uuid")),
|
|
197
|
+
],
|
|
198
|
+
temporary: true,
|
|
199
|
+
};
|
|
200
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("rejects onCommit actions", () => {
|
|
204
|
+
const node: CreateTableNode = {
|
|
205
|
+
kind: "CreateTableNode",
|
|
206
|
+
table: TableNode.create("temp_users"),
|
|
207
|
+
columns: [
|
|
208
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("uuid")),
|
|
209
|
+
],
|
|
210
|
+
onCommit: "drop",
|
|
211
|
+
};
|
|
212
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("rejects CREATE TABLE AS (selectQuery)", () => {
|
|
216
|
+
const node: CreateTableNode = {
|
|
217
|
+
kind: "CreateTableNode",
|
|
218
|
+
table: TableNode.create("user_copy"),
|
|
219
|
+
columns: [],
|
|
220
|
+
selectQuery: SelectQueryNode.create(),
|
|
221
|
+
};
|
|
222
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("rejects inline indexes inside CREATE TABLE", () => {
|
|
226
|
+
// SAFETY: Cast mock index node to AddIndexNode array for test assertion.
|
|
227
|
+
const mockIndex = {
|
|
228
|
+
kind: "AddIndexNode",
|
|
229
|
+
columns: [ColumnNode.create("id")],
|
|
230
|
+
} as any;
|
|
231
|
+
const node: CreateTableNode = {
|
|
232
|
+
kind: "CreateTableNode",
|
|
233
|
+
table: TableNode.create("users"),
|
|
234
|
+
columns: [
|
|
235
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("uuid")),
|
|
236
|
+
],
|
|
237
|
+
indexes: [mockIndex],
|
|
238
|
+
};
|
|
239
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("rejects foreign key constraints inside CREATE TABLE", () => {
|
|
243
|
+
const node: CreateTableNode = {
|
|
244
|
+
kind: "CreateTableNode",
|
|
245
|
+
table: TableNode.create("orders"),
|
|
246
|
+
columns: [
|
|
247
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("uuid")),
|
|
248
|
+
],
|
|
249
|
+
constraints: [
|
|
250
|
+
ForeignKeyConstraintNode.create(
|
|
251
|
+
[ColumnNode.create("user_id")],
|
|
252
|
+
TableNode.create("users"),
|
|
253
|
+
[ColumnNode.create("id")]
|
|
254
|
+
),
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("structured ColumnDefinition guards", () => {
|
|
262
|
+
it("rejects references on column definition", () => {
|
|
263
|
+
const node = ColumnDefinitionNode.cloneWith(
|
|
264
|
+
ColumnDefinitionNode.create("user_id", DataTypeNode.create("uuid")),
|
|
265
|
+
{
|
|
266
|
+
references: ReferencesNode.create(TableNode.create("users"), [
|
|
267
|
+
ColumnNode.create("id"),
|
|
268
|
+
]),
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
const tableNode: CreateTableNode = {
|
|
272
|
+
kind: "CreateTableNode",
|
|
273
|
+
table: TableNode.create("orders"),
|
|
274
|
+
columns: [node],
|
|
275
|
+
};
|
|
276
|
+
expect(() => compileNode(tableNode)).toThrow(DSQLQueryCompilerError);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("rejects autoIncrement on column definition", () => {
|
|
280
|
+
const node = ColumnDefinitionNode.cloneWith(
|
|
281
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("integer")),
|
|
282
|
+
{ autoIncrement: true }
|
|
283
|
+
);
|
|
284
|
+
const tableNode: CreateTableNode = {
|
|
285
|
+
kind: "CreateTableNode",
|
|
286
|
+
table: TableNode.create("users"),
|
|
287
|
+
columns: [node],
|
|
288
|
+
};
|
|
289
|
+
expect(() => compileNode(tableNode)).toThrow(DSQLQueryCompilerError);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("rejects identity on column definition", () => {
|
|
293
|
+
const node = ColumnDefinitionNode.cloneWith(
|
|
294
|
+
ColumnDefinitionNode.create("id", DataTypeNode.create("bigint")),
|
|
295
|
+
{ identity: true }
|
|
296
|
+
);
|
|
297
|
+
const tableNode: CreateTableNode = {
|
|
298
|
+
kind: "CreateTableNode",
|
|
299
|
+
table: TableNode.create("users"),
|
|
300
|
+
columns: [node],
|
|
301
|
+
};
|
|
302
|
+
expect(() => compileNode(tableNode)).toThrow(DSQLQueryCompilerError);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("structured CREATE/DROP/REFRESH VIEW guards", () => {
|
|
307
|
+
it("rejects temporary views", () => {
|
|
308
|
+
const node: CreateViewNode = {
|
|
309
|
+
kind: "CreateViewNode",
|
|
310
|
+
name: SchemableIdentifierNode.create("temp_view"),
|
|
311
|
+
temporary: true,
|
|
312
|
+
};
|
|
313
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("rejects materialized views in createView", () => {
|
|
317
|
+
const node: CreateViewNode = {
|
|
318
|
+
kind: "CreateViewNode",
|
|
319
|
+
name: SchemableIdentifierNode.create("mat_view"),
|
|
320
|
+
materialized: true,
|
|
321
|
+
};
|
|
322
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("rejects materialized views in dropView", () => {
|
|
326
|
+
const node: DropViewNode = {
|
|
327
|
+
kind: "DropViewNode",
|
|
328
|
+
name: SchemableIdentifierNode.create("mat_view"),
|
|
329
|
+
materialized: true,
|
|
330
|
+
};
|
|
331
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("rejects refresh materialized view", () => {
|
|
335
|
+
const node: RefreshMaterializedViewNode = {
|
|
336
|
+
kind: "RefreshMaterializedViewNode",
|
|
337
|
+
name: SchemableIdentifierNode.create("mat_view"),
|
|
338
|
+
};
|
|
339
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("compiles supported views", () => {
|
|
343
|
+
const createNode: CreateViewNode = {
|
|
344
|
+
kind: "CreateViewNode",
|
|
345
|
+
name: SchemableIdentifierNode.create("active_users"),
|
|
346
|
+
orReplace: true,
|
|
347
|
+
as: RawNode.createWithSql("SELECT id FROM users"),
|
|
348
|
+
};
|
|
349
|
+
expect(compileNode(createNode)).toBe(
|
|
350
|
+
'create or replace view "active_users" as SELECT id FROM users'
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const dropNode: DropViewNode = {
|
|
354
|
+
kind: "DropViewNode",
|
|
355
|
+
name: SchemableIdentifierNode.create("active_users"),
|
|
356
|
+
};
|
|
357
|
+
expect(compileNode(dropNode)).toBe('drop view "active_users"');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("structured Type guards", () => {
|
|
362
|
+
it("rejects createType", () => {
|
|
363
|
+
const node: CreateTypeNode = {
|
|
364
|
+
kind: "CreateTypeNode",
|
|
365
|
+
name: SchemableIdentifierNode.create("status_enum"),
|
|
366
|
+
};
|
|
367
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("rejects dropType", () => {
|
|
371
|
+
const node: DropTypeNode = {
|
|
372
|
+
kind: "DropTypeNode",
|
|
373
|
+
name: SchemableIdentifierNode.create("status_enum"),
|
|
374
|
+
};
|
|
375
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("rejects alterType", () => {
|
|
379
|
+
const node: AlterTypeNode = {
|
|
380
|
+
kind: "AlterTypeNode",
|
|
381
|
+
name: SchemableIdentifierNode.create("status_enum"),
|
|
382
|
+
};
|
|
383
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("structured Foreign Key and References visitor guards", () => {
|
|
388
|
+
it("rejects visitForeignKeyConstraint directly", () => {
|
|
389
|
+
const node = ForeignKeyConstraintNode.create(
|
|
390
|
+
[ColumnNode.create("user_id")],
|
|
391
|
+
TableNode.create("users"),
|
|
392
|
+
[ColumnNode.create("id")]
|
|
393
|
+
);
|
|
394
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("rejects visitReferences directly", () => {
|
|
398
|
+
const node = ReferencesNode.create(TableNode.create("users"), [
|
|
399
|
+
ColumnNode.create("id"),
|
|
400
|
+
]);
|
|
401
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("structured ALTER TABLE column modification guards", () => {
|
|
406
|
+
it("rejects visitModifyColumn", () => {
|
|
407
|
+
const node: ModifyColumnNode = {
|
|
408
|
+
kind: "ModifyColumnNode",
|
|
409
|
+
column: ColumnDefinitionNode.create(
|
|
410
|
+
"age",
|
|
411
|
+
DataTypeNode.create("integer")
|
|
412
|
+
),
|
|
413
|
+
};
|
|
414
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("rejects altering column type", () => {
|
|
418
|
+
const node: AlterColumnNode = {
|
|
419
|
+
kind: "AlterColumnNode",
|
|
420
|
+
column: ColumnNode.create("age"),
|
|
421
|
+
dataType: DataTypeNode.create("bigint"),
|
|
422
|
+
};
|
|
423
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("rejects altering column type with expression", () => {
|
|
427
|
+
const node: AlterColumnNode = {
|
|
428
|
+
kind: "AlterColumnNode",
|
|
429
|
+
column: ColumnNode.create("age"),
|
|
430
|
+
dataTypeExpression: RawNode.createWithSql("bigint"),
|
|
431
|
+
};
|
|
432
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("rejects altering column SET NOT NULL", () => {
|
|
436
|
+
const node: AlterColumnNode = {
|
|
437
|
+
kind: "AlterColumnNode",
|
|
438
|
+
column: ColumnNode.create("age"),
|
|
439
|
+
setNotNull: true,
|
|
440
|
+
};
|
|
441
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("compiles supported alter column actions: drop not null, set default, drop default", () => {
|
|
445
|
+
const dropNotNull: AlterColumnNode = {
|
|
446
|
+
kind: "AlterColumnNode",
|
|
447
|
+
column: ColumnNode.create("age"),
|
|
448
|
+
dropNotNull: true,
|
|
449
|
+
};
|
|
450
|
+
expect(compileNode(dropNotNull)).toBe('alter column "age" drop not null');
|
|
451
|
+
|
|
452
|
+
const setDefault: AlterColumnNode = {
|
|
453
|
+
kind: "AlterColumnNode",
|
|
454
|
+
column: ColumnNode.create("status"),
|
|
455
|
+
setDefault: ValueNode.create("active"),
|
|
456
|
+
};
|
|
457
|
+
expect(compileNode(setDefault)).toBe(
|
|
458
|
+
'alter column "status" set default $1'
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const dropDefault: AlterColumnNode = {
|
|
462
|
+
kind: "AlterColumnNode",
|
|
463
|
+
column: ColumnNode.create("status"),
|
|
464
|
+
dropDefault: true,
|
|
465
|
+
};
|
|
466
|
+
expect(compileNode(dropDefault)).toBe(
|
|
467
|
+
'alter column "status" drop default'
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("structured ALTER TABLE ADD COLUMN guards", () => {
|
|
473
|
+
it("rejects adding column with defaultTo", () => {
|
|
474
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
475
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
476
|
+
{ defaultTo: DefaultValueNode.create(ValueNode.create(0)) }
|
|
477
|
+
);
|
|
478
|
+
const node = AddColumnNode.create(col);
|
|
479
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("rejects adding column with notNull", () => {
|
|
483
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
484
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
485
|
+
{ notNull: true }
|
|
486
|
+
);
|
|
487
|
+
const node = AddColumnNode.create(col);
|
|
488
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("rejects adding column with primaryKey", () => {
|
|
492
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
493
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
494
|
+
{ primaryKey: true }
|
|
495
|
+
);
|
|
496
|
+
const node = AddColumnNode.create(col);
|
|
497
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("rejects adding column with unique", () => {
|
|
501
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
502
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
503
|
+
{ unique: true }
|
|
504
|
+
);
|
|
505
|
+
const node = AddColumnNode.create(col);
|
|
506
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("rejects adding column with references", () => {
|
|
510
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
511
|
+
ColumnDefinitionNode.create("user_id", DataTypeNode.create("uuid")),
|
|
512
|
+
{
|
|
513
|
+
references: ReferencesNode.create(TableNode.create("users"), [
|
|
514
|
+
ColumnNode.create("id"),
|
|
515
|
+
]),
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
const node = AddColumnNode.create(col);
|
|
519
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("rejects adding column with check", () => {
|
|
523
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
524
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
525
|
+
{
|
|
526
|
+
check: CheckConstraintNode.create(
|
|
527
|
+
RawNode.createWithSql("score >= 0")
|
|
528
|
+
),
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
const node = AddColumnNode.create(col);
|
|
532
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("rejects adding column with generated", () => {
|
|
536
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
537
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
538
|
+
{
|
|
539
|
+
generated: GeneratedNode.cloneWith(
|
|
540
|
+
GeneratedNode.createWithExpression(RawNode.createWithSql("1 + 1")),
|
|
541
|
+
{ stored: true }
|
|
542
|
+
),
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
const node = AddColumnNode.create(col);
|
|
546
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("rejects adding column with autoIncrement or identity", () => {
|
|
550
|
+
const col1 = ColumnDefinitionNode.cloneWith(
|
|
551
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
552
|
+
{ autoIncrement: true }
|
|
553
|
+
);
|
|
554
|
+
expect(() => compileNode(AddColumnNode.create(col1))).toThrow(
|
|
555
|
+
DSQLQueryCompilerError
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const col2 = ColumnDefinitionNode.cloneWith(
|
|
559
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
560
|
+
{ identity: true }
|
|
561
|
+
);
|
|
562
|
+
expect(() => compileNode(AddColumnNode.create(col2))).toThrow(
|
|
563
|
+
DSQLQueryCompilerError
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("rejects adding column with unsigned", () => {
|
|
568
|
+
const col = ColumnDefinitionNode.cloneWith(
|
|
569
|
+
ColumnDefinitionNode.create("score", DataTypeNode.create("integer")),
|
|
570
|
+
{ unsigned: true }
|
|
571
|
+
);
|
|
572
|
+
const node = AddColumnNode.create(col);
|
|
573
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("compiles plain ADD COLUMN", () => {
|
|
577
|
+
const col = ColumnDefinitionNode.create(
|
|
578
|
+
"age",
|
|
579
|
+
DataTypeNode.create("integer")
|
|
580
|
+
);
|
|
581
|
+
const node = AddColumnNode.create(col);
|
|
582
|
+
expect(compileNode(node)).toBe('add column "age" integer');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe("structured ALTER TABLE ADD CONSTRAINT guards and NOT VALID emission", () => {
|
|
587
|
+
it("rejects adding foreign key constraint via ALTER TABLE", () => {
|
|
588
|
+
const foreignKey = ForeignKeyConstraintNode.create(
|
|
589
|
+
[ColumnNode.create("user_id")],
|
|
590
|
+
TableNode.create("users"),
|
|
591
|
+
[ColumnNode.create("id")]
|
|
592
|
+
);
|
|
593
|
+
const node = AddConstraintNode.create(foreignKey);
|
|
594
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("appends NOT VALID when adding check constraint", () => {
|
|
598
|
+
const checkNode = CheckConstraintNode.create(
|
|
599
|
+
RawNode.createWithSql("length(name) > 0"),
|
|
600
|
+
"chk_name"
|
|
601
|
+
);
|
|
602
|
+
const node = AddConstraintNode.create(checkNode);
|
|
603
|
+
expect(compileNode(node)).toBe(
|
|
604
|
+
'add constraint "chk_name" check (length(name) > 0) not valid'
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe("structured CREATE INDEX guards and deterministic output", () => {
|
|
610
|
+
it("rejects index without table", () => {
|
|
611
|
+
const node: CreateIndexNode = {
|
|
612
|
+
kind: "CreateIndexNode",
|
|
613
|
+
name: IdentifierNode.create("idx_test"),
|
|
614
|
+
columns: [ColumnNode.create("email")],
|
|
615
|
+
};
|
|
616
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("rejects index without columns", () => {
|
|
620
|
+
const node: CreateIndexNode = {
|
|
621
|
+
kind: "CreateIndexNode",
|
|
622
|
+
name: IdentifierNode.create("idx_test"),
|
|
623
|
+
table: TableNode.create("users"),
|
|
624
|
+
columns: [],
|
|
625
|
+
};
|
|
626
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("rejects index with using clause", () => {
|
|
630
|
+
const node: CreateIndexNode = {
|
|
631
|
+
kind: "CreateIndexNode",
|
|
632
|
+
name: IdentifierNode.create("idx_test"),
|
|
633
|
+
table: TableNode.create("users"),
|
|
634
|
+
columns: [ColumnNode.create("email")],
|
|
635
|
+
using: RawNode.createWithSql("gin"),
|
|
636
|
+
};
|
|
637
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("rejects index with where clause", () => {
|
|
641
|
+
// SAFETY: Cast mock where node to WhereNode for test assertion.
|
|
642
|
+
const mockWhere = {
|
|
643
|
+
kind: "WhereNode",
|
|
644
|
+
where: RawNode.createWithSql("active = true"),
|
|
645
|
+
} as any;
|
|
646
|
+
const node: CreateIndexNode = {
|
|
647
|
+
kind: "CreateIndexNode",
|
|
648
|
+
name: IdentifierNode.create("idx_test"),
|
|
649
|
+
table: TableNode.create("users"),
|
|
650
|
+
columns: [ColumnNode.create("email")],
|
|
651
|
+
where: mockWhere,
|
|
652
|
+
};
|
|
653
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("rejects index with ordered columns (ASC/DESC)", () => {
|
|
657
|
+
const orderedCol = OrderByItemNode.create(
|
|
658
|
+
ColumnNode.create("email"),
|
|
659
|
+
RawNode.createWithSql("asc")
|
|
660
|
+
);
|
|
661
|
+
const node: CreateIndexNode = {
|
|
662
|
+
kind: "CreateIndexNode",
|
|
663
|
+
name: IdentifierNode.create("idx_test"),
|
|
664
|
+
table: TableNode.create("users"),
|
|
665
|
+
columns: [orderedCol],
|
|
666
|
+
};
|
|
667
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("rejects index with raw expression columns", () => {
|
|
671
|
+
const rawCol = RawNode.createWithSql("lower(email)");
|
|
672
|
+
const node: CreateIndexNode = {
|
|
673
|
+
kind: "CreateIndexNode",
|
|
674
|
+
name: IdentifierNode.create("idx_test"),
|
|
675
|
+
table: TableNode.create("users"),
|
|
676
|
+
columns: [rawCol],
|
|
677
|
+
};
|
|
678
|
+
expect(() => compileNode(node)).toThrow(DSQLQueryCompilerError);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("emits deterministic CREATE INDEX ASYNC", () => {
|
|
682
|
+
const node: CreateIndexNode = {
|
|
683
|
+
kind: "CreateIndexNode",
|
|
684
|
+
name: IdentifierNode.create("idx_users_email"),
|
|
685
|
+
table: TableNode.create("users"),
|
|
686
|
+
columns: [ColumnNode.create("email")],
|
|
687
|
+
};
|
|
688
|
+
expect(compileNode(node)).toBe(
|
|
689
|
+
'create index async "idx_users_email" on "users" ("email")'
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("emits deterministic CREATE UNIQUE INDEX ASYNC IF NOT EXISTS", () => {
|
|
694
|
+
const node: CreateIndexNode = {
|
|
695
|
+
kind: "CreateIndexNode",
|
|
696
|
+
name: IdentifierNode.create("idx_users_email"),
|
|
697
|
+
table: TableNode.create("users"),
|
|
698
|
+
columns: [ColumnNode.create("email")],
|
|
699
|
+
unique: true,
|
|
700
|
+
ifNotExists: true,
|
|
701
|
+
};
|
|
702
|
+
expect(compileNode(node)).toBe(
|
|
703
|
+
'create unique index async if not exists "idx_users_email" on "users" ("email")'
|
|
704
|
+
);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("emits deterministic index with NULLS NOT DISTINCT", () => {
|
|
708
|
+
const node: CreateIndexNode = {
|
|
709
|
+
kind: "CreateIndexNode",
|
|
710
|
+
name: IdentifierNode.create("idx_users_email"),
|
|
711
|
+
table: TableNode.create("users"),
|
|
712
|
+
columns: [ColumnNode.create("email")],
|
|
713
|
+
nullsNotDistinct: true,
|
|
714
|
+
};
|
|
715
|
+
expect(compileNode(node)).toBe(
|
|
716
|
+
'create index async "idx_users_email" on "users" ("email") nulls not distinct'
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
describe("Kysely runtime integration and backstop", () => {
|
|
722
|
+
it("validates direct sql.raw queries via compiler", async () => {
|
|
723
|
+
const driver = new SpyDriver();
|
|
724
|
+
const testDb = createTestDb(driver);
|
|
725
|
+
|
|
726
|
+
await expect(
|
|
727
|
+
sql.raw("TRUNCATE TABLE users;").execute(testDb)
|
|
728
|
+
).rejects.toThrow(DSQLQueryCompilerError);
|
|
729
|
+
|
|
730
|
+
expect(driver.acquireConnection).not.toHaveBeenCalled();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("validates queries after withoutPlugins()", async () => {
|
|
734
|
+
const driver = new SpyDriver();
|
|
735
|
+
const testDb = createTestDb(driver);
|
|
736
|
+
const bareDb = testDb.withoutPlugins();
|
|
737
|
+
|
|
738
|
+
await expect(sql.raw("VACUUM users;").execute(bareDb)).rejects.toThrow(
|
|
739
|
+
DSQLQueryCompilerError
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
expect(driver.acquireConnection).not.toHaveBeenCalled();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("executes supported queries through driver", async () => {
|
|
746
|
+
const driver = new SpyDriver();
|
|
747
|
+
const mockConn: DatabaseConnection = {
|
|
748
|
+
executeQuery: vi
|
|
749
|
+
.fn<() => Promise<QueryResult<any>>>()
|
|
750
|
+
.mockResolvedValue({ rows: [] }),
|
|
751
|
+
// oxlint-disable-next-line require-yield
|
|
752
|
+
streamQuery: vi.fn<() => AsyncIterableIterator<QueryResult<any>>>(
|
|
753
|
+
async function* streamQuery() {}
|
|
754
|
+
),
|
|
755
|
+
};
|
|
756
|
+
driver.acquireConnection.mockResolvedValue(mockConn);
|
|
757
|
+
const testDb = createTestDb(driver);
|
|
758
|
+
|
|
759
|
+
const result = await testDb.selectFrom("person").selectAll().execute();
|
|
760
|
+
expect(result).toStrictEqual([]);
|
|
761
|
+
expect(driver.acquireConnection).toHaveBeenCalledOnce();
|
|
762
|
+
expect(mockConn.executeQuery).toHaveBeenCalledOnce();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
});
|