tempest-db-js 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { ActiveRecord, Agg, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, Column, DeleteBuilder, InsertBuilder, InvalidDatabaseUrl, JoinBuilder, Model, MysqlDialect, NoResultError, NodeSqliteDriver, OPERATORS, PostgresDialect, QueryExecutionError, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncResult, SyncSession, UpdateBuilder, ValidationError, activeRecord, and, avg, belongsTo, column, columnsOf, count, createEngine, createSyncEngine, del, detectDialect, foreignKey, fromDict, getDialect, hasMany, insert, isCondition, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, select, sql, stringify, sum, toCondNode, toDict, toJSON, unique, update } from './chunk-JR4MLFQN.js';
1
+ export { ActiveRecord, Agg, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, Column, DeleteBuilder, InsertBuilder, InvalidDatabaseUrl, JoinBuilder, Model, MysqlDialect, NoResultError, NodeSqliteDriver, OPERATORS, Params, PostgresDialect, QueryExecutionError, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncResult, SyncSession, UpdateBuilder, ValidationError, activeRecord, and, avg, belongsTo, column, columnNamesOf, columnPropsOf, columnsOf, count, createEngine, createSyncEngine, dbColumn, del, detectDialect, foreignKey, fromDict, getDialect, hasMany, insert, isCondition, isSqlExpression, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, select, sql, stringify, sum, toCondNode, toDict, toJSON, toSnakeCase, unique, update } from './chunk-5QQMVTS5.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -1,9 +1,28 @@
1
1
  'use strict';
2
2
 
3
+ // src/expressions.ts
4
+ function renderPortableToken(token, dialect) {
5
+ switch (token) {
6
+ case "now":
7
+ return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
8
+ case "current_date":
9
+ return "CURRENT_DATE";
10
+ case "current_time":
11
+ return "CURRENT_TIME";
12
+ case "uuidv4":
13
+ if (dialect === "postgresql") return "gen_random_uuid()";
14
+ if (dialect === "mysql") return "(UUID())";
15
+ return "(lower(hex(randomblob(16))))";
16
+ }
17
+ }
18
+
3
19
  // src/index.ts
4
20
  function isDefaultValue(value) {
5
21
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
6
22
  }
