@getstrata/core 2.0.6 → 2.0.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.0.8
4
+
5
+ - fix(schema): preserve constraints and generate builder migrations
6
+
7
+ ## 2.0.7
8
+
9
+ - Fix concurrent Postgres role provisioning across databases
10
+
3
11
  ## 2.0.6
4
12
 
5
13
  - Dispatch registered Bun route preflights through CORS
@@ -7,12 +7,25 @@ interface IndexDefinition {
7
7
  where?: string;
8
8
  order?: "asc" | "desc";
9
9
  }
10
+ interface TableForeignKeyDefinition {
11
+ columns: string[];
12
+ referencesTable: string;
13
+ referencesColumns: string[];
14
+ name?: string;
15
+ onDelete?: "cascade" | "set null" | "restrict";
16
+ }
17
+ interface TableCheckDefinition {
18
+ expression: string;
19
+ name?: string;
20
+ }
10
21
  type BlueprintAction = "create" | "alter" | "drop";
11
22
  declare class Blueprint {
12
23
  readonly table: string;
13
24
  readonly action: BlueprintAction;
14
25
  readonly columns: ColumnDefinition[];
15
26
  readonly indexes: IndexDefinition[];
27
+ readonly foreignKeys: TableForeignKeyDefinition[];
28
+ readonly checks: TableCheckDefinition[];
16
29
  readonly droppedColumns: string[];
17
30
  readonly droppedIndexes: string[];
18
31
  constructor(table: string, action: BlueprintAction);
@@ -32,6 +45,11 @@ declare class Blueprint {
32
45
  dropSoftDeletes(): void;
33
46
  dropIndex(name: string): void;
34
47
  unique(columns: string | string[], name?: string): void;
48
+ foreignKey(columns: string[], referencesTable: string, referencesColumns: string[], options?: {
49
+ name?: string;
50
+ onDelete?: "cascade" | "set null" | "restrict";
51
+ }): void;
52
+ check(expression: string, name?: string): void;
35
53
  index(columns: string | string[], options?: {
36
54
  name?: string;
37
55
  order?: "asc" | "desc";
@@ -43,5 +61,5 @@ declare class Blueprint {
43
61
  fullText(columns: string | string[], name?: string): void;
44
62
  ginIndex(column: string, name?: string): void;
45
63
  }
46
- export type { BlueprintAction, IndexDefinition, IndexKind };
64
+ export type { BlueprintAction, IndexDefinition, IndexKind, TableCheckDefinition, TableForeignKeyDefinition, };
47
65
  export { Blueprint };
@@ -3,6 +3,6 @@ import type { ColumnDefinition } from "../columnDefinition.ts";
3
3
  import type { DatabaseDriver } from "../driver.ts";
4
4
  declare function compileDropTable(driver: DatabaseDriver, tableName: string): string[];
5
5
  declare function compileColumn(driver: DatabaseDriver, column: ColumnDefinition, mode: "create" | "alter"): string;
6
- declare function compileIndex(_driver: DatabaseDriver, tableName: string, index: IndexDefinition): string;
6
+ declare function compileIndex(driver: DatabaseDriver, tableName: string, index: IndexDefinition): string;
7
7
  declare function compileBlueprint(driver: DatabaseDriver, blueprint: Blueprint): string[];
8
8
  export { compileBlueprint, compileColumn, compileDropTable, compileIndex };
@@ -1,4 +1,4 @@
1
- export type { BlueprintAction, IndexDefinition, IndexKind } from "./blueprint.ts";
1
+ export type { BlueprintAction, IndexDefinition, IndexKind, TableCheckDefinition, TableForeignKeyDefinition, } from "./blueprint.ts";
2
2
  export { Blueprint } from "./blueprint.ts";
3
3
  export type { ColumnKind, ForeignKeyOptions } from "./columnDefinition.ts";
4
4
  export { ColumnDefinition, ForeignIdColumnDefinition, inferReferencedTable, } from "./columnDefinition.ts";
@@ -52,6 +52,181 @@ function resolveDatabaseDriver(options = {}) {
52
52
  }
53
53
  var init_driver = () => {};
54
54
 
55
+ // ../../src/core/runtime/asyncContextStore.ts
56
+ import { AsyncLocalStorage } from "async_hooks";
57
+ function createAsyncContextStore(key) {
58
+ const symbol = Symbol.for(key);
59
+ const globalRecord = globalThis;
60
+ const existing = globalRecord[symbol];
61
+ if (existing) {
62
+ return existing;
63
+ }
64
+ const store = new AsyncLocalStorage;
65
+ globalRecord[symbol] = store;
66
+ return store;
67
+ }
68
+ var init_asyncContextStore = () => {};
69
+
70
+ // ../../src/core/database/dialect.ts
71
+ function assertSafeIdentifier(identifier) {
72
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
73
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
74
+ }
75
+ return identifier;
76
+ }
77
+ function quoteIdentifierFor(dialect, column) {
78
+ return dialect.quoteIdentifier(column);
79
+ }
80
+ function dialectFor(driver) {
81
+ return dialects[driver];
82
+ }
83
+ function currentSqlDialect() {
84
+ return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
85
+ }
86
+ var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
87
+ var init_dialect = __esm(() => {
88
+ init_asyncContextStore();
89
+ init_driver();
90
+ postgresDialect = {
91
+ driver: "pgsql",
92
+ placeholder(index) {
93
+ return `$${index}`;
94
+ },
95
+ quoteIdentifier(identifier) {
96
+ return `"${assertSafeIdentifier(identifier)}"`;
97
+ },
98
+ nowExpression() {
99
+ return "NOW()";
100
+ },
101
+ epochMillisecondsExpression() {
102
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
103
+ },
104
+ rowLockClause(skipLocked = false) {
105
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
106
+ },
107
+ timestampValue(value) {
108
+ return value.toISOString();
109
+ },
110
+ returningClause(columns) {
111
+ return ` RETURNING ${columns}`;
112
+ },
113
+ ilikeOperator() {
114
+ return "ILIKE";
115
+ },
116
+ nullsLastSuffix() {
117
+ return " NULLS LAST";
118
+ },
119
+ castToText(expression) {
120
+ return `${expression}::text`;
121
+ },
122
+ upsertSuffix(conflictColumns, updateColumns) {
123
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
124
+ if (updateColumns.length === 0) {
125
+ return ` ON CONFLICT (${target}) DO NOTHING`;
126
+ }
127
+ const assignments = updateColumns.map((column) => {
128
+ const quoted = quoteIdentifierFor(this, column);
129
+ return `${quoted} = excluded.${quoted}`;
130
+ }).join(", ");
131
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
132
+ }
133
+ };
134
+ mysqlDialect = {
135
+ driver: "mysql",
136
+ placeholder() {
137
+ return "?";
138
+ },
139
+ quoteIdentifier(identifier) {
140
+ return `\`${assertSafeIdentifier(identifier)}\``;
141
+ },
142
+ nowExpression() {
143
+ return "CURRENT_TIMESTAMP";
144
+ },
145
+ epochMillisecondsExpression() {
146
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
147
+ },
148
+ rowLockClause(skipLocked = false) {
149
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
150
+ },
151
+ timestampValue(value) {
152
+ return value.toISOString().slice(0, 19).replace("T", " ");
153
+ },
154
+ returningClause() {
155
+ return "";
156
+ },
157
+ ilikeOperator() {
158
+ return "LIKE";
159
+ },
160
+ nullsLastSuffix() {
161
+ return "";
162
+ },
163
+ castToText(expression) {
164
+ return `CAST(${expression} AS CHAR)`;
165
+ },
166
+ upsertSuffix(conflictColumns, updateColumns) {
167
+ if (updateColumns.length === 0) {
168
+ const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
169
+ return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
170
+ }
171
+ const assignments = updateColumns.map((column) => {
172
+ const quoted = quoteIdentifierFor(this, column);
173
+ return `${quoted} = VALUES(${quoted})`;
174
+ }).join(", ");
175
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
176
+ }
177
+ };
178
+ sqliteDialect = {
179
+ driver: "sqlite",
180
+ placeholder() {
181
+ return "?";
182
+ },
183
+ quoteIdentifier(identifier) {
184
+ return `"${assertSafeIdentifier(identifier)}"`;
185
+ },
186
+ nowExpression() {
187
+ return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
188
+ },
189
+ epochMillisecondsExpression() {
190
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
191
+ },
192
+ rowLockClause() {
193
+ return "";
194
+ },
195
+ timestampValue(value) {
196
+ return value.toISOString();
197
+ },
198
+ returningClause(columns) {
199
+ return ` RETURNING ${columns}`;
200
+ },
201
+ ilikeOperator() {
202
+ return "LIKE";
203
+ },
204
+ nullsLastSuffix() {
205
+ return "";
206
+ },
207
+ castToText(expression) {
208
+ return `CAST(${expression} AS TEXT)`;
209
+ },
210
+ upsertSuffix(conflictColumns, updateColumns) {
211
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
212
+ if (updateColumns.length === 0) {
213
+ return ` ON CONFLICT (${target}) DO NOTHING`;
214
+ }
215
+ const assignments = updateColumns.map((column) => {
216
+ const quoted = quoteIdentifierFor(this, column);
217
+ return `${quoted} = excluded.${quoted}`;
218
+ }).join(", ");
219
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
220
+ }
221
+ };
222
+ dialects = {
223
+ pgsql: postgresDialect,
224
+ mysql: mysqlDialect,
225
+ sqlite: sqliteDialect
226
+ };
227
+ dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
228
+ });
229
+
55
230
  // ../../src/core/database/schema/columnDefinition.ts
56
231
  class ColumnDefinition {
57
232
  name;
@@ -150,6 +325,8 @@ class Blueprint {
150
325
  action;
151
326
  columns = [];
152
327
  indexes = [];
328
+ foreignKeys = [];
329
+ checks = [];
153
330
  droppedColumns = [];
154
331
  droppedIndexes = [];
155
332
  constructor(table, action) {
@@ -164,6 +341,9 @@ class Blueprint {
164
341
  return column;
165
342
  }
166
343
  string(name, length) {
344
+ if (length !== undefined && (!Number.isSafeInteger(length) || length <= 0)) {
345
+ throw new Error("String length must be a positive safe integer.");
346
+ }
167
347
  const column = new ColumnDefinition(name, "string");
168
348
  column.length = length;
169
349
  column.notNullable();
@@ -241,6 +421,22 @@ class Blueprint {
241
421
  kind: "unique"
242
422
  });
243
423
  }
424
+ foreignKey(columns, referencesTable, referencesColumns, options = {}) {
425
+ if (!columns.length || columns.length !== referencesColumns.length) {
426
+ throw new Error("Foreign key columns must be nonempty and match referenced columns.");
427
+ }
428
+ this.foreignKeys.push({
429
+ columns: [...columns],
430
+ referencesTable,
431
+ referencesColumns: [...referencesColumns],
432
+ ...options
433
+ });
434
+ }
435
+ check(expression, name) {
436
+ if (!expression.trim())
437
+ throw new Error("Check expression must not be empty.");
438
+ this.checks.push({ expression, name });
439
+ }
244
440
  index(columns, options = {}) {
245
441
  this.indexes.push({
246
442
  name: options.name,
@@ -284,518 +480,8 @@ class UnsupportedSchemaFeatureError extends Error {
284
480
  this.name = "UnsupportedSchemaFeatureError";
285
481
  }
286
482
  }
287
- // ../../src/core/database/query.ts
288
- import { currentSqlDialect } from "@getstrata/core/database/dialect";
289
- function quoteIdentifier(identifier) {
290
- return currentSqlDialect().quoteIdentifier(identifier);
291
- }
292
- function returningSuffix(columns) {
293
- return currentSqlDialect().returningClause(columns);
294
- }
295
- function qualifyColumn(tableName, column) {
296
- return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
297
- }
298
- function resolveQualifiedColumn(defaultTable, columnName) {
299
- if (columnName.includes(".")) {
300
- const [table, column] = columnName.split(".", 2);
301
- if (!table || !column) {
302
- throw new Error(`Invalid qualified column: ${columnName}`);
303
- }
304
- return qualifyColumn(table, column);
305
- }
306
- return qualifyColumn(defaultTable, columnName);
307
- }
308
- function parseQualifiedColumn(reference) {
309
- const [table, column] = reference.split(".", 2);
310
- if (!table || !column) {
311
- throw new Error(`Join columns must be qualified as table.column: ${reference}`);
312
- }
313
- return { table, column };
314
- }
315
- function normalizeDirection(direction = "ASC") {
316
- return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
317
- }
318
- function isQueryOperator(value) {
319
- return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
320
- }
321
- function pushParam(values, value) {
322
- values.push(value);
323
- return currentSqlDialect().placeholder(values.length);
324
- }
325
- var SUPPORTED_OPERATORS = new Set([
326
- "eq",
327
- "ne",
328
- "in",
329
- "notIn",
330
- "gt",
331
- "gte",
332
- "lt",
333
- "lte",
334
- "isNull",
335
- "ilike",
336
- "tsMatch"
337
- ]);
338
- function buildInClause(column, values, params, negated = false) {
339
- if (values.length === 0) {
340
- return negated ? "1 = 1" : "1 = 0";
341
- }
342
- const placeholders = values.map((value) => pushParam(params, value)).join(", ");
343
- return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
344
- }
345
- function buildOperatorClauses(column, operator, params) {
346
- const clauses = [];
347
- const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
348
- if (unsupported.length > 0) {
349
- throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
350
- }
351
- if (operator.isNull === true) {
352
- clauses.push(`${column} IS NULL`);
353
- }
354
- if (operator.isNull === false) {
355
- clauses.push(`${column} IS NOT NULL`);
356
- }
357
- if (operator.eq !== undefined) {
358
- if (operator.eq === null) {
359
- clauses.push(`${column} IS NULL`);
360
- } else {
361
- clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
362
- }
363
- }
364
- if (operator.ne !== undefined) {
365
- if (operator.ne === null) {
366
- clauses.push(`${column} IS NOT NULL`);
367
- } else {
368
- clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
369
- }
370
- }
371
- if (operator.in !== undefined) {
372
- clauses.push(buildInClause(column, operator.in, params));
373
- }
374
- if (operator.notIn !== undefined) {
375
- clauses.push(buildInClause(column, operator.notIn, params, true));
376
- }
377
- if (operator.gt !== undefined) {
378
- clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
379
- }
380
- if (operator.gte !== undefined) {
381
- clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
382
- }
383
- if (operator.lt !== undefined) {
384
- clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
385
- }
386
- if (operator.lte !== undefined) {
387
- clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
388
- }
389
- if (operator.ilike !== undefined) {
390
- clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
391
- }
392
- if (operator.tsMatch !== undefined) {
393
- if (currentSqlDialect().driver !== "pgsql") {
394
- throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
395
- }
396
- clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
397
- }
398
- return clauses;
399
- }
400
- function appendWhereParts(tableName, where, params) {
401
- const clauses = [];
402
- for (const [columnName, filterValue] of Object.entries(where)) {
403
- if (filterValue === undefined) {
404
- continue;
405
- }
406
- const column = resolveQualifiedColumn(tableName, columnName);
407
- if (Array.isArray(filterValue)) {
408
- clauses.push(buildInClause(column, filterValue, params));
409
- continue;
410
- }
411
- if (isQueryOperator(filterValue)) {
412
- clauses.push(...buildOperatorClauses(column, filterValue, params));
413
- continue;
414
- }
415
- if (filterValue === null) {
416
- clauses.push(`${column} IS NULL`);
417
- continue;
418
- }
419
- clauses.push(`${column} = ${pushParam(params, filterValue)}`);
420
- }
421
- return clauses.join(" AND ");
422
- }
423
- function buildWhereClause(tableName, where = {}) {
424
- const params = [];
425
- const body = appendWhereParts(tableName, where, params);
426
- return {
427
- clause: body.length > 0 ? ` WHERE ${body}` : "",
428
- params
429
- };
430
- }
431
- function remapExistsSql(sql, existsParams, params) {
432
- const offset = params.length;
433
- params.push(...existsParams);
434
- return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
435
- }
436
- function buildWhereNodeClause(tableName, node, params) {
437
- if ("exists" in node) {
438
- const body = remapExistsSql(node.exists.sql, node.exists.params, params);
439
- return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
440
- }
441
- if ("where" in node) {
442
- return appendWhereParts(tableName, node.where, params);
443
- }
444
- const grouped = buildWhereGroupClause(tableName, node.group, params);
445
- if (!grouped) {
446
- return "";
447
- }
448
- return grouped.includes(" OR ") ? `(${grouped})` : grouped;
449
- }
450
- function buildWhereGroupClause(tableName, nodes, params) {
451
- let result = "";
452
- for (const node of nodes) {
453
- const part = buildWhereNodeClause(tableName, node, params);
454
- if (!part) {
455
- continue;
456
- }
457
- if (!result) {
458
- result = part;
459
- continue;
460
- }
461
- result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
462
- }
463
- if (!result) {
464
- return "";
465
- }
466
- return result;
467
- }
468
- function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
469
- const nodes = [];
470
- if (Object.keys(where).length > 0) {
471
- nodes.push({ kind: "and", where });
472
- }
473
- nodes.push(...whereNodes);
474
- const combined = buildWhereGroupClause(tableName, nodes, params);
475
- return {
476
- clause: combined ? ` WHERE ${combined}` : "",
477
- params
478
- };
479
- }
480
- function resolveSoftDeleteColumn(table) {
481
- if (!table.softDeletes) {
482
- return null;
483
- }
484
- if (table.softDeletes === true) {
485
- return "deleted_at";
486
- }
487
- return table.softDeletes.column ?? "deleted_at";
488
- }
489
- function appendSoftDeleteScope(table, options, clauses) {
490
- const column = resolveSoftDeleteColumn(table);
491
- if (!column) {
492
- return;
493
- }
494
- const qualifiedColumn = qualifyColumn(table.name, column);
495
- if (options.onlyTrashed) {
496
- clauses.push(`${qualifiedColumn} IS NOT NULL`);
497
- return;
498
- }
499
- if (!options.withTrashed) {
500
- clauses.push(`${qualifiedColumn} IS NULL`);
501
- }
502
- }
503
- function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
504
- const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
505
- const softDeleteClauses = [];
506
- appendSoftDeleteScope(table, options, softDeleteClauses);
507
- if (softDeleteClauses.length === 0) {
508
- return { clause, params: whereParams };
509
- }
510
- const base = clause.replace(/^ WHERE /, "");
511
- const scope = softDeleteClauses.join(" AND ");
512
- return {
513
- clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
514
- params: whereParams
515
- };
516
- }
517
- function isQueryOrder(value) {
518
- return "column" in value;
519
- }
520
- function normalizeOrderBy(orderBy) {
521
- if (!orderBy) {
522
- return [];
523
- }
524
- if (Array.isArray(orderBy)) {
525
- return orderBy;
526
- }
527
- if (isQueryOrder(orderBy)) {
528
- return [orderBy];
529
- }
530
- return Object.entries(orderBy).map(([column, direction]) => ({
531
- column,
532
- direction
533
- }));
534
- }
535
- function buildOrderByClause(tableName, orderBy) {
536
- const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
537
- return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
538
- });
539
- return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
540
- }
541
- function buildGroupByClause(tableName, groupBy) {
542
- if (!groupBy) {
543
- return "";
544
- }
545
- const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
546
- const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
547
- return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
548
- }
549
- function buildHavingClause(tableName, having, params) {
550
- if (!having) {
551
- return "";
552
- }
553
- const body = appendWhereParts(tableName, having, params);
554
- return body.length > 0 ? ` HAVING ${body}` : "";
555
- }
556
- function buildJoinClause(joins = []) {
557
- return joins.map((join) => {
558
- const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
559
- const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
560
- return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
561
- }).join("");
562
- }
563
- function buildLimitClause(limit) {
564
- if (limit === undefined) {
565
- return "";
566
- }
567
- if (!Number.isInteger(limit) || limit <= 0) {
568
- throw new Error("Query limit must be a positive integer.");
569
- }
570
- return ` LIMIT ${limit}`;
571
- }
572
- function buildOffsetClause(offset) {
573
- if (offset === undefined) {
574
- return "";
575
- }
576
- if (!Number.isInteger(offset) || offset < 0) {
577
- throw new Error("Query offset must be a non-negative integer.");
578
- }
579
- return ` OFFSET ${offset}`;
580
- }
581
- function buildReturningColumns(table) {
582
- return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
583
- }
584
- function buildSelectList(table, select, params = []) {
585
- if (!select || select.length === 0) {
586
- return buildReturningColumns(table);
587
- }
588
- return select.map((item) => {
589
- if (item.kind === "column") {
590
- const column = qualifyColumn(item.table, item.column);
591
- return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
592
- }
593
- if (item.kind === "literalText") {
594
- return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
595
- }
596
- if (item.kind === "subqueryCount") {
597
- const body = remapExistsSql(item.sql, item.params, params);
598
- return `(${body}) AS ${quoteIdentifier(item.as)}`;
599
- }
600
- const column = qualifyColumn(item.table, item.column);
601
- const placeholder = pushParam(params, item.query);
602
- return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
603
- }).join(", ");
604
- }
605
- function getDefinedColumnEntries(table, values, options = {}) {
606
- const record = values;
607
- const excluded = new Set(options.exclude ?? []);
608
- return table.columns.flatMap((column) => {
609
- if (excluded.has(column) || !Object.hasOwn(record, column)) {
610
- return [];
611
- }
612
- const value = record[column];
613
- if (value === undefined) {
614
- return [];
615
- }
616
- return [[column, value]];
617
- });
618
- }
619
- function buildSelectQuery(table, options = {}, whereNodes = []) {
620
- const params = [];
621
- const columns = buildSelectList(table, options.select, params);
622
- const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
623
- const joins = buildJoinClause(options.joins);
624
- const groupBy = buildGroupByClause(table.name, options.groupBy);
625
- const havingClause = buildHavingClause(table.name, options.having, params);
626
- const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
627
- const limit = buildLimitClause(options.limit);
628
- const offset = buildOffsetClause(options.offset);
629
- return {
630
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
631
- params
632
- };
633
- }
634
- function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
635
- const params = [];
636
- const { clause, params: whereParams } = buildQueryWhereClause(table, {
637
- where,
638
- withTrashed: options.withTrashed,
639
- onlyTrashed: options.onlyTrashed
640
- }, whereNodes);
641
- params.push(...whereParams);
642
- const joins = buildJoinClause(options.joins);
643
- const groupBy = buildGroupByClause(table.name, options.groupBy);
644
- return {
645
- text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
646
- params
647
- };
648
- }
649
- function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
650
- assertSafeProjectionExpression(expression);
651
- const params = [];
652
- const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
653
- params.push(...whereParams);
654
- const joins = buildJoinClause(options.joins);
655
- const groupBy = buildGroupByClause(table.name, options.groupBy);
656
- const orderBy = buildOrderByClause(table.name, options.orderBy);
657
- const limit = buildLimitClause(options.limit);
658
- return {
659
- text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
660
- params
661
- };
662
- }
663
- var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
664
- function assertSafeProjectionExpression(expression) {
665
- if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
666
- throw new Error(`Unsafe projection expression: ${expression}`);
667
- }
668
- }
669
- function buildGroupedCountQuery(table, column, where = {}, options = {}) {
670
- const qualifiedColumn = qualifyColumn(table.name, column);
671
- const { clause, params } = buildQueryWhereClause(table, {
672
- where,
673
- ...options
674
- });
675
- return {
676
- text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
677
- params
678
- };
679
- }
680
- function buildInsertQuery(table, values) {
681
- const entries = getDefinedColumnEntries(table, values);
682
- if (entries.length === 0) {
683
- throw new Error(`Cannot insert into ${table.name} without any column values.`);
684
- }
685
- const params = [];
686
- const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
687
- const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
688
- const returningColumns = buildReturningColumns(table);
689
- return {
690
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
691
- params
692
- };
693
- }
694
- function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
695
- const entries = getDefinedColumnEntries(table, values);
696
- if (entries.length === 0) {
697
- throw new Error(`Cannot upsert into ${table.name} without any column values.`);
698
- }
699
- if (conflictColumns.length === 0) {
700
- throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
701
- }
702
- const insertable = new Set(entries.map(([column]) => column));
703
- const conflict = new Set(conflictColumns);
704
- const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
705
- const params = [];
706
- const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
707
- const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
708
- const returningColumns = buildReturningColumns(table);
709
- const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
710
- return {
711
- text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
712
- params
713
- };
714
- }
715
- function buildIncrementQuery(table, id, column, amount, extra = {}) {
716
- if (!Number.isFinite(amount)) {
717
- throw new Error("Increment amount must be a finite number.");
718
- }
719
- if (!table.columns.includes(column)) {
720
- throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
721
- }
722
- const params = [];
723
- const target = quoteIdentifier(column);
724
- const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
725
- for (const [name, value] of getDefinedColumnEntries(table, extra, {
726
- exclude: [table.primaryKey, column]
727
- })) {
728
- assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
729
- }
730
- const primaryKeyPlaceholder = pushParam(params, id);
731
- const returningColumns = buildReturningColumns(table);
732
- const scopeClauses = [];
733
- appendSoftDeleteScope(table, {}, scopeClauses);
734
- const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
735
- return {
736
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
737
- params
738
- };
739
- }
740
- function buildUpdateQuery(table, id, changes) {
741
- const entries = getDefinedColumnEntries(table, changes, {
742
- exclude: [table.primaryKey]
743
- });
744
- if (entries.length === 0) {
745
- throw new Error(`Cannot update ${table.name} without any changed column values.`);
746
- }
747
- const params = [];
748
- const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
749
- const primaryKeyPlaceholder = pushParam(params, id);
750
- const returningColumns = buildReturningColumns(table);
751
- const scopeClauses = [];
752
- appendSoftDeleteScope(table, {}, scopeClauses);
753
- const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
754
- return {
755
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
756
- params
757
- };
758
- }
759
- function buildSoftDeleteByIdQuery(table, id, deletedAt) {
760
- const deletedAtColumn = resolveSoftDeleteColumn(table);
761
- if (!deletedAtColumn) {
762
- throw new Error(`Table ${table.name} does not support soft deletes.`);
763
- }
764
- const returningColumns = buildReturningColumns(table);
765
- const scopeClauses = [];
766
- appendSoftDeleteScope(table, {}, scopeClauses);
767
- const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
768
- const params = [];
769
- const deletedAtPlaceholder = pushParam(params, deletedAt);
770
- const idPlaceholder = pushParam(params, id);
771
- return {
772
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
773
- params
774
- };
775
- }
776
- function buildRestoreByIdQuery(table, id) {
777
- const deletedAtColumn = resolveSoftDeleteColumn(table);
778
- if (!deletedAtColumn) {
779
- throw new Error(`Table ${table.name} does not support soft deletes.`);
780
- }
781
- const returningColumns = buildReturningColumns(table);
782
- const params = [];
783
- const deletedAtPlaceholder = pushParam(params, null);
784
- const idPlaceholder = pushParam(params, id);
785
- return {
786
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
787
- params
788
- };
789
- }
790
- function buildDeleteByIdQuery(table, id) {
791
- const params = [];
792
- const idPlaceholder = pushParam(params, id);
793
- const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
794
- return {
795
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
796
- params
797
- };
798
- }
483
+ // ../../src/core/database/schema/grammars/compileStatements.ts
484
+ init_dialect();
799
485
 
