turbine-orm 0.28.3 → 0.30.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli/index.js +5 -0
  3. package/dist/cjs/cli/mcp.js +22 -92
  4. package/dist/cjs/client.js +69 -5
  5. package/dist/cjs/generate.js +71 -25
  6. package/dist/cjs/index.js +4 -1
  7. package/dist/cjs/introspect.js +350 -120
  8. package/dist/cjs/mssql.js +18 -133
  9. package/dist/cjs/mysql.js +16 -129
  10. package/dist/cjs/optional-peer-import.cjs +122 -0
  11. package/dist/cjs/powdb.js +440 -81
  12. package/dist/cjs/powql.js +49 -25
  13. package/dist/cjs/query/builder.js +290 -23
  14. package/dist/cjs/query/filters.js +32 -1
  15. package/dist/cjs/schema-metadata.js +316 -0
  16. package/dist/cjs/sqlite.js +8 -89
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +5 -0
  19. package/dist/cli/mcp.d.ts +18 -0
  20. package/dist/cli/mcp.js +22 -93
  21. package/dist/client.d.ts +44 -6
  22. package/dist/client.js +69 -5
  23. package/dist/generate.d.ts +16 -4
  24. package/dist/generate.js +71 -25
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +2 -0
  27. package/dist/introspect.d.ts +94 -1
  28. package/dist/introspect.js +345 -120
  29. package/dist/mssql.js +16 -101
  30. package/dist/mysql.js +14 -97
  31. package/dist/optional-peer-import.cjs +89 -0
  32. package/dist/optional-peer-import.d.cts +53 -0
  33. package/dist/powdb.d.ts +94 -26
  34. package/dist/powdb.js +435 -80
  35. package/dist/powql.d.ts +6 -0
  36. package/dist/powql.js +51 -27
  37. package/dist/query/builder.d.ts +60 -3
  38. package/dist/query/builder.js +291 -24
  39. package/dist/query/deferred.d.ts +7 -2
  40. package/dist/query/filters.d.ts +18 -0
  41. package/dist/query/filters.js +30 -0
  42. package/dist/query/types.d.ts +19 -0
  43. package/dist/schema-metadata.d.ts +77 -0
  44. package/dist/schema-metadata.js +313 -0
  45. package/dist/schema.d.ts +10 -0
  46. package/dist/sqlite.js +9 -90
  47. package/package.json +3 -3
package/dist/powql.d.ts CHANGED
@@ -156,6 +156,12 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
156
156
  * it and the trailing `returning` reads it back — as is any non-string PK.
157
157
  */
158
158
  private applyPkDefault;
159
+ /**
160
+ * The table name as a PowQL type reference — backtick-quoted when it is a
161
+ * reserved word (e.g. a table named `order`). Used in every emitted
162
+ * statement; plain `this.table` stays in error messages.
163
+ */
164
+ private get qt();
159
165
  create(args: CreateArgs<T>): Promise<T>;
160
166
  createMany(args: CreateManyArgs<T>): Promise<T[]>;
161
167
  update(args: UpdateArgs<T>): Promise<T>;
package/dist/powql.js CHANGED
@@ -37,9 +37,9 @@
37
37
  import { randomUUID } from 'node:crypto';
38
38
  import { NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError } from './errors.js';
39
39
  import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
40
- import { PowdbFloatParam, powqlColumnType, rowToEntity } from './powdb.js';
40
+ import { PowdbFloatParam, powqlColumnType, quotePowqlIdent, rowToEntity } from './powdb.js';
41
41
  import { escapeLike } from './query/utils.js';