23
+ function bindsParameters(value) {
24
+ return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
25
+ }
7
26
  function parseReference(ref, options) {
8
27
  const dot = ref.lastIndexOf(".");
9
28
  if (dot <= 0 || dot === ref.length - 1) {
@@ -17,48 +36,71 @@ function parseReference(ref, options) {
17
36
  };
18
37
  }
19
38
  var Column = class _Column {
20
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
39
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
21
40
  this.type = type;
22
41
  this.flags = flags;
23
42
  this.defaultValue = defaultValue;
24
43
  this.onUpdateValue = onUpdateValue;
25
44
  this.reference = reference;
45
+ this.dbName = dbName;
26
46
  }
27
47
  type;
28
48
  flags;
29
49
  defaultValue;
30
50
  onUpdateValue;
31
51
  reference;
32
- primaryKey() {
52
+ dbName;
53
+ /** Clone this column with one facet replaced, carrying every other over. */
54
+ derive(patch) {
33
55
  return new _Column(
34
56
  this.type,
35
- { ...this.flags, primaryKey: true, hasDefault: true },
36
- this.defaultValue,
37
- this.onUpdateValue,
38
- this.reference
57
+ patch.flags ?? this.flags,
58
+ patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
59
+ patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
60
+ patch.reference !== void 0 ? patch.reference : this.reference,
61
+ patch.dbName !== void 0 ? patch.dbName : this.dbName
39
62
  );
40
63
  }
64
+ primaryKey() {
65
+ return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
66
+ }
41
67
  notNull() {
42
- return new _Column(
43
- this.type,
44
- { ...this.flags, notNull: true },
45
- this.defaultValue,
46
- this.onUpdateValue,
47
- this.reference
48
- );
68
+ return this.derive({ flags: { ...this.flags, notNull: true } });
49
69
  }
50
70
  /**
51
71
  * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
52
72
  * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
53
73
  */
54
74
  unique() {
55
- return new _Column(
56
- this.type,
57
- { ...this.flags, unique: true },
58
- this.defaultValue,
59
- this.onUpdateValue,
60
- this.reference
61
- );
75
+ return this.derive({ flags: { ...this.flags, unique: true } });
76
+ }
77
+ /**
78
+ * Map this property to a differently-named database column, à la SQLAlchemy's
79
+ * `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
80
+ *
81
+ * The override applies everywhere the name reaches SQL — select, insert,
82
+ * update, delete, where, order by, group by, returning, conflict targets, the
83
+ * migration IR and the drift check — while the TypeScript row keeps the
84
+ * property name. Use it to keep a `snake_case` schema behind a `camelCase`
85
+ * model; {@link Model.naming} does the same for a whole table at once.
86
+ *
87
+ * @param dbName The real column name in the database.
88
+ * @returns A new column bound to that name.
89
+ * @throws Error When `dbName` is empty.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * class ApiKey extends Model {
94
+ * static tablename = "api_keys";
95
+ * consumerName = column.text().name("consumer_name").notNull();
96
+ * }
97
+ * ```
98
+ */
99
+ name(dbName) {
100
+ if (dbName.length === 0) {
101
+ throw new Error("column.name() requires a non-empty database column name.");
102
+ }
103
+ return this.derive({ dbName });
62
104
  }
63
105
  /**
64
106
  * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
@@ -71,37 +113,50 @@ var Column = class _Column {
71
113
  * @throws Error When `ref` is not a valid `"table.column"` string.
72
114
  */
73
115
  references(ref, options) {
74
- return new _Column(
75
- this.type,
76
- this.flags,
77
- this.defaultValue,
78
- this.onUpdateValue,
79
- parseReference(ref, options)
80
- );
116
+ return this.derive({ reference: parseReference(ref, options) });
81
117
  }
82
118
  /**
83
119
  * Set the insert-time default: a constant value of type `T`, or a portable
84
120
  * server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
121
+ *
122
+ * @param value The literal default, or a {@link sql} expression.
123
+ * @returns A new column carrying the default.
124
+ * @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
125
+ * nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
85
126
  */
86
127
  default(value) {
87
128
  const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
88
- return new _Column(
89
- this.type,
90
- { ...this.flags, hasDefault: true },
91
- resolved,
92
- this.onUpdateValue,
93
- this.reference
94
- );
129
+ if (bindsParameters(resolved)) {
130
+ throw new Error(
131
+ "sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
132
+ );
133
+ }
134
+ return this.derive({
135
+ flags: { ...this.flags, hasDefault: true },
136
+ defaultValue: resolved
137
+ });
95
138
  }
96
139
  /**
97
140
  * Re-apply a value whenever the row is updated (e.g. an `updated_at` column
98
141
  * with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
142
+ *
143
+ * @param value The literal value, or a {@link sql} expression.
144
+ * @returns A new column carrying the on-update value.
145
+ * @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
99
146
  */
100
147
  onUpdate(value) {
101
148
  const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
102
- return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
149
+ if (bindsParameters(resolved)) {
150
+ throw new Error(
151
+ "sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
152
+ );
153
+ }
154
+ return this.derive({ onUpdateValue: resolved });
103
155
  }
104
156
  };
157
+ function toSnakeCase(name) {
158
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
159
+ }
105
160
  var columnsCache = /* @__PURE__ */ new WeakMap();
106
161
  function columnsOf(model) {
107
162
  const cached = columnsCache.get(model);
@@ -116,16 +171,43 @@ function columnsOf(model) {
116
171
  columnsCache.set(model, out);
117
172
  return out;
118
173
  }
174
+ var nameMapCache = /* @__PURE__ */ new WeakMap();
175
+ function columnNamesOf(model) {
176
+ const cached = nameMapCache.get(model);
177
+ if (cached !== void 0) return cached;
178
+ const strategy = model.naming ?? "preserve";
179
+ const map = {};
180
+ const seen = /* @__PURE__ */ new Map();
181
+ let renamed = false;
182
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
183
+ const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
184
+ const collision = seen.get(dbName);
185
+ if (collision !== void 0) {
186
+ throw new Error(
187
+ `${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
188
+ );
189
+ }
190
+ seen.set(dbName, prop);
191
+ map[prop] = dbName;
192
+ if (dbName !== prop) renamed = true;
193
+ }
194
+ const result = renamed ? map : null;
195
+ nameMapCache.set(model, result);
196
+ return result;
197
+ }
119
198
 
120
199
  // src/migrations/ir.ts
121
200
  function constraintName(prefix, table, columns) {
122
201
  return `${prefix}_${table}_${columns.join("_")}`;
123
202
  }
124
203
  function reflectTable(model) {
204
+ const names = columnNamesOf(model);
205
+ const toColumn = (prop) => names?.[prop] ?? prop;
125
206
  const columns = {};
126
207
  const primaryKey = [];
127
- for (const [name, col] of Object.entries(columnsOf(model))) {
208
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
128
209
  const isPk = col.flags.primaryKey;
210
+ const name = toColumn(prop);
129
211
  columns[name] = {
130
212
  name,
131
213
  type: col.type,
@@ -140,15 +222,16 @@ function reflectTable(model) {
140
222
  const uniqueConstraints = [];
141
223
  const foreignKeys = [];
142
224
  for (const c of model.tableArgs?.() ?? []) {
225
+ const cols = c.columns.map(toColumn);
143
226
  if (c.kind === "unique") {
144
227
  uniqueConstraints.push({
145
- name: c.name ?? constraintName("uq", model.tablename, c.columns),
146
- columns: c.columns
228
+ name: c.name ?? constraintName("uq", model.tablename, cols),
229
+ columns: cols
147
230
  });
148
231
  } else {
149
232
  foreignKeys.push({
150
- name: c.name ?? constraintName("fk", model.tablename, c.columns),
151
- columns: c.columns,
233
+ name: c.name ?? constraintName("fk", model.tablename, cols),
234
+ columns: cols,
152
235
  refTable: c.refTable,
153
236
  refColumns: c.refColumns,
154
237
  onDelete: c.onDelete,
@@ -239,6 +322,8 @@ function renderColumnType(type, dialect) {
239
322
  return "NUMERIC";
240
323
  case "blob":
241
324
  return "BLOB";
325
+ case "array":
326
+ throw new Error(unsupportedArray("sqlite"));
242
327
  default:
243
328
  return "TEXT";
244
329
  }
@@ -280,6 +365,8 @@ function renderColumnType(type, dialect) {
280
365
  return "CHAR(36)";
281
366
  case "enum":
282
367
  return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
368
+ case "array":
369
+ throw new Error(unsupportedArray("mysql"));
283
370
  }
284
371
  }
285
372
  switch (kind) {
@@ -318,27 +405,39 @@ function renderColumnType(type, dialect) {
318
405
  return "UUID";
319
406
  case "enum":
320
407
  return "TEXT";
408
+ case "array":
409
+ return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
321
410
  }
322
411
  }
323
- function renderDefault(def, dialect) {
412
+ function unsupportedArray(dialect) {
413
+ 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.`;
414
+ }
415
+ function arrayElement(element) {
416
+ if (!element) {
417
+ throw new Error(
418
+ "An array column has no element type \u2014 build it with column.array()."
419
+ );
420
+ }
421
+ return element;
422
+ }
423
+ function renderDefault(def, dialect, type) {
324
424
  if (def.kind === "expression") {
325
425
  const expr = def.expression;
326
- if (typeof expr === "object") return expr.raw;
327
- switch (expr) {
328
- case "now":
329
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
330
- case "current_date":
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))))";
426
+ if (typeof expr === "object") {
427
+ if ("raw" in expr) return expr.raw;
428
+ throw new Error(
429
+ "sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
430
+ );
338
431
  }
432
+ return renderPortableToken(expr, dialect);
339
433
  }
340
434
  const value = def.value;
341
435
  if (value === null) return "NULL";
436
+ if (Array.isArray(value) && type?.kind === "array") {
437
+ const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
438
+ const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
439
+ return `ARRAY[${items.join(", ")}]::${elementType}[]`;
440
+ }
342
441
  if (typeof value === "boolean") {
343
442
  return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
344
443
  }
@@ -384,7 +483,9 @@ function tableConstraintClauses(table, dialect) {
384
483
  function renderColumnDef(col, dialect) {
385
484
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
386
485
  if (col.notNull) sql += " NOT NULL";
387
- if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
486
+ if (col.default !== null) {
487
+ sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
488
+ }
388
489
  sql += columnConstraintSuffix(col, dialect);
389
490
  return sql;
390
491
  }
@@ -408,7 +509,9 @@ function renderCreateTable(table, dialect) {
408
509
  typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
409
510
  let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
410
511
  if (c.notNull) def += " NOT NULL";
411
- if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
512
+ if (c.default !== null) {
513
+ def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
514
+ }
412
515
  return def + columnConstraintSuffix(c, dialect);
413
516
  }
414
517
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
@@ -552,7 +655,7 @@ function renderAlterColumn(table, to, dialect) {
552
655
  to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
553
656
  );
554
657
  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`
658
+ 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
659
  );
557
660
  return stmts;
558
661
  }
@@ -1197,6 +1300,54 @@ function checkDrift(driver, models) {
1197
1300
  }
1198
1301
  return issues;
1199
1302
  }
1303
+ function pgTypeToColumnType(dataType, udtName) {
1304
+ if (dataType.toLowerCase() === "array") {
1305
+ return {
1306
+ kind: "array",
1307
+ meta: { element: { kind: pgUdtToKind(udtName.replace(/^_/, "")), meta: {} } }
1308
+ };
1309
+ }
1310
+ return { kind: pgTypeToKind(dataType, udtName), meta: {} };
1311
+ }
1312
+ function pgUdtToKind(udtName) {
1313
+ switch (udtName) {
1314
+ case "int2":
1315
+ return "smallint";
1316
+ case "int4":
1317
+ return "integer";
1318
+ case "int8":
1319
+ return "bigint";
1320
+ case "float4":
1321
+ return "real";
1322
+ case "float8":
1323
+ return "double";
1324
+ case "numeric":
1325
+ return "numeric";
1326
+ case "varchar":
1327
+ return "varchar";
1328
+ case "bpchar":
1329
+ return "char";
1330
+ case "bool":
1331
+ return "boolean";
1332
+ case "date":
1333
+ return "date";
1334
+ case "time":
1335
+ case "timetz":
1336
+ return "time";
1337
+ case "timestamp":
1338
+ case "timestamptz":
1339
+ return "timestamp";
1340
+ case "bytea":
1341
+ return "blob";
1342
+ case "json":
1343
+ case "jsonb":
1344
+ return "json";
1345
+ case "uuid":
1346
+ return "uuid";
1347
+ default:
1348
+ return "text";
1349
+ }
1350
+ }
1200
1351
  function pgTypeToKind(dataType, udtName) {
1201
1352
  const t = dataType.toLowerCase();
1202
1353
  if (t === "user-defined") return "enum";
@@ -1244,10 +1395,7 @@ async function introspectPostgres(driver) {
1244
1395
  const isPk = pkSet.has(name);
1245
1396
  columns[name] = {
1246
1397
  name,
1247
- type: {
1248
- kind: pgTypeToKind(String(col.data_type), String(col.udt_name)),
1249
- meta: {}
1250
- },
1398
+ type: pgTypeToColumnType(String(col.data_type), String(col.udt_name)),
1251
1399
  notNull: col.is_nullable === "NO" || isPk,
1252
1400
  primaryKey: isPk,
1253
1401
  default: null,
@@ -1303,6 +1451,10 @@ async function postgresUniques(driver, table) {
1303
1451
  columns: r.cols ?? []
1304
1452
  }));
1305
1453
  }
1454
+ function describeKind(type) {
1455
+ if (type.kind !== "array") return type.kind;
1456
+ return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
1457
+ }
1306
1458
  async function checkDriftPostgres(driver, models) {
1307
1459
  const actual = await introspectPostgres(driver);
1308
1460
  const expected = reflectSchema(models);
@@ -1319,9 +1471,9 @@ async function checkDriftPostgres(driver, models) {
1319
1471
  issues.push(`column "${tableName}.${colName}" is missing from the database`);
1320
1472
  continue;
1321
1473
  }
1322
- if (expectedCol.type.kind !== actualCol.type.kind) {
1474
+ if (describeKind(expectedCol.type) !== describeKind(actualCol.type)) {
1323
1475
  issues.push(
1324
- `column "${tableName}.${colName}" type differs: model ${expectedCol.type.kind}, db ${actualCol.type.kind}`
1476
+ `column "${tableName}.${colName}" type differs: model ${describeKind(expectedCol.type)}, db ${describeKind(actualCol.type)}`
1325
1477
  );
1326
1478
  }
1327
1479
  if (expectedCol.notNull !== actualCol.notNull) {