800
486
  // ../../src/core/database/schema/grammars/grammar.ts
801
487
  function compileColumnType(driver, column) {
@@ -836,7 +522,7 @@ function compileIdType(driver) {
836
522
  function compileStringType(driver, length) {
837
523
  switch (driver) {
838
524
  case "pgsql":
839
- return "TEXT";
525
+ return length ? `VARCHAR(${length})` : "TEXT";
840
526
  case "mysql":
841
527
  return length ? `VARCHAR(${length})` : "VARCHAR(255)";
842
528
  case "sqlite":
@@ -914,15 +600,30 @@ function compileJsonbType(driver) {
914
600
  }
915
601
 
916
602
  // ../../src/core/database/schema/grammars/compileStatements.ts
603
+ function compileTableConstraints(driver, blueprint) {
604
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
605
+ const named = (name) => name ? `CONSTRAINT ${quoteIdentifier(name)} ` : "";
606
+ const columns = (names) => names.map(quoteIdentifier).join(", ");
607
+ return [
608
+ ...blueprint.foreignKeys.map((key) => {
609
+ const deletion = key.onDelete ? ` ON DELETE ${key.onDelete.toUpperCase()}` : "";
610
+ return `${named(key.name)}FOREIGN KEY (${columns(key.columns)}) REFERENCES ${quoteIdentifier(key.referencesTable)}(${columns(key.referencesColumns)})${deletion}`;
611
+ }),
612
+ ...blueprint.checks.map((check) => `${named(check.name)}CHECK (${check.expression})`)
613
+ ];
614
+ }
917
615
  function compileCreateTable(driver, blueprint) {
616
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
918
617
  const table = quoteIdentifier(blueprint.table);
919
618
  const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
920
619
  for (const index of blueprint.indexes) {
921
- if (index.kind === "unique" && index.columns.length > 1) {
620
+ if (index.kind === "unique") {
922
621
  const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
923
- parts.push(`UNIQUE (${columns})`);
622
+ const name = index.name ? `CONSTRAINT ${quoteIdentifier(index.name)} ` : "";
623
+ parts.push(`${name}UNIQUE (${columns})`);
924
624
  }
925
625
  }
626
+ parts.push(...compileTableConstraints(driver, blueprint));
926
627
  const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
927
628
  ${parts.join(`,
928
629
  `)}
@@ -940,6 +641,7 @@ function compileCreateTable(driver, blueprint) {
940
641
  return statements;
941
642
  }
942
643
  function compileAlterTable(driver, blueprint) {
644
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
943
645
  const statements = [];
944
646
  const table = quoteIdentifier(blueprint.table);
945
647
  for (const column of blueprint.columns) {
@@ -947,12 +649,20 @@ function compileAlterTable(driver, blueprint) {
947
649
  statements.push(`ALTER TABLE ${table}
948
650
  ${addPrefix} ${compileColumn(driver, column, "alter")}`);
949
651
  }
652
+ if (blueprint.foreignKeys.length || blueprint.checks.length) {
653
+ if (driver === "sqlite") {
654
+ throw new UnsupportedSchemaFeatureError("adding table constraints; rebuild the SQLite table", driver);
655
+ }
656
+ for (const constraint of compileTableConstraints(driver, blueprint)) {
657
+ statements.push(`ALTER TABLE ${table} ADD ${constraint}`);
658
+ }
659
+ }
950
660
  for (const columnName of blueprint.droppedColumns) {
951
661
  const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
952
662
  statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
953
663
  }
954
664
  for (const indexName of blueprint.droppedIndexes) {
955
- statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
665
+ statements.push(driver === "mysql" ? `DROP INDEX ${quoteIdentifier(indexName)} ON ${table}` : `DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
956
666
  }
957
667
  for (const index of blueprint.indexes) {
958
668
  if (index.kind === "index" || index.kind === "unique") {
@@ -964,10 +674,12 @@ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
964
674
  return statements;
965
675
  }
966
676
  function compileDropTable(driver, tableName) {
677
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
967
678
  const cascade = driver === "pgsql" ? " CASCADE" : "";
968
679
  return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
969
680
  }
970
681
  function compileColumn(driver, column, mode) {
682
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
971
683
  const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
972
684
  if (column.autoIncrement && driver === "mysql") {
973
685
  parts[1] = `${parts[1]} AUTO_INCREMENT`;
@@ -1005,7 +717,8 @@ function compileColumn(driver, column, mode) {
1005
717
  }
1006
718
  return parts.join(" ");
1007
719
  }
1008
- function compileIndex(_driver, tableName, index) {
720
+ function compileIndex(driver, tableName, index) {
721
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
1009
722
  const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
1010
723
  const columns = index.columns.map((column) => {
1011
724
  const quoted = quoteIdentifier(column);
@@ -1015,9 +728,11 @@ function compileIndex(_driver, tableName, index) {
1015
728
  return quoted;
1016
729
  }).join(", ");
1017
730
  const unique = index.kind === "unique" ? "UNIQUE " : "";
1018
- return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
731
+ const existence = driver === "mysql" ? "" : "IF NOT EXISTS ";
732
+ return `CREATE ${unique}INDEX ${existence}${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
1019
733
  }
1020
734
  function compileSpecialIndex(driver, tableName, index) {
735
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
1021
736
  const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
1022
737
  const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
1023
738
  switch (index.kind) {
@@ -175,6 +175,9 @@ function postgresAppRoleCreateSql(options = {}) {
175
175
  const password = quoteSqlLiteral(options.password ?? POSTGRES_APP_ROLE_PASSWORD);
176
176
  return `DO $$
177
177
  BEGIN
178
+ -- pg_authid is shared across databases. Database-local advisory locks cannot
179
+ -- serialize a cluster-global role. This lock permits readers and lasts until commit.
180
+ LOCK TABLE pg_catalog.pg_authid IN SHARE ROW EXCLUSIVE MODE;
178
181
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
179
182
  CREATE ROLE ${role} LOGIN PASSWORD ${password}
180
183
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
@@ -216,8 +219,7 @@ async function grantPostgresAppRolePrivileges(sql, options) {
216
219
  await sql.unsafe(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO ${role}`);
217
220
  }
218
221
  async function ensurePostgresAppRole(sql, options) {
219
- await sql.unsafe(postgresAppRoleCreateSql(options));
220
- await grantPostgresAppRolePrivileges(sql, options);
222
+ await sql.unsafe(postgresAppRoleSql(options));
221
223
  }
222
224
  async function ensurePostgresDatabaseAndAppRole(options) {
223
225
  const name = postgresDatabaseNameFromUrl(options.runtimeUrl);
package/dist/index.js CHANGED
@@ -7733,6 +7733,8 @@ class Blueprint {
7733
7733
  action;
7734
7734
  columns = [];
7735
7735
  indexes = [];
7736
+ foreignKeys = [];
7737
+ checks = [];
7736
7738
  droppedColumns = [];
7737
7739
  droppedIndexes = [];
7738
7740
  constructor(table, action) {
@@ -7747,6 +7749,9 @@ class Blueprint {
7747
7749
  return column;
7748
7750
  }
7749
7751
  string(name, length) {
7752
+ if (length !== undefined && (!Number.isSafeInteger(length) || length <= 0)) {
7753
+ throw new Error("String length must be a positive safe integer.");
7754
+ }
7750
7755
  const column = new ColumnDefinition(name, "string");
7751
7756
  column.length = length;
7752
7757
  column.notNullable();
@@ -7824,6 +7829,22 @@ class Blueprint {
7824
7829
  kind: "unique"
7825
7830
  });
7826
7831
  }
7832
+ foreignKey(columns, referencesTable, referencesColumns, options = {}) {
7833
+ if (!columns.length || columns.length !== referencesColumns.length) {
7834
+ throw new Error("Foreign key columns must be nonempty and match referenced columns.");
7835
+ }
7836
+ this.foreignKeys.push({
7837
+ columns: [...columns],
7838
+ referencesTable,
7839
+ referencesColumns: [...referencesColumns],
7840
+ ...options
7841
+ });
7842
+ }
7843
+ check(expression, name) {
7844
+ if (!expression.trim())
7845
+ throw new Error("Check expression must not be empty.");
7846
+ this.checks.push({ expression, name });
7847
+ }
7827
7848
  index(columns, options = {}) {
7828
7849
  this.indexes.push({
7829
7850
  name: options.name,
@@ -7867,6 +7888,9 @@ class UnsupportedSchemaFeatureError extends Error {
7867
7888
  this.name = "UnsupportedSchemaFeatureError";
7868
7889
  }
7869
7890
  }
7891
+ // ../../src/core/database/schema/grammars/compileStatements.ts
7892
+ init_dialect();
7893
+
7870
7894
  // ../../src/core/database/schema/grammars/grammar.ts
7871
7895
  function compileColumnType(driver, column) {
7872
7896
  switch (column.kind) {
@@ -7906,7 +7930,7 @@ function compileIdType(driver) {
7906
7930
  function compileStringType(driver, length) {
7907
7931
  switch (driver) {
7908
7932
  case "pgsql":
7909
- return "TEXT";
7933
+ return length ? `VARCHAR(${length})` : "TEXT";
7910
7934
  case "mysql":
7911
7935
  return length ? `VARCHAR(${length})` : "VARCHAR(255)";
7912
7936
  case "sqlite":
@@ -7984,15 +8008,30 @@ function compileJsonbType(driver) {
7984
8008
  }
7985
8009
 
7986
8010
  // ../../src/core/database/schema/grammars/compileStatements.ts
8011
+ function compileTableConstraints(driver, blueprint) {
8012
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8013
+ const named = (name) => name ? `CONSTRAINT ${quoteIdentifier(name)} ` : "";
8014
+ const columns = (names) => names.map(quoteIdentifier).join(", ");
8015
+ return [
8016
+ ...blueprint.foreignKeys.map((key) => {
8017
+ const deletion = key.onDelete ? ` ON DELETE ${key.onDelete.toUpperCase()}` : "";
8018
+ return `${named(key.name)}FOREIGN KEY (${columns(key.columns)}) REFERENCES ${quoteIdentifier(key.referencesTable)}(${columns(key.referencesColumns)})${deletion}`;
8019
+ }),
8020
+ ...blueprint.checks.map((check) => `${named(check.name)}CHECK (${check.expression})`)
8021
+ ];
8022
+ }
7987
8023
  function compileCreateTable(driver, blueprint) {
8024
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
7988
8025
  const table = quoteIdentifier(blueprint.table);
7989
8026
  const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
7990
8027
  for (const index of blueprint.indexes) {
7991
- if (index.kind === "unique" && index.columns.length > 1) {
8028
+ if (index.kind === "unique") {
7992
8029
  const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
7993
- parts.push(`UNIQUE (${columns})`);
8030
+ const name = index.name ? `CONSTRAINT ${quoteIdentifier(index.name)} ` : "";
8031
+ parts.push(`${name}UNIQUE (${columns})`);
7994
8032
  }
7995
8033
  }
8034
+ parts.push(...compileTableConstraints(driver, blueprint));
7996
8035
  const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
7997
8036
  ${parts.join(`,
7998
8037
  `)}
@@ -8010,6 +8049,7 @@ function compileCreateTable(driver, blueprint) {
8010
8049
  return statements;
8011
8050
  }
8012
8051
  function compileAlterTable(driver, blueprint) {
8052
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8013
8053
  const statements = [];
8014
8054
  const table = quoteIdentifier(blueprint.table);
8015
8055
  for (const column of blueprint.columns) {
@@ -8017,12 +8057,20 @@ function compileAlterTable(driver, blueprint) {
8017
8057
  statements.push(`ALTER TABLE ${table}
8018
8058
  ${addPrefix} ${compileColumn(driver, column, "alter")}`);
8019
8059
  }
8060
+ if (blueprint.foreignKeys.length || blueprint.checks.length) {
8061
+ if (driver === "sqlite") {
8062
+ throw new UnsupportedSchemaFeatureError("adding table constraints; rebuild the SQLite table", driver);
8063
+ }
8064
+ for (const constraint of compileTableConstraints(driver, blueprint)) {
8065
+ statements.push(`ALTER TABLE ${table} ADD ${constraint}`);
8066
+ }
8067
+ }
8020
8068
  for (const columnName of blueprint.droppedColumns) {
8021
8069
  const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
8022
8070
  statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
8023
8071
  }
8024
8072
  for (const indexName of blueprint.droppedIndexes) {
8025
- statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
8073
+ statements.push(driver === "mysql" ? `DROP INDEX ${quoteIdentifier(indexName)} ON ${table}` : `DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
8026
8074
  }
8027
8075
  for (const index of blueprint.indexes) {
8028
8076
  if (index.kind === "index" || index.kind === "unique") {
@@ -8034,10 +8082,12 @@ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
8034
8082
  return statements;
8035
8083
  }
8036
8084
  function compileDropTable(driver, tableName) {
8085
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8037
8086
  const cascade = driver === "pgsql" ? " CASCADE" : "";
8038
8087
  return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
8039
8088
  }
8040
8089
  function compileColumn(driver, column, mode) {
8090
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8041
8091
  const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
8042
8092
  if (column.autoIncrement && driver === "mysql") {
8043
8093
  parts[1] = `${parts[1]} AUTO_INCREMENT`;
@@ -8075,7 +8125,8 @@ function compileColumn(driver, column, mode) {
8075
8125
  }
8076
8126
  return parts.join(" ");
8077
8127
  }
8078
- function compileIndex(_driver, tableName, index) {
8128
+ function compileIndex(driver, tableName, index) {
8129
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8079
8130
  const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
8080
8131
  const columns = index.columns.map((column) => {
8081
8132
  const quoted = quoteIdentifier(column);
@@ -8085,9 +8136,11 @@ function compileIndex(_driver, tableName, index) {
8085
8136
  return quoted;
8086
8137
  }).join(", ");
8087
8138
  const unique = index.kind === "unique" ? "UNIQUE " : "";
8088
- return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
8139
+ const existence = driver === "mysql" ? "" : "IF NOT EXISTS ";
8140
+ return `CREATE ${unique}INDEX ${existence}${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
8089
8141
  }
8090
8142
  function compileSpecialIndex(driver, tableName, index) {
8143
+ const quoteIdentifier = dialectFor(driver).quoteIdentifier;
8091
8144
  const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
8092
8145
  const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
8093
8146
  switch (index.kind) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",