tempest-db-js 0.4.0 → 0.6.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/README.md +13 -3
- package/dist/bin.cjs +604 -250
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-JR4MLFQN.js → chunk-G7O5DCCC.js} +1347 -289
- package/dist/chunk-G7O5DCCC.js.map +1 -0
- package/dist/{chunk-43XL66JG.js → chunk-KOW3LSWP.js} +209 -86
- package/dist/chunk-KOW3LSWP.js.map +1 -0
- package/dist/index.cjs +1357 -286
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +897 -42
- package/dist/index.d.ts +897 -42
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +336 -117
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +58 -5
- package/dist/migrations/index.d.ts +58 -5
- package/dist/migrations/index.js +2 -2
- package/package.json +6 -1
- package/dist/chunk-43XL66JG.js.map +0 -1
- package/dist/chunk-JR4MLFQN.js.map +0 -1
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var module$1 = require('module');
|
|
4
|
+
|
|
5
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
6
|
+
// src/expressions.ts
|
|
7
|
+
function renderPortableToken(token, dialect) {
|
|
8
|
+
switch (token) {
|
|
9
|
+
case "now":
|
|
10
|
+
return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
|
|
11
|
+
case "current_date":
|
|
12
|
+
return "CURRENT_DATE";
|
|
13
|
+
case "current_time":
|
|
14
|
+
return "CURRENT_TIME";
|
|
15
|
+
case "uuidv4":
|
|
16
|
+
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
17
|
+
if (dialect === "mysql") return "(UUID())";
|
|
18
|
+
return "(lower(hex(randomblob(16))))";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
22
|
+
function toAsyncDriver(driver) {
|
|
23
|
+
return {
|
|
24
|
+
async execute(sql, params) {
|
|
25
|
+
return await driver.execute(sql, params);
|
|
26
|
+
},
|
|
27
|
+
async close() {
|
|
28
|
+
await driver.close();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
// src/index.ts
|
|
4
34
|
function isDefaultValue(value) {
|
|
5
35
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
6
36
|
}
|
|
37
|
+
function bindsParameters(value) {
|
|
38
|
+
return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
|
|
39
|
+
}
|
|
7
40
|
function parseReference(ref, options) {
|
|
8
41
|
const dot = ref.lastIndexOf(".");
|
|
9
42
|
if (dot <= 0 || dot === ref.length - 1) {
|
|
@@ -17,48 +50,71 @@ function parseReference(ref, options) {
|
|
|
17
50
|
};
|
|
18
51
|
}
|
|
19
52
|
var Column = class _Column {
|
|
20
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
|
|
53
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
21
54
|
this.type = type;
|
|
22
55
|
this.flags = flags;
|
|
23
56
|
this.defaultValue = defaultValue;
|
|
24
57
|
this.onUpdateValue = onUpdateValue;
|
|
25
58
|
this.reference = reference;
|
|
59
|
+
this.dbName = dbName;
|
|
26
60
|
}
|
|
27
61
|
type;
|
|
28
62
|
flags;
|
|
29
63
|
defaultValue;
|
|
30
64
|
onUpdateValue;
|
|
31
65
|
reference;
|
|
32
|
-
|
|
66
|
+
dbName;
|
|
67
|
+
/** Clone this column with one facet replaced, carrying every other over. */
|
|
68
|
+
derive(patch) {
|
|
33
69
|
return new _Column(
|
|
34
70
|
this.type,
|
|
35
|
-
|
|
36
|
-
this.defaultValue,
|
|
37
|
-
this.onUpdateValue,
|
|
38
|
-
this.reference
|
|
71
|
+
patch.flags ?? this.flags,
|
|
72
|
+
patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
|
|
73
|
+
patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
|
|
74
|
+
patch.reference !== void 0 ? patch.reference : this.reference,
|
|
75
|
+
patch.dbName !== void 0 ? patch.dbName : this.dbName
|
|
39
76
|
);
|
|
40
77
|
}
|
|
78
|
+
primaryKey() {
|
|
79
|
+
return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
|
|
80
|
+
}
|
|
41
81
|
notNull() {
|
|
42
|
-
return
|
|
43
|
-
this.type,
|
|
44
|
-
{ ...this.flags, notNull: true },
|
|
45
|
-
this.defaultValue,
|
|
46
|
-
this.onUpdateValue,
|
|
47
|
-
this.reference
|
|
48
|
-
);
|
|
82
|
+
return this.derive({ flags: { ...this.flags, notNull: true } });
|
|
49
83
|
}
|
|
50
84
|
/**
|
|
51
85
|
* Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
|
|
52
86
|
* `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
|
|
53
87
|
*/
|
|
54
88
|
unique() {
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
return this.derive({ flags: { ...this.flags, unique: true } });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Map this property to a differently-named database column, à la SQLAlchemy's
|
|
93
|
+
* `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
|
|
94
|
+
*
|
|
95
|
+
* The override applies everywhere the name reaches SQL — select, insert,
|
|
96
|
+
* update, delete, where, order by, group by, returning, conflict targets, the
|
|
97
|
+
* migration IR and the drift check — while the TypeScript row keeps the
|
|
98
|
+
* property name. Use it to keep a `snake_case` schema behind a `camelCase`
|
|
99
|
+
* model; {@link Model.naming} does the same for a whole table at once.
|
|
100
|
+
*
|
|
101
|
+
* @param dbName The real column name in the database.
|
|
102
|
+
* @returns A new column bound to that name.
|
|
103
|
+
* @throws Error When `dbName` is empty.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* class ApiKey extends Model {
|
|
108
|
+
* static tablename = "api_keys";
|
|
109
|
+
* consumerName = column.text().name("consumer_name").notNull();
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
name(dbName) {
|
|
114
|
+
if (dbName.length === 0) {
|
|
115
|
+
throw new Error("column.name() requires a non-empty database column name.");
|
|
116
|
+
}
|
|
117
|
+
return this.derive({ dbName });
|
|
62
118
|
}
|
|
63
119
|
/**
|
|
64
120
|
* Declare a foreign-key reference to another table's column, à la SQLAlchemy's
|
|
@@ -71,37 +127,50 @@ var Column = class _Column {
|
|
|
71
127
|
* @throws Error When `ref` is not a valid `"table.column"` string.
|
|
72
128
|
*/
|
|
73
129
|
references(ref, options) {
|
|
74
|
-
return
|
|
75
|
-
this.type,
|
|
76
|
-
this.flags,
|
|
77
|
-
this.defaultValue,
|
|
78
|
-
this.onUpdateValue,
|
|
79
|
-
parseReference(ref, options)
|
|
80
|
-
);
|
|
130
|
+
return this.derive({ reference: parseReference(ref, options) });
|
|
81
131
|
}
|
|
82
132
|
/**
|
|
83
133
|
* Set the insert-time default: a constant value of type `T`, or a portable
|
|
84
134
|
* server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
|
|
135
|
+
*
|
|
136
|
+
* @param value The literal default, or a {@link sql} expression.
|
|
137
|
+
* @returns A new column carrying the default.
|
|
138
|
+
* @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
|
|
139
|
+
* nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
|
|
85
140
|
*/
|
|
86
141
|
default(value) {
|
|
87
142
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
if (bindsParameters(resolved)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return this.derive({
|
|
149
|
+
flags: { ...this.flags, hasDefault: true },
|
|
150
|
+
defaultValue: resolved
|
|
151
|
+
});
|
|
95
152
|
}
|
|
96
153
|
/**
|
|
97
154
|
* Re-apply a value whenever the row is updated (e.g. an `updated_at` column
|
|
98
155
|
* with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
|
|
156
|
+
*
|
|
157
|
+
* @param value The literal value, or a {@link sql} expression.
|
|
158
|
+
* @returns A new column carrying the on-update value.
|
|
159
|
+
* @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
|
|
99
160
|
*/
|
|
100
161
|
onUpdate(value) {
|
|
101
162
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
102
|
-
|
|
163
|
+
if (bindsParameters(resolved)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return this.derive({ onUpdateValue: resolved });
|
|
103
169
|
}
|
|
104
170
|
};
|
|
171
|
+
function toSnakeCase(name) {
|
|
172
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
173
|
+
}
|
|
105
174
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
106
175
|
function columnsOf(model) {
|
|
107
176
|
const cached = columnsCache.get(model);
|
|
@@ -116,16 +185,43 @@ function columnsOf(model) {
|
|
|
116
185
|
columnsCache.set(model, out);
|
|
117
186
|
return out;
|
|
118
187
|
}
|
|
188
|
+
var nameMapCache = /* @__PURE__ */ new WeakMap();
|
|
189
|
+
function columnNamesOf(model) {
|
|
190
|
+
const cached = nameMapCache.get(model);
|
|
191
|
+
if (cached !== void 0) return cached;
|
|
192
|
+
const strategy = model.naming ?? "preserve";
|
|
193
|
+
const map = {};
|
|
194
|
+
const seen = /* @__PURE__ */ new Map();
|
|
195
|
+
let renamed = false;
|
|
196
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
197
|
+
const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
|
|
198
|
+
const collision = seen.get(dbName);
|
|
199
|
+
if (collision !== void 0) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
seen.set(dbName, prop);
|
|
205
|
+
map[prop] = dbName;
|
|
206
|
+
if (dbName !== prop) renamed = true;
|
|
207
|
+
}
|
|
208
|
+
const result = renamed ? map : null;
|
|
209
|
+
nameMapCache.set(model, result);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
119
212
|
|
|
120
213
|
// src/migrations/ir.ts
|
|
121
214
|
function constraintName(prefix, table, columns) {
|
|
122
215
|
return `${prefix}_${table}_${columns.join("_")}`;
|
|
123
216
|
}
|
|
124
217
|
function reflectTable(model) {
|
|
218
|
+
const names = columnNamesOf(model);
|
|
219
|
+
const toColumn = (prop) => names?.[prop] ?? prop;
|
|
125
220
|
const columns = {};
|
|
126
221
|
const primaryKey = [];
|
|
127
|
-
for (const [
|
|
222
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
128
223
|
const isPk = col.flags.primaryKey;
|
|
224
|
+
const name = toColumn(prop);
|
|
129
225
|
columns[name] = {
|
|
130
226
|
name,
|
|
131
227
|
type: col.type,
|
|
@@ -140,15 +236,16 @@ function reflectTable(model) {
|
|
|
140
236
|
const uniqueConstraints = [];
|
|
141
237
|
const foreignKeys = [];
|
|
142
238
|
for (const c of model.tableArgs?.() ?? []) {
|
|
239
|
+
const cols = c.columns.map(toColumn);
|
|
143
240
|
if (c.kind === "unique") {
|
|
144
241
|
uniqueConstraints.push({
|
|
145
|
-
name: c.name ?? constraintName("uq", model.tablename,
|
|
146
|
-
columns:
|
|
242
|
+
name: c.name ?? constraintName("uq", model.tablename, cols),
|
|
243
|
+
columns: cols
|
|
147
244
|
});
|
|
148
245
|
} else {
|
|
149
246
|
foreignKeys.push({
|
|
150
|
-
name: c.name ?? constraintName("fk", model.tablename,
|
|
151
|
-
columns:
|
|
247
|
+
name: c.name ?? constraintName("fk", model.tablename, cols),
|
|
248
|
+
columns: cols,
|
|
152
249
|
refTable: c.refTable,
|
|
153
250
|
refColumns: c.refColumns,
|
|
154
251
|
onDelete: c.onDelete,
|
|
@@ -239,6 +336,8 @@ function renderColumnType(type, dialect) {
|
|
|
239
336
|
return "NUMERIC";
|
|
240
337
|
case "blob":
|
|
241
338
|
return "BLOB";
|
|
339
|
+
case "array":
|
|
340
|
+
throw new Error(unsupportedArray("sqlite"));
|
|
242
341
|
default:
|
|
243
342
|
return "TEXT";
|
|
244
343
|
}
|
|
@@ -280,6 +379,8 @@ function renderColumnType(type, dialect) {
|
|
|
280
379
|
return "CHAR(36)";
|
|
281
380
|
case "enum":
|
|
282
381
|
return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
|
|
382
|
+
case "array":
|
|
383
|
+
throw new Error(unsupportedArray("mysql"));
|
|
283
384
|
}
|
|
284
385
|
}
|
|
285
386
|
switch (kind) {
|
|
@@ -318,27 +419,39 @@ function renderColumnType(type, dialect) {
|
|
|
318
419
|
return "UUID";
|
|
319
420
|
case "enum":
|
|
320
421
|
return "TEXT";
|
|
422
|
+
case "array":
|
|
423
|
+
return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
|
|
321
424
|
}
|
|
322
425
|
}
|
|
323
|
-
function
|
|
426
|
+
function unsupportedArray(dialect) {
|
|
427
|
+
return `column.array() is PostgreSQL-only \u2014 ${dialect} has no native array type. Model the column as JSON there, accepting that array operators will not work.`;
|
|
428
|
+
}
|
|
429
|
+
function arrayElement(element) {
|
|
430
|
+
if (!element) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
"An array column has no element type \u2014 build it with column.array()."
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return element;
|
|
436
|
+
}
|
|
437
|
+
function renderDefault(def, dialect, type) {
|
|
324
438
|
if (def.kind === "expression") {
|
|
325
439
|
const expr = def.expression;
|
|
326
|
-
if (typeof expr === "object")
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return "CURRENT_DATE";
|
|
332
|
-
case "current_time":
|
|
333
|
-
return "CURRENT_TIME";
|
|
334
|
-
case "uuidv4":
|
|
335
|
-
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
336
|
-
if (dialect === "mysql") return "(UUID())";
|
|
337
|
-
return "(lower(hex(randomblob(16))))";
|
|
440
|
+
if (typeof expr === "object") {
|
|
441
|
+
if ("raw" in expr) return expr.raw;
|
|
442
|
+
throw new Error(
|
|
443
|
+
"sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
|
|
444
|
+
);
|
|
338
445
|
}
|
|
446
|
+
return renderPortableToken(expr, dialect);
|
|
339
447
|
}
|
|
340
448
|
const value = def.value;
|
|
341
449
|
if (value === null) return "NULL";
|
|
450
|
+
if (Array.isArray(value) && type?.kind === "array") {
|
|
451
|
+
const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
|
|
452
|
+
const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
|
|
453
|
+
return `ARRAY[${items.join(", ")}]::${elementType}[]`;
|
|
454
|
+
}
|
|
342
455
|
if (typeof value === "boolean") {
|
|
343
456
|
return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
|
|
344
457
|
}
|
|
@@ -384,7 +497,9 @@ function tableConstraintClauses(table, dialect) {
|
|
|
384
497
|
function renderColumnDef(col, dialect) {
|
|
385
498
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
386
499
|
if (col.notNull) sql += " NOT NULL";
|
|
387
|
-
if (col.default !== null)
|
|
500
|
+
if (col.default !== null) {
|
|
501
|
+
sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
|
|
502
|
+
}
|
|
388
503
|
sql += columnConstraintSuffix(col, dialect);
|
|
389
504
|
return sql;
|
|
390
505
|
}
|
|
@@ -408,7 +523,9 @@ function renderCreateTable(table, dialect) {
|
|
|
408
523
|
typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
|
|
409
524
|
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
410
525
|
if (c.notNull) def += " NOT NULL";
|
|
411
|
-
if (c.default !== null)
|
|
526
|
+
if (c.default !== null) {
|
|
527
|
+
def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
|
|
528
|
+
}
|
|
412
529
|
return def + columnConstraintSuffix(c, dialect);
|
|
413
530
|
}
|
|
414
531
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
@@ -552,7 +669,7 @@ function renderAlterColumn(table, to, dialect) {
|
|
|
552
669
|
to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
|
|
553
670
|
);
|
|
554
671
|
stmts.push(
|
|
555
|
-
to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
|
|
672
|
+
to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect, to.type)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
|
|
556
673
|
);
|
|
557
674
|
return stmts;
|
|
558
675
|
}
|
|
@@ -1041,62 +1158,26 @@ function affinityToKind(affinity) {
|
|
|
1041
1158
|
}
|
|
1042
1159
|
function sqliteForeignKeys(driver, table) {
|
|
1043
1160
|
const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
|
|
1044
|
-
|
|
1045
|
-
for (const r of rows) {
|
|
1046
|
-
const list = byId.get(r.id) ?? [];
|
|
1047
|
-
list.push(r);
|
|
1048
|
-
byId.set(r.id, list);
|
|
1049
|
-
}
|
|
1050
|
-
const fks = [];
|
|
1051
|
-
for (const group of byId.values()) {
|
|
1052
|
-
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
1053
|
-
const columns = ordered.map((r) => r.from);
|
|
1054
|
-
fks.push({
|
|
1055
|
-
name: `fk_${table}_${columns.join("_")}`,
|
|
1056
|
-
columns,
|
|
1057
|
-
refTable: ordered[0]?.table ?? "",
|
|
1058
|
-
refColumns: ordered.map((r) => r.to)
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
return fks;
|
|
1161
|
+
return sqliteForeignKeysFromPragma(table, rows);
|
|
1062
1162
|
}
|
|
1063
1163
|
function sqliteUniques(driver, table) {
|
|
1064
1164
|
const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
|
|
1065
1165
|
const uniques = [];
|
|
1066
1166
|
for (const idx of indexes) {
|
|
1067
|
-
if (
|
|
1068
|
-
const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1167
|
+
if (!isUniqueIndex(idx)) continue;
|
|
1168
|
+
const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows;
|
|
1169
|
+
const unique = sqliteUniqueFromPragma(table, cols);
|
|
1170
|
+
if (unique) uniques.push(unique);
|
|
1072
1171
|
}
|
|
1073
1172
|
return uniques;
|
|
1074
1173
|
}
|
|
1075
1174
|
function introspectSqlite(driver) {
|
|
1076
|
-
const tablesRows = driver.execute(
|
|
1077
|
-
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
1078
|
-
[]
|
|
1079
|
-
).rows;
|
|
1175
|
+
const tablesRows = driver.execute(SQLITE_TABLES_SQL, []).rows;
|
|
1080
1176
|
const tables = {};
|
|
1081
1177
|
for (const row of tablesRows) {
|
|
1082
1178
|
const tableName = String(row.name);
|
|
1083
1179
|
const info = driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, []).rows;
|
|
1084
|
-
const columns =
|
|
1085
|
-
const primaryKey = [];
|
|
1086
|
-
for (const col of info) {
|
|
1087
|
-
const isPk = Number(col.pk) > 0;
|
|
1088
|
-
const affinity = sqliteAffinity(col.type);
|
|
1089
|
-
columns[col.name] = {
|
|
1090
|
-
name: col.name,
|
|
1091
|
-
type: { kind: affinityToKind(affinity), meta: {} },
|
|
1092
|
-
notNull: Number(col.notnull) === 1 || isPk,
|
|
1093
|
-
primaryKey: isPk,
|
|
1094
|
-
default: null,
|
|
1095
|
-
unique: false,
|
|
1096
|
-
references: null
|
|
1097
|
-
};
|
|
1098
|
-
if (isPk) primaryKey.push(col.name);
|
|
1099
|
-
}
|
|
1180
|
+
const { columns, primaryKey } = sqliteColumnsFromPragma(info);
|
|
1100
1181
|
tables[tableName] = {
|
|
1101
1182
|
name: tableName,
|
|
1102
1183
|
columns,
|
|
@@ -1125,8 +1206,9 @@ function constraintKeys(table) {
|
|
|
1125
1206
|
return { fks, uniques };
|
|
1126
1207
|
}
|
|
1127
1208
|
function checkDrift(driver, models) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1209
|
+
return compareSqliteSchemas(introspectSqlite(driver), reflectSchema(models));
|
|
1210
|
+
}
|
|
1211
|
+
function compareSqliteSchemas(actual, expected) {
|
|
1130
1212
|
const issues = [];
|
|
1131
1213
|
for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
|
|
1132
1214
|
const actualTable = actual.tables[tableName];
|
|
@@ -1197,6 +1279,138 @@ function checkDrift(driver, models) {
|
|
|
1197
1279
|
}
|
|
1198
1280
|
return issues;
|
|
1199
1281
|
}
|
|
1282
|
+
function sqliteColumnsFromPragma(info) {
|
|
1283
|
+
const columns = {};
|
|
1284
|
+
const primaryKey = [];
|
|
1285
|
+
for (const col of info) {
|
|
1286
|
+
const isPk = Number(col.pk) > 0;
|
|
1287
|
+
columns[col.name] = {
|
|
1288
|
+
name: col.name,
|
|
1289
|
+
type: { kind: affinityToKind(sqliteAffinity(col.type)), meta: {} },
|
|
1290
|
+
notNull: Number(col.notnull) === 1 || isPk,
|
|
1291
|
+
primaryKey: isPk,
|
|
1292
|
+
default: null,
|
|
1293
|
+
unique: false,
|
|
1294
|
+
references: null
|
|
1295
|
+
};
|
|
1296
|
+
if (isPk) primaryKey.push(col.name);
|
|
1297
|
+
}
|
|
1298
|
+
return { columns, primaryKey };
|
|
1299
|
+
}
|
|
1300
|
+
function sqliteForeignKeysFromPragma(table, rows) {
|
|
1301
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1302
|
+
for (const row of rows) {
|
|
1303
|
+
const group = byId.get(row.id) ?? [];
|
|
1304
|
+
group.push(row);
|
|
1305
|
+
byId.set(row.id, group);
|
|
1306
|
+
}
|
|
1307
|
+
const fks = [];
|
|
1308
|
+
for (const group of byId.values()) {
|
|
1309
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
1310
|
+
fks.push({
|
|
1311
|
+
name: `fk_${table}_${ordered.map((r) => r.from).join("_")}`,
|
|
1312
|
+
columns: ordered.map((r) => r.from),
|
|
1313
|
+
refTable: ordered[0]?.table ?? "",
|
|
1314
|
+
refColumns: ordered.map((r) => r.to)
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return fks;
|
|
1318
|
+
}
|
|
1319
|
+
function sqliteUniqueFromPragma(table, rows) {
|
|
1320
|
+
const cols = [...rows].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
1321
|
+
if (cols.length === 0) return null;
|
|
1322
|
+
return { name: `uq_${table}_${cols.join("_")}`, columns: cols };
|
|
1323
|
+
}
|
|
1324
|
+
function isUniqueIndex(idx) {
|
|
1325
|
+
return Number(idx.unique) === 1 && idx.origin !== "pk";
|
|
1326
|
+
}
|
|
1327
|
+
var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
|
|
1328
|
+
async function introspectSqliteAsync(driver) {
|
|
1329
|
+
const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
|
|
1330
|
+
const tables = {};
|
|
1331
|
+
for (const row of tablesRows) {
|
|
1332
|
+
const tableName = String(row.name);
|
|
1333
|
+
const info = (await driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, [])).rows;
|
|
1334
|
+
const { columns, primaryKey } = sqliteColumnsFromPragma(info);
|
|
1335
|
+
const fkRows = (await driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(tableName)})`, [])).rows;
|
|
1336
|
+
const indexes = (await driver.execute(`PRAGMA index_list(${JSON.stringify(tableName)})`, [])).rows;
|
|
1337
|
+
const uniqueConstraints = [];
|
|
1338
|
+
for (const idx of indexes) {
|
|
1339
|
+
if (!isUniqueIndex(idx)) continue;
|
|
1340
|
+
const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
|
|
1341
|
+
const unique = sqliteUniqueFromPragma(tableName, cols);
|
|
1342
|
+
if (unique) uniqueConstraints.push(unique);
|
|
1343
|
+
}
|
|
1344
|
+
tables[tableName] = {
|
|
1345
|
+
name: tableName,
|
|
1346
|
+
columns,
|
|
1347
|
+
primaryKey,
|
|
1348
|
+
uniqueConstraints,
|
|
1349
|
+
foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return { tables };
|
|
1353
|
+
}
|
|
1354
|
+
async function checkDriftAsync(driver, dialect, models) {
|
|
1355
|
+
if (dialect === "postgresql") return checkDriftPostgres(driver, models);
|
|
1356
|
+
if (dialect === "sqlite") {
|
|
1357
|
+
return compareSqliteSchemas(
|
|
1358
|
+
await introspectSqliteAsync(driver),
|
|
1359
|
+
reflectSchema(models)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
return [
|
|
1363
|
+
"drift checking is not implemented for MySQL \u2014 its information_schema introspection is still missing"
|
|
1364
|
+
];
|
|
1365
|
+
}
|
|
1366
|
+
function pgTypeToColumnType(dataType, udtName) {
|
|
1367
|
+
if (dataType.toLowerCase() === "array") {
|
|
1368
|
+
return {
|
|
1369
|
+
kind: "array",
|
|
1370
|
+
meta: { element: { kind: pgUdtToKind(udtName.replace(/^_/, "")), meta: {} } }
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
return { kind: pgTypeToKind(dataType, udtName), meta: {} };
|
|
1374
|
+
}
|
|
1375
|
+
function pgUdtToKind(udtName) {
|
|
1376
|
+
switch (udtName) {
|
|
1377
|
+
case "int2":
|
|
1378
|
+
return "smallint";
|
|
1379
|
+
case "int4":
|
|
1380
|
+
return "integer";
|
|
1381
|
+
case "int8":
|
|
1382
|
+
return "bigint";
|
|
1383
|
+
case "float4":
|
|
1384
|
+
return "real";
|
|
1385
|
+
case "float8":
|
|
1386
|
+
return "double";
|
|
1387
|
+
case "numeric":
|
|
1388
|
+
return "numeric";
|
|
1389
|
+
case "varchar":
|
|
1390
|
+
return "varchar";
|
|
1391
|
+
case "bpchar":
|
|
1392
|
+
return "char";
|
|
1393
|
+
case "bool":
|
|
1394
|
+
return "boolean";
|
|
1395
|
+
case "date":
|
|
1396
|
+
return "date";
|
|
1397
|
+
case "time":
|
|
1398
|
+
case "timetz":
|
|
1399
|
+
return "time";
|
|
1400
|
+
case "timestamp":
|
|
1401
|
+
case "timestamptz":
|
|
1402
|
+
return "timestamp";
|
|
1403
|
+
case "bytea":
|
|
1404
|
+
return "blob";
|
|
1405
|
+
case "json":
|
|
1406
|
+
case "jsonb":
|
|
1407
|
+
return "json";
|
|
1408
|
+
case "uuid":
|
|
1409
|
+
return "uuid";
|
|
1410
|
+
default:
|
|
1411
|
+
return "text";
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1200
1414
|
function pgTypeToKind(dataType, udtName) {
|
|
1201
1415
|
const t = dataType.toLowerCase();
|
|
1202
1416
|
if (t === "user-defined") return "enum";
|
|
@@ -1244,10 +1458,7 @@ async function introspectPostgres(driver) {
|
|
|
1244
1458
|
const isPk = pkSet.has(name);
|
|
1245
1459
|
columns[name] = {
|
|
1246
1460
|
name,
|
|
1247
|
-
type:
|
|
1248
|
-
kind: pgTypeToKind(String(col.data_type), String(col.udt_name)),
|
|
1249
|
-
meta: {}
|
|
1250
|
-
},
|
|
1461
|
+
type: pgTypeToColumnType(String(col.data_type), String(col.udt_name)),
|
|
1251
1462
|
notNull: col.is_nullable === "NO" || isPk,
|
|
1252
1463
|
primaryKey: isPk,
|
|
1253
1464
|
default: null,
|
|
@@ -1303,6 +1514,10 @@ async function postgresUniques(driver, table) {
|
|
|
1303
1514
|
columns: r.cols ?? []
|
|
1304
1515
|
}));
|
|
1305
1516
|
}
|
|
1517
|
+
function describeKind(type) {
|
|
1518
|
+
if (type.kind !== "array") return type.kind;
|
|
1519
|
+
return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
|
|
1520
|
+
}
|
|
1306
1521
|
async function checkDriftPostgres(driver, models) {
|
|
1307
1522
|
const actual = await introspectPostgres(driver);
|
|
1308
1523
|
const expected = reflectSchema(models);
|
|
@@ -1319,9 +1534,9 @@ async function checkDriftPostgres(driver, models) {
|
|
|
1319
1534
|
issues.push(`column "${tableName}.${colName}" is missing from the database`);
|
|
1320
1535
|
continue;
|
|
1321
1536
|
}
|
|
1322
|
-
if (expectedCol.type
|
|
1537
|
+
if (describeKind(expectedCol.type) !== describeKind(actualCol.type)) {
|
|
1323
1538
|
issues.push(
|
|
1324
|
-
`column "${tableName}.${colName}" type differs: model ${expectedCol.type
|
|
1539
|
+
`column "${tableName}.${colName}" type differs: model ${describeKind(expectedCol.type)}, db ${describeKind(actualCol.type)}`
|
|
1325
1540
|
);
|
|
1326
1541
|
}
|
|
1327
1542
|
if (expectedCol.notNull !== actualCol.notNull) {
|
|
@@ -1588,23 +1803,24 @@ function parseRenameFlags(rest) {
|
|
|
1588
1803
|
}
|
|
1589
1804
|
return out;
|
|
1590
1805
|
}
|
|
1591
|
-
function pending(config, runner) {
|
|
1592
|
-
const done = runner.applied();
|
|
1806
|
+
async function pending(config, runner) {
|
|
1807
|
+
const done = await runner.applied();
|
|
1593
1808
|
return topoOrder(config.migrations).filter((m) => !done.has(m.revision));
|
|
1594
1809
|
}
|
|
1595
|
-
function runMigrationCli(argv, config) {
|
|
1810
|
+
async function runMigrationCli(argv, config) {
|
|
1596
1811
|
const [command, ...rest] = argv;
|
|
1597
|
-
const
|
|
1812
|
+
const driver = toAsyncDriver(config.driver);
|
|
1813
|
+
const runner = new AsyncMigrationRunner(driver, config.dialect);
|
|
1598
1814
|
const appliedAt = config.appliedAt ?? "1970-01-01T00:00:00.000Z";
|
|
1599
1815
|
switch (command) {
|
|
1600
1816
|
case "current": {
|
|
1601
|
-
const applied = [...runner.applied()].sort();
|
|
1817
|
+
const applied = [...await runner.applied()].sort();
|
|
1602
1818
|
return ok(applied.length > 0 ? applied : ["(no migrations applied)"]);
|
|
1603
1819
|
}
|
|
1604
1820
|
case "heads":
|
|
1605
1821
|
return ok(heads(config.migrations));
|
|
1606
1822
|
case "history": {
|
|
1607
|
-
const done = runner.applied();
|
|
1823
|
+
const done = await runner.applied();
|
|
1608
1824
|
return ok(
|
|
1609
1825
|
topoOrder(config.migrations).map(
|
|
1610
1826
|
(m) => `${done.has(m.revision) ? "\u2713" : "\xB7"} ${m.revision}${m.label ? ` \u2014 ${m.label}` : ""}`
|
|
@@ -1614,7 +1830,7 @@ function runMigrationCli(argv, config) {
|
|
|
1614
1830
|
case "upgrade": {
|
|
1615
1831
|
if (rest.includes("--sql")) {
|
|
1616
1832
|
const lines = [];
|
|
1617
|
-
for (const migration of pending(config, runner)) {
|
|
1833
|
+
for (const migration of await pending(config, runner)) {
|
|
1618
1834
|
const op = new Op();
|
|
1619
1835
|
migration.up(op);
|
|
1620
1836
|
lines.push(`-- ${migration.revision}`);
|
|
@@ -1625,19 +1841,19 @@ function runMigrationCli(argv, config) {
|
|
|
1625
1841
|
}
|
|
1626
1842
|
return ok(lines.length > 0 ? lines : ["-- nothing to upgrade"]);
|
|
1627
1843
|
}
|
|
1628
|
-
const ran = runner.upgrade(config.migrations, appliedAt);
|
|
1844
|
+
const ran = await runner.upgrade(config.migrations, appliedAt);
|
|
1629
1845
|
return ok(ran.length > 0 ? ran.map((r) => `applied ${r}`) : ["nothing to upgrade"]);
|
|
1630
1846
|
}
|
|
1631
1847
|
case "downgrade": {
|
|
1632
1848
|
const steps = rest[0] ? Number(rest[0]) : 1;
|
|
1633
|
-
const reverted = runner.downgrade(config.migrations, steps);
|
|
1849
|
+
const reverted = await runner.downgrade(config.migrations, steps);
|
|
1634
1850
|
return ok(
|
|
1635
1851
|
reverted.length > 0 ? reverted.map((r) => `reverted ${r}`) : ["nothing to downgrade"]
|
|
1636
1852
|
);
|
|
1637
1853
|
}
|
|
1638
1854
|
case "check": {
|
|
1639
1855
|
if (!config.models) return fail(["check requires models in the config"]);
|
|
1640
|
-
const drift =
|
|
1856
|
+
const drift = await checkDriftAsync(driver, config.dialect, config.models);
|
|
1641
1857
|
const undiffed = diffSchema(
|
|
1642
1858
|
replaySchema(config.migrations),
|
|
1643
1859
|
reflectSchema(config.models)
|
|
@@ -1684,7 +1900,9 @@ exports.UnknownRevision = UnknownRevision;
|
|
|
1684
1900
|
exports.applyOperation = applyOperation;
|
|
1685
1901
|
exports.applyRenames = applyRenames;
|
|
1686
1902
|
exports.checkDrift = checkDrift;
|
|
1903
|
+
exports.checkDriftAsync = checkDriftAsync;
|
|
1687
1904
|
exports.checkDriftPostgres = checkDriftPostgres;
|
|
1905
|
+
exports.compareSqliteSchemas = compareSqliteSchemas;
|
|
1688
1906
|
exports.defineMigrationConfig = defineMigrationConfig;
|
|
1689
1907
|
exports.detectRenames = detectRenames;
|
|
1690
1908
|
exports.diffSchema = diffSchema;
|
|
@@ -1693,6 +1911,7 @@ exports.generateMigration = generateMigration;
|
|
|
1693
1911
|
exports.heads = heads;
|
|
1694
1912
|
exports.introspectPostgres = introspectPostgres;
|
|
1695
1913
|
exports.introspectSqlite = introspectSqlite;
|
|
1914
|
+
exports.introspectSqliteAsync = introspectSqliteAsync;
|
|
1696
1915
|
exports.invert = invert;
|
|
1697
1916
|
exports.invertAll = invertAll;
|
|
1698
1917
|
exports.makeRevisionId = makeRevisionId;
|