42
- import { normalizeKeyColumns, } from './schema.js';
42
+ import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
43
43
  /**
44
44
  * Max parent keys per relation-loader `in (…)` query. A `with` over a large
45
45
  * parent set is split into batches of this size so a single query never exceeds
@@ -106,7 +106,14 @@ export class PowqlInterface {
106
106
  }
107
107
  this.meta = meta;
108
108
  this.defaultLimit = options.defaultLimit;
109
- this.warnOnUnlimited = options.warnOnUnlimited !== false;
109
+ // Same per-table resolution as QueryInterface: an object map accepts BOTH
110
+ // the snake_case table name and the camelCase accessor as keys (snake_case
111
+ // wins on conflict); unlisted tables keep the default (warn on).
112
+ const warnOpt = options.warnOnUnlimited;
113
+ this.warnOnUnlimited =
114
+ typeof warnOpt === 'object' && warnOpt !== null
115
+ ? (warnOpt[table] ?? warnOpt[snakeToCamel(table)]) !== false
116
+ : warnOpt !== false;
110
117
  this.onQuery = options._onQuery;
111
118
  }
112
119
  // -------------------------------------------------------------------------
@@ -411,7 +418,7 @@ export class PowqlInterface {
411
418
  const chunk = targetPks.slice(i, i + MAX_RELATION_KEYS);
412
419
  const params = [];
413
420
  const ph = chunk.map((v) => this.param(v, params)).join(', ');
414
- const { rows } = await this.exec(`${through.table} filter .${targetJCol} in (${ph}) { .${sourceJCol} }`, params, timeout);
421
+ const { rows } = await this.exec(`${quotePowqlIdent(through.table)} filter .${targetJCol} in (${ph}) { .${sourceJCol} }`, params, timeout);
415
422
  for (const r of rows) {
416
423
  const v = r[sourceJCol];
417
424
  if (v != null)
@@ -563,7 +570,7 @@ export class PowqlInterface {
563
570
  }
564
571
  const limitClause = limit !== undefined ? ` limit ${this.param(limit, params)}` : '';
565
572
  const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
566
- const powql = `${this.table}${distinct}${filter}${order}${limitClause}${offsetClause} ${this.projection(cols)}`;
573
+ const powql = `${this.qt}${distinct}${filter}${order}${limitClause}${offsetClause} ${this.projection(cols)}`;
567
574
  const { rows } = await this.exec(powql, params, args.timeout);
568
575
  return rows;
569
576
  }
@@ -712,7 +719,7 @@ export class PowqlInterface {
712
719
  const chunk = parentKeys.slice(i, i + MAX_RELATION_KEYS);
713
720
  const params = [];
714
721
  const placeholders = chunk.map((v) => this.param(v, params)).join(', ');
715
- const powql = `${through.table} filter .${sourceJCol} in (${placeholders}) { .${sourceJCol}, .${targetJCol} }`;
722
+ const powql = `${quotePowqlIdent(through.table)} filter .${sourceJCol} in (${placeholders}) { .${sourceJCol}, .${targetJCol} }`;
716
723
  const { rows } = await this.exec(powql, params, timeout);
717
724
  for (const row of rows) {
718
725
  const sv = String(row[sourceJCol]);
@@ -794,6 +801,14 @@ export class PowqlInterface {
794
801
  }
795
802
  return out;
796
803
  }
804
+ /**
805
+ * The table name as a PowQL type reference — backtick-quoted when it is a
806
+ * reserved word (e.g. a table named `order`). Used in every emitted
807
+ * statement; plain `this.table` stays in error messages.
808
+ */
809
+ get qt() {
810
+ return quotePowqlIdent(this.table);
811
+ }
797
812
  async create(args) {
798
813
  return this.withMiddleware('create', args, async () => {
799
814
  if (hasRelationFields(args.data, this.meta)) {
@@ -802,9 +817,11 @@ export class PowqlInterface {
802
817
  const data = this.applyPkDefault(args.data);
803
818
  const assigns = this.scalarData(data);
804
819
  const params = [];
805
- const body = assigns.map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`).join(', ');
820
+ const body = assigns
821
+ .map((a) => `${quotePowqlIdent(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
822
+ .join(', ');
806
823
  // `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
807
- const { rows } = await this.exec(`insert ${this.table} { ${body} } returning`, params, args.timeout);
824
+ const { rows } = await this.exec(`insert ${this.qt} { ${body} } returning`, params, args.timeout);
808
825
  const row = rows.length ? this.shape(rows)[0] : null;
809
826
  if (!row)
810
827
  throw new NotFoundError({ table: this.table, where: data });
@@ -819,10 +836,10 @@ export class PowqlInterface {
819
836
  const params = [];
820
837
  const tuples = inputs.map((d) => {
821
838
  const assigns = this.scalarData(d);
822
- return `{ ${assigns.map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`).join(', ')} }`;
839
+ return `{ ${assigns.map((a) => `${quotePowqlIdent(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`).join(', ')} }`;
823
840
  });
824
841
  // Multi-row insert with `returning` hands back every inserted row in one round-trip.
825
- const { rows } = await this.exec(`insert ${this.table} ${tuples.join(', ')} returning`, params, args.timeout);
842
+ const { rows } = await this.exec(`insert ${this.qt} ${tuples.join(', ')} returning`, params, args.timeout);
826
843
  return this.shape(rows);
827
844
  });
828
845
  }
@@ -837,7 +854,7 @@ export class PowqlInterface {
837
854
  this.assertCompiledWhere(where, false, 'update');
838
855
  const setClause = this.buildUpdateAssignments(args.data, params);
839
856
  // `returning` hands back the post-update row(s); take the first (single-row contract).
840
- const { rows } = await this.exec(`${this.table} filter ${where} update { ${setClause} } returning`, params, args.timeout);
857
+ const { rows } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout);
841
858
  const row = rows.length ? this.shape(rows)[0] : null;
842
859
  if (!row)
843
860
  throw new NotFoundError({ table: this.table, where: args.where });
@@ -852,7 +869,7 @@ export class PowqlInterface {
852
869
  this.assertCompiledWhere(where, args.allowFullTableScan, 'updateMany');
853
870
  const setClause = this.buildUpdateAssignments(args.data, params);
854
871
  const filter = where ? ` filter ${where}` : '';
855
- const { rowCount } = await this.exec(`${this.table}${filter} update { ${setClause} }`, params, args.timeout);
872
+ const { rowCount } = await this.exec(`${this.qt}${filter} update { ${setClause} }`, params, args.timeout);
856
873
  return { count: rowCount };
857
874
  });
858
875
  }
@@ -867,7 +884,7 @@ export class PowqlInterface {
867
884
  }
868
885
  const colMeta = this.column(field);
869
886
  const ref = this.ref(field);
870
- const col = colMeta.name;
887
+ const col = quotePowqlIdent(colMeta.name);
871
888
  if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
872
889
  const opObj = value;
873
890
  if ('set' in opObj)
@@ -925,8 +942,10 @@ export class PowqlInterface {
925
942
  // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
926
943
  const d = this.options.dialect;
927
944
  const client = await this.pool.connect();
945
+ let began = false;
928
946
  try {
929
947
  await client.query(d?.beginStatement?.() ?? 'begin');
948
+ began = true;
930
949
  const { TransactionClient } = await import('./client.js');
931
950
  const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
932
951
  const ctx = { schema: this.schema, tx: tx };
@@ -935,11 +954,16 @@ export class PowqlInterface {
935
954
  return result;
936
955
  }
937
956
  catch (err) {
938
- try {
939
- await client.query(d?.rollbackStatement?.() ?? 'rollback');
940
- }
941
- catch {
942
- /* best-effort — the connection may be gone */
957
+ // Only roll back a transaction we actually opened — a failed BEGIN
958
+ // (queue timeout, re-entrancy E017) must not emit a stray ROLLBACK that
959
+ // could land inside another transaction on a shared engine handle.
960
+ if (began) {
961
+ try {
962
+ await client.query(d?.rollbackStatement?.() ?? 'rollback');
963
+ }
964
+ catch {
965
+ /* best-effort — the connection may be gone */
966
+ }
943
967
  }
944
968
  throw err;
945
969
  }
@@ -962,7 +986,7 @@ export class PowqlInterface {
962
986
  const where = this.buildWhere(resolvedWhere, params);
963
987
  this.assertCompiledWhere(where, false, 'delete');
964
988
  // `returning` hands back the deleted row(s) — no separate pre-image reselect needed.
965
- const { rows } = await this.exec(`${this.table} filter ${where} delete returning`, params, args.timeout);
989
+ const { rows } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout);
966
990
  const row = rows.length ? this.shape(rows)[0] : null;
967
991
  if (!row)
968
992
  throw new NotFoundError({ table: this.table, where: args.where });
@@ -976,7 +1000,7 @@ export class PowqlInterface {
976
1000
  const where = this.buildWhere(resolvedWhere, params);
977
1001
  this.assertCompiledWhere(where, args.allowFullTableScan, 'deleteMany');
978
1002
  const filter = where ? ` filter ${where}` : '';
979
- const { rowCount } = await this.exec(`${this.table}${filter} delete`, params, args.timeout);
1003
+ const { rowCount } = await this.exec(`${this.qt}${filter} delete`, params, args.timeout);
980
1004
  return { count: rowCount };
981
1005
  });
982
1006
  }
@@ -991,14 +1015,14 @@ export class PowqlInterface {
991
1015
  }
992
1016
  const params = [];
993
1017
  const createBody = this.scalarData(createData)
994
- .map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`)
1018
+ .map((a) => `${quotePowqlIdent(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
995
1019
  .join(', ');
996
1020
  const updateBody = this.buildUpdateAssignments(args.update, params);
997
1021
  // PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
998
1022
  // (verified: "unexpected trailing token … 'returning'"), because it is one
999
1023
  // atomic insert-or-update, not two branches. So upsert alone keeps the
1000
1024
  // reselect-by-PK fetch; create/update/delete all use `returning`.
1001
- await this.exec(`upsert ${this.table} on .${pkCol} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout);
1025
+ await this.exec(`upsert ${this.qt} on .${pkCol} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout);
1002
1026
  const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
1003
1027
  const row = await this.reselectByPk(createData[pkField], args.timeout);
1004
1028
  if (!row)
@@ -1043,7 +1067,7 @@ export class PowqlInterface {
1043
1067
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1044
1068
  const where = this.buildWhere(resolvedWhere, params);
1045
1069
  const filter = where ? ` filter ${where}` : '';
1046
- const { rows } = await this.exec(`count(${this.table}${filter})`, params, args.timeout);
1070
+ const { rows } = await this.exec(`count(${this.qt}${filter})`, params, args.timeout);
1047
1071
  return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
1048
1072
  });
1049
1073
  }
@@ -1063,12 +1087,12 @@ export class PowqlInterface {
1063
1087
  };
1064
1088
  if (args._count) {
1065
1089
  if (args._count === true) {
1066
- result._count = (await scalar(`count(${this.table}${filter})`)) ?? 0;
1090
+ result._count = (await scalar(`count(${this.qt}${filter})`)) ?? 0;
1067
1091
  }
1068
1092
  else {
1069
1093
  const counts = {};
1070
1094
  for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
1071
- counts[field] = (await scalar(`count(${this.table}${filter} { ${this.ref(field)} })`)) ?? 0;
1095
+ counts[field] = (await scalar(`count(${this.qt}${filter} { ${this.ref(field)} })`)) ?? 0;
1072
1096
  }
1073
1097
  result._count = counts;
1074
1098
  }
@@ -1080,7 +1104,7 @@ export class PowqlInterface {
1080
1104
  const acc = {};
1081
1105
  for (const field of Object.keys(spec).filter((f) => spec[f])) {
1082
1106
  const powfn = fn.slice(1); // sum/avg/min/max
1083
- acc[field] = await scalar(`${powfn}(${this.table}${filter} { ${this.ref(field)} })`);
1107
+ acc[field] = await scalar(`${powfn}(${this.qt}${filter} { ${this.ref(field)} })`);
1084
1108
  }
1085
1109
  result[fn] = acc;
1086
1110
  }
@@ -1114,7 +1138,7 @@ export class PowqlInterface {
1114
1138
  }
1115
1139
  const having = this.buildHaving(args.having, params);
1116
1140
  const order = this.buildOrder(args.orderBy);
1117
- const powql = `${this.table}${filter} group ${groupKeys.join(', ')}${having}${order} { ${proj.join(', ')} }`;
1141
+ const powql = `${this.qt}${filter} group ${groupKeys.join(', ')}${having}${order} { ${proj.join(', ')} }`;
1118
1142
  const { rows } = await this.exec(powql, params, args.timeout);
1119
1143
  // Reshape: group keys → camel fields + coerced; aggregates → nested {_sum:{field}}.
1120
1144
  return rows.map((raw) => {
@@ -55,6 +55,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
55
55
  /** Pre-computed column type lookups (avoids linear scans per query) */
56
56
  private readonly columnPgTypeMap;
57
57
  private readonly columnArrayTypeMap;
58
+ /**
59
+ * Columns whose type lives in a DIFFERENT schema than the introspected one
60
+ * (ColumnMetadata.pgTypeSchema is recorded only in that case) — such columns
61
+ * must never receive this schema's `::"enum"` cast (see enumTypeForColumn).
62
+ */
63
+ private readonly crossSchemaTypeColumns;
58
64
  /** Tracks tables that have already triggered a deep-with warning (one-time) */
59
65
  private readonly deepWithWarned;
60
66
  /**
@@ -249,7 +255,10 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
249
255
  * loop calling `db.users.findMany()` thousands of times only logs once.
250
256
  * Suppressed when `defaultLimit` is configured (the caller has already
251
257
  * opted in to a bounded query) and when the user passed an explicit
252
- * `limit`, `take`, or `cursor`.
258
+ * `limit`, `take`, or `cursor`. A per-call `warnOnUnlimited` overrides the
259
+ * config-level setting in either direction (`false` silences a call that
260
+ * intentionally reads the full set; `true` forces the warning even when
261
+ * disabled in config).
253
262
  */
254
263
  private maybeWarnUnlimited;
255
264
  /**
@@ -429,7 +438,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
429
438
  private collectRelFilterParams;
430
439
  /** Collect params from operator clauses. Mirrors buildOperatorClauses. */
431
440
  private collectOperatorParams;
432
- /** Collect params from JSON filter. Mirrors buildJsonFilterClauses. */
441
+ /**
442
+ * Collect params from JSON filter. Mirrors buildJsonFilterClauses exactly:
443
+ * the `path` is bound at most once (its placeholder is shared by every
444
+ * extraction clause), then equals/contains/hasKey values, then the range
445
+ * comparison values in {@link JSON_RANGE_OPERATORS} order.
446
+ */
433
447
  private collectJsonFilterParams;
434
448
  /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
435
449
  private collectArrayFilterParams;
@@ -545,6 +559,28 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
545
559
  * (relation targets, not just `this.table`).
546
560
  */
547
561
  private pgTypeForColumn;
562
+ /**
563
+ * The Postgres enum type name for a column, when the schema knows one.
564
+ *
565
+ * Introspection stores each column's `udt_name` in `pgTypes` and every
566
+ * database enum in `schema.enums` (typname → labels); a column whose type
567
+ * matches an enum key needs an explicit `::"EnumName"` cast on its write
568
+ * binds — bulk-insert forms like `UNNEST($1::text[])` otherwise type the
569
+ * value as text and Postgres refuses the implicit text→enum coercion
570
+ * ("column X is of type Y but expression is of type text").
571
+ *
572
+ * Postgres-only by construction: gated on the active dialect being
573
+ * `postgresql` AND on `schema.enums` having entries (only PG introspection
574
+ * produces them — `defineSchema` and the other engines leave it empty), so
575
+ * SQLite/MySQL/MSSQL/PowDB output is byte-identical.
576
+ */
577
+ private enumTypeForColumn;
578
+ /**
579
+ * `::"EnumName"` cast suffix for a write-bind placeholder on an enum
580
+ * column; `''` for every other column, so non-enum SQL stays byte-identical.
581
+ * The type name is an introspected identifier and is quoted via the dialect.
582
+ */
583
+ private enumCastSuffix;
548
584
  /**
549
585
  * Equality-fallthrough guard shared by every SQL-build path AND every
550
586
  * cache-hit param-collect path. A plain object literal that matched no known
@@ -899,11 +935,32 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
899
935
  * E.g. '_text' → 'text', '_int4' → 'integer'
900
936
  */
901
937
  private getArrayElementType;
938
+ /**
939
+ * Validate and enumerate the range comparisons (`gt`/`gte`/`lt`/`lte`) on a
940
+ * JSON filter, in the fixed {@link JSON_RANGE_OPERATORS} order. Shared by
941
+ * the SQL-build path ({@link buildJsonFilterClauses}) and the cache-hit
942
+ * param-collect path ({@link collectJsonFilterParams}) so both always agree
943
+ * on which params are pushed — and both throw identically for invalid
944
+ * shapes, so a warmed cache can never skip validation.
945
+ */
946
+ private jsonRangeEntries;
902
947
  /**
903
948
  * Build SQL clauses for JSONB filter operators on a column.
904
- * Supports: path, equals, contains, hasKey.
949
+ * Supports: path, equals, contains, hasKey, gt, gte, lt, lte.
950
+ *
951
+ * The `path` param is bound at most once and its placeholder is shared by
952
+ * every clause that extracts it (equals + range ops), so the param list
953
+ * stays byte-identical to {@link collectJsonFilterParams}.
905
954
  */
906
955
  private buildJsonFilterClauses;
956
+ /**
957
+ * Cast an extracted JSON path text value to a numeric type for range
958
+ * comparison. PostgreSQL uses `(expr)::numeric` (exact — the right way to
959
+ * compare JSON numbers, and `::float` would lose precision on big ints);
960
+ * other dialects route through {@link Dialect.castAggregate} (SQLite/MySQL/
961
+ * SQL Server have no `::` operator) as a float cast.
962
+ */
963
+ private castJsonNumeric;
907
964
  /**
908
965
  * Build SQL clauses for Array filter operators on a column.
909
966
  * Supports: has, hasEvery, hasSome, isEmpty.