uql-orm 0.80.0 → 0.82.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 (123) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +15 -5
  9. package/dist/dialect/abstractSqlDialect.js +109 -40
  10. package/dist/dialect/operators.d.ts +7 -1
  11. package/dist/dialect/operators.js +13 -1
  12. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  13. package/dist/dialect/vectorSqlDialect.js +4 -0
  14. package/dist/entity/metadata/definition.d.ts +1 -2
  15. package/dist/entity/metadata/definition.js +37 -39
  16. package/dist/http/handler.js +5 -4
  17. package/dist/http/query.d.ts +1 -1
  18. package/dist/http/query.js +2 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  22. package/dist/migrate/assertCliConfig.js +7 -6
  23. package/dist/migrate/bin.js +0 -0
  24. package/dist/migrate/builder/expressions.d.ts +2 -0
  25. package/dist/migrate/builder/expressions.js +26 -1
  26. package/dist/migrate/cli-config.js +5 -4
  27. package/dist/migrate/cli.js +1 -1
  28. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  30. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  31. package/dist/migrate/ddl/indexDdl.js +4 -3
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +12 -10
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +10 -7
  38. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  39. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  40. package/dist/migrate/ddl/tableDdl.js +11 -8
  41. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  42. package/dist/migrate/drift/driftDetector.js +21 -21
  43. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  44. package/dist/migrate/generator/definitionToNode.js +9 -20
  45. package/dist/migrate/generator/mongoCommand.js +2 -1
  46. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  47. package/dist/migrate/generator/mongoSchemaGenerator.js +17 -24
  48. package/dist/migrate/index.d.ts +2 -1
  49. package/dist/migrate/index.js +1 -0
  50. package/dist/migrate/indexPredicate.js +2 -1
  51. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  52. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -19
  53. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  54. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  55. package/dist/migrate/introspection/mongoIntrospector.js +5 -4
  56. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  57. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  58. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  59. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  60. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  61. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  62. package/dist/migrate/migrationTarget.js +2 -1
  63. package/dist/migrate/migrator.d.ts +5 -0
  64. package/dist/migrate/migrator.js +35 -45
  65. package/dist/migrate/schemaChange.d.ts +18 -0
  66. package/dist/migrate/schemaChange.js +37 -0
  67. package/dist/migrate/schemaGenerator.d.ts +13 -14
  68. package/dist/migrate/schemaGenerator.js +88 -181
  69. package/dist/migrate/triggerSql.d.ts +1 -1
  70. package/dist/migrate/triggerSql.js +77 -61
  71. package/dist/mongo/mongoDialect.d.ts +1 -3
  72. package/dist/mongo/mongoDialect.js +9 -14
  73. package/dist/mongo/mongodbQuerier.js +3 -7
  74. package/dist/querier/abstractQuerier.d.ts +9 -4
  75. package/dist/querier/abstractQuerier.js +26 -19
  76. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  77. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  78. package/dist/querier/abstractSqlQuerier.js +1 -1
  79. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  80. package/dist/querier/queryError.d.ts +2 -2
  81. package/dist/schema/indexDifferences.d.ts +22 -6
  82. package/dist/schema/indexDifferences.js +23 -8
  83. package/dist/schema/matchByKey.d.ts +10 -0
  84. package/dist/schema/matchByKey.js +18 -0
  85. package/dist/schema/schemaAST.d.ts +6 -2
  86. package/dist/schema/schemaAST.js +7 -3
  87. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  88. package/dist/schema/schemaASTBuilder.js +17 -11
  89. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  90. package/dist/schema/schemaASTDiffer.js +15 -36
  91. package/dist/schema/types.d.ts +14 -15
  92. package/dist/type/dialect.d.ts +23 -1
  93. package/dist/type/entity.d.ts +15 -11
  94. package/dist/type/logger.d.ts +2 -2
  95. package/dist/type/migration.d.ts +32 -50
  96. package/dist/type/querier.d.ts +3 -3
  97. package/dist/type/query.d.ts +3 -13
  98. package/dist/type/queryAggregate.d.ts +4 -10
  99. package/dist/type/queryRaw.d.ts +17 -3
  100. package/dist/type/queryRaw.js +2 -1
  101. package/dist/type/queryWhere.d.ts +7 -7
  102. package/dist/type/universalQuerier.d.ts +3 -3
  103. package/dist/type/vector.d.ts +2 -1
  104. package/dist/type/vector.js +2 -1
  105. package/dist/util/ddlExpression.util.d.ts +5 -1
  106. package/dist/util/ddlExpression.util.js +6 -2
  107. package/dist/util/dialect.util.d.ts +13 -5
  108. package/dist/util/dialect.util.js +28 -20
  109. package/dist/util/field.util.d.ts +4 -4
  110. package/dist/util/field.util.js +10 -2
  111. package/dist/util/hook.util.d.ts +1 -1
  112. package/dist/util/hook.util.js +8 -1
  113. package/dist/util/index.d.ts +1 -0
  114. package/dist/util/index.js +1 -0
  115. package/dist/util/logger.d.ts +3 -3
  116. package/dist/util/object.util.js +3 -2
  117. package/dist/util/raw.d.ts +6 -7
  118. package/dist/util/raw.js +10 -12
  119. package/dist/util/sqlLiteral.js +3 -2
  120. package/dist/util/triggerWrite.d.ts +15 -0
  121. package/dist/util/triggerWrite.js +20 -0
  122. package/package.json +1 -1
  123. package/skills/uql-orm/SKILL.md +3 -3
@@ -1,7 +1,8 @@
1
1
  import { stampEvents } from '../util/field.util.js';
2
2
  import { definedEntries } from '../util/object.util.js';
3
- import { raw, rowColumn, rowRefs } from '../util/raw.js';
3
+ import { raw, refs, rowRefs } from '../util/raw.js';
4
4
  import { ownedName } from '../util/sql.util.js';
5
+ import { UqlUsageError } from '../util/uqlError.js';
5
6
  /**
6
7
  * One trigger for `dialect`, named for its table and label and ending in a hash of its own SQL. That hash
7
8
  * is the whole of change detection: a trigger is in place exactly when its name is installed, and an
@@ -20,7 +21,7 @@ export function renderTrigger(dialect, meta, trigger, position) {
20
21
  */
21
22
  export function dropTrigger(dialect, meta, name) {
22
23
  const { scope, body } = dialect.features.triggers;
23
- const table = dialect.escapeId(dialect.resolveTableName(meta));
24
+ const table = dialect.escapedTableName(meta);
24
25
  return [
25
26
  `DROP TRIGGER IF EXISTS ${triggerId(dialect, meta, name)}${scope === 'table' ? ` ON ${table}` : ''}`,
26
27
  ...(body === 'function' ? [`DROP FUNCTION IF EXISTS ${schemaObjectId(dialect, meta, name)}()`] : []),
@@ -45,17 +46,15 @@ function schemaObjectId(dialect, meta, name) {
45
46
  */
46
47
  function triggerStatements(dialect, meta, trigger, name) {
47
48
  const features = dialect.features.triggers;
48
- // The event, read apart once: `beforeUpdate` is `BEFORE` and `UPDATE`, and everything else follows.
49
- const before = trigger.on.startsWith('before');
50
- const operation = trigger.on.slice(before ? 6 : 5).toUpperCase();
49
+ const [timing, operation] = EVENT_PARTS[trigger.on];
50
+ const before = timing === 'BEFORE';
51
51
  const body = triggerBody(dialect, meta, trigger, before);
52
52
  const id = triggerId(dialect, meta, name);
53
- const table = dialect.escapeId(dialect.resolveTableName(meta));
54
- const column = (key) => dialect.escapeId(dialect.columnOf(meta, key));
53
+ const table = dialect.escapedTableName(meta);
55
54
  const names = rowNames(dialect);
56
- const rows = [rowRefs(names.$new), rowRefs(names.$old)];
57
- const sql = dialect.compileDdl(body(...rows), meta.entity);
58
- const guard = triggerGuard(dialect, meta, trigger, rows, names, column);
55
+ const rows = [rowRefs(meta.entity, names.$new), rowRefs(meta.entity, names.$old)];
56
+ const sql = dialect.compileDdl(body(...rows), meta.entity, { rows: rowsFrom(dialect, meta, operation) });
57
+ const guard = triggerGuard(dialect, meta, trigger, rows, names);
59
58
  const inBody = features.guards !== 'clause';
60
59
  const guarded = !guard || !inBody
61
60
  ? sql
@@ -66,14 +65,14 @@ function triggerStatements(dialect, meta, trigger, name) {
66
65
  // which rows are touched, so it runs even when the guard keeps the statements from running.
67
66
  const opened = features.preamble ? `${features.preamble}\n${guarded}` : guarded;
68
67
  const of = features.guards === 'clause' && operation === 'UPDATE' && trigger.of?.length
69
- ? ` OF ${trigger.of.map(column).join(', ')}`
68
+ ? ` OF ${trigger.of.map((key) => dialect.escapedColumnName(meta, key)).join(', ')}`
70
69
  : '';
71
70
  const each = features.rows === 'set' ? '' : '\nFOR EACH ROW';
72
71
  const clause = guard && !inBody ? `\nWHEN (${guard})` : '';
73
- const timing = `${before ? 'BEFORE' : 'AFTER'} ${operation}${of}`;
72
+ const when = `${timing} ${operation}${of}`;
74
73
  const header = features.layout === 'tableFirst'
75
- ? `CREATE TRIGGER ${id}\nON ${table} ${timing}${each}${clause}\nAS`
76
- : `CREATE TRIGGER ${id}\n${timing} ON ${table}${each}${clause}`;
74
+ ? `CREATE TRIGGER ${id}\nON ${table} ${when}${each}${clause}\nAS`
75
+ : `CREATE TRIGGER ${id}\n${when} ON ${table}${each}${clause}`;
77
76
  if (features.body !== 'function') {
78
77
  return [`${header}\nBEGIN\n${opened}\nEND`];
79
78
  }
@@ -83,6 +82,15 @@ function triggerStatements(dialect, meta, trigger, name) {
83
82
  const fn = schemaObjectId(dialect, meta, name);
84
83
  return [plpgsqlFunction(fn, `BEGIN\n${opened}\nRETURN ${returned};\nEND`), `${header}\nEXECUTE FUNCTION ${fn}()`];
85
84
  }
85
+ /** Each event read apart: `beforeUpdate` is `BEFORE` and `UPDATE`. */
86
+ const EVENT_PARTS = {
87
+ beforeInsert: ['BEFORE', 'INSERT'],
88
+ afterInsert: ['AFTER', 'INSERT'],
89
+ beforeUpdate: ['BEFORE', 'UPDATE'],
90
+ afterUpdate: ['AFTER', 'UPDATE'],
91
+ beforeDelete: ['BEFORE', 'DELETE'],
92
+ afterDelete: ['AFTER', 'DELETE'],
93
+ };
86
94
  /**
87
95
  * The body for the engine in use, refusing first what the engine cannot render at all. One body serves
88
96
  * every engine; in a map, one written for the family serves its forks: CockroachDB runs Postgres's
@@ -91,25 +99,25 @@ function triggerStatements(dialect, meta, trigger, name) {
91
99
  function triggerBody(dialect, meta, trigger, before) {
92
100
  const features = dialect.features.triggers;
93
101
  if (before && !features.before) {
94
- throw new TypeError(`${dialect.dialectName} has no BEFORE trigger, only AFTER and INSTEAD OF, so '${trigger.on}' cannot be ` +
102
+ throw new UqlUsageError(`${dialect.dialectName} has no BEFORE trigger, only AFTER and INSTEAD OF, so '${trigger.on}' cannot be ` +
95
103
  'rendered there. Use the matching after event, which sees the row already written.');
96
104
  }
97
105
  if (trigger.where && features.rows === 'set') {
98
- throw new TypeError(`${dialect.dialectName} fires a trigger once per statement, over the rows it touched, so no condition ` +
106
+ throw new UqlUsageError(`${dialect.dialectName} fires a trigger once per statement, over the rows it touched, so no condition ` +
99
107
  `can read one row: '${meta.entity.name}' cannot state a trigger 'where' there. Guard inside the body ` +
100
108
  'instead, where `inserted` and `deleted` can be read as tables.');
101
109
  }
102
110
  const { run } = trigger;
103
111
  const body = typeof run === 'function' ? run : (run[dialect.dialectName] ?? run[dialect.dialectFamily]);
104
112
  if (!body) {
105
- throw new TypeError(`'${meta.entity.name}' has a trigger with no body for ${dialect.dialectName}, the engine in use. ` +
113
+ throw new UqlUsageError(`'${meta.entity.name}' has a trigger with no body for ${dialect.dialectName}, the engine in use. ` +
106
114
  'Write one for it, or one body for every engine.');
107
115
  }
108
116
  return body;
109
117
  }
110
118
  /** The one condition both guards reduce to: any watched column that moved, and whatever `where` asks. */
111
- function triggerGuard(dialect, meta, trigger, rows, names, column) {
112
- const moved = movedColumns(dialect, meta, trigger.of ?? [], names, column);
119
+ function triggerGuard(dialect, meta, trigger, rows, names) {
120
+ const moved = movedColumns(dialect, meta, trigger.of ?? [], names);
113
121
  return [
114
122
  ...(moved ? [moved] : []),
115
123
  ...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(moved)) : []),
@@ -137,16 +145,10 @@ function condition(dialect, meta, where, rows, names, operand) {
137
145
  return [operand ? `(${sql})` : sql];
138
146
  }
139
147
  const predicates = definedEntries(where);
140
- return predicates.map(([row, predicate]) => {
141
- const ctx = dialect.createContext({ inlineValues: true });
142
- const escapedPrefix = `${names[row]}.`;
143
- dialect.where(ctx, meta.entity, predicate, {
144
- clause: false,
145
- operand: operand || predicates.length > 1,
146
- escapedPrefix,
147
- });
148
- return ctx.sql;
149
- });
148
+ return predicates.map(([row, predicate]) => dialect.compileDdl(predicate, meta.entity, {
149
+ escapedPrefix: `${names[row]}.`,
150
+ operand: operand || predicates.length > 1,
151
+ }));
150
152
  }
151
153
  /**
152
154
  * The triggers a stamp needs: one per event it names, each assigning the field's expression to its
@@ -157,8 +159,8 @@ export function stampTriggers(dialect, meta) {
157
159
  const features = dialect.features.triggers;
158
160
  // Restating the row has to wait for it to be there, so those engines stamp after the write.
159
161
  const after = !features.assignsRow;
160
- const newName = rowNames(dialect).$new;
161
- return definedEntries(meta.fields).flatMap(([key, field]) => {
162
+ const fields = meta.fields;
163
+ return definedEntries(fields).flatMap(([key, field]) => {
162
164
  const events = stampEvents(field);
163
165
  const { computed } = field;
164
166
  if (!events?.length || !computed) {
@@ -169,7 +171,7 @@ export function stampTriggers(dialect, meta) {
169
171
  // The event is part of the name: a stamp on both writes installs two triggers, and one identifier
170
172
  // between them would have the second replace the first rather than sit beside it.
171
173
  name: `${key}_${event}`,
172
- run: () => stampBody(dialect, meta, key, computed, newName),
174
+ run: (newRow) => stampBody(dialect, meta, key, computed, newRow),
173
175
  }));
174
176
  });
175
177
  }
@@ -178,26 +180,25 @@ const STAMP_EVENTS = {
178
180
  before: { insert: 'beforeInsert', update: 'beforeUpdate' },
179
181
  after: { insert: 'afterInsert', update: 'afterUpdate' },
180
182
  };
181
- /** The one statement a stamp runs, in whichever of the two shapes the engine leaves open. */
182
- function stampBody(dialect, meta, key, value, newName) {
183
- const features = dialect.features.triggers;
184
- const read = (member) => rowColumn(newName, member);
185
- if (features.assignsRow) {
186
- const target = read(key);
187
- return features.body === 'function' ? raw `${target} := ${value};` : raw `SET ${target} = ${value};`;
183
+ /**
184
+ * The one statement a stamp runs, in whichever of the two shapes the engine leaves open: an assignment
185
+ * to the incoming row, or where it may not be written, the row restated after the write, as any write in
186
+ * a trigger's body is, keyed on its whole key and only where the stamp still differs: that `UPDATE` fires
187
+ * the trigger again, and with recursive triggers on it then finds nothing left to change.
188
+ */
189
+ function stampBody(dialect, meta, key, value, newRow) {
190
+ const { entity } = meta;
191
+ if (dialect.features.triggers.assignsRow) {
192
+ const target = newRow[key];
193
+ return dialect.features.triggers.body === 'function' ? raw `${target} := ${value};` : raw `SET ${target} = ${value};`;
188
194
  }
189
- const table = dialect.escapeId(dialect.resolveTableName(meta));
190
- const column = dialect.escapeId(dialect.columnOf(meta, key));
191
- const keyed = meta.ids
192
- .map((id) => raw `${text(`${table}.${dialect.escapeId(dialect.columnOf(meta, id))}`)} = ${read(id)}`)
193
- .reduce((all, part) => raw `${all} AND ${part}`);
194
- // A set-based engine hands the rows as a table, which an `UPDATE` has to name in a `FROM` before its
195
- // condition can read one: `inserted."id"` binds to nothing on its own.
196
- const from = features.rows === 'set' ? ` FROM ${newName}` : '';
197
- // Only where the stamp still differs: this `UPDATE` fires the trigger again, and with recursive triggers
198
- // on, the restatement it runs then finds nothing left to change instead of recursing without end.
199
- const differs = dialect.neExpr(`${table}.${column}`, dialect.compileDdl(value, meta.entity));
200
- return raw `UPDATE ${text(table)} SET ${text(column)} = ${value}${text(from)} WHERE ${keyed} AND ${text(differs)};`;
195
+ const table = refs(entity);
196
+ const keyed = meta.ids.map((id) => raw `${table[id]} = ${newRow[id]}`);
197
+ // Read through the write's own qualifier, as `keyed` is: `inserted` holds the same column name.
198
+ const stamped = dialect.compileDdl(value, entity);
199
+ const differs = raw(({ ctx, escapedPrefix }) => ctx.append(dialect.neExpr(`${escapedPrefix}${dialect.escapedColumnName(meta, key)}`, stamped)));
200
+ const where = { $and: [...keyed, differs] };
201
+ return raw(({ ctx, rows }) => dialect.triggerWrite(ctx, { kind: 'update', entity, set: { [key]: value }, where }, rows));
201
202
  }
202
203
  /** A dollar quote the body does not contain, so no `$$` in it - a literal, a comment - ends the function early. */
203
204
  function dollarQuote(body) {
@@ -207,23 +208,38 @@ function dollarQuote(body) {
207
208
  }
208
209
  return tag;
209
210
  }
210
- /** SQL already written out, for the identifiers a statement splices rather than binds. */
211
- function text(sql) {
212
- return raw((opts) => opts.ctx.append(sql));
213
- }
214
211
  /**
215
212
  * Whether any watched column moved, null-safely, or `undefined` where none is watched: the two records
216
213
  * compared on a row-based engine, and on a set-based one the same question over a join of its two tables.
217
214
  */
218
- function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }, column) {
215
+ function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
219
216
  if (!of.length) {
220
217
  return undefined;
221
218
  }
222
- const differs = of.map((key) => dialect.neExpr(`${oldName}.${column(key)}`, `${newName}.${column(key)}`));
219
+ const differs = of.map((key) => {
220
+ const column = dialect.escapedColumnName(meta, key);
221
+ return dialect.neExpr(`${oldName}.${column}`, `${newName}.${column}`);
222
+ });
223
223
  const moved = differs.length > 1 ? `(${differs.join(' OR ')})` : differs.join('');
224
- if (dialect.features.triggers.rows === 'row') {
225
- return moved;
224
+ const source = rowsFrom(dialect, meta, 'UPDATE');
225
+ return source ? `EXISTS (SELECT 1 ${source} WHERE ${moved})` : moved;
226
+ }
227
+ /**
228
+ * Where a set-based engine's body reads the rows it fires for, as the `FROM` a statement names them in:
229
+ * `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key. None on a
230
+ * row-based engine, whose body reads `NEW` and `OLD` bare.
231
+ */
232
+ function rowsFrom(dialect, meta, operation) {
233
+ if (dialect.features.triggers.rows !== 'set') {
234
+ return undefined;
235
+ }
236
+ const { $new, $old } = rowNames(dialect);
237
+ if (operation !== 'UPDATE') {
238
+ return `FROM ${operation === 'INSERT' ? $new : $old}`;
226
239
  }
227
- const keyed = meta.ids.map((id) => `${newName}.${column(id)} = ${oldName}.${column(id)}`).join(' AND ');
228
- return `EXISTS (SELECT 1 FROM ${newName} JOIN ${oldName} ON ${keyed} WHERE ${moved})`;
240
+ const keyed = meta.ids.map((id) => {
241
+ const column = dialect.escapedColumnName(meta, id);
242
+ return `${$new}.${column} = ${$old}.${column}`;
243
+ });
244
+ return `FROM ${$new} JOIN ${$old} ON ${keyed.join(' AND ')}`;
229
245
  }
@@ -90,9 +90,7 @@ export declare class MongoDialect extends AbstractDialect {
90
90
  * apply to JSON paths.
91
91
  */
92
92
  private assertKnownPathRoot;
93
- /**
94
- * Transform UQL operators to MongoDB operators.
95
- */
93
+ /** Transform UQL operators to MongoDB operators, refusing as `refusal` a key that is none. */
96
94
  private transformOperators;
97
95
  /**
98
96
  * What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array
@@ -1,7 +1,7 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
3
  import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
- import { GROUP_OPS, groupClauses, isGroupOp } from '../dialect/operators.js';
4
+ import { betweenBounds, GROUP_OPS, groupClauses, isGroupOp, whereOperators } from '../dialect/operators.js';
5
5
  import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
6
6
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
7
7
  import { COUNT_RESULT_KEY } from '../type/query.js';
@@ -334,12 +334,10 @@ export class MongoDialect extends AbstractDialect {
334
334
  }
335
335
  throw new UqlUsageError(`path ${key} does not exist in ${entityName(meta)}`);
336
336
  }
337
- /**
338
- * Transform UQL operators to MongoDB operators.
339
- */
340
- transformOperators(ops) {
337
+ /** Transform UQL operators to MongoDB operators, refusing as `refusal` a key that is none. */
338
+ transformOperators(ops, refusal = 'unknown operator') {
341
339
  const result = {};
342
- for (const [op, val] of Object.entries(ops)) {
340
+ for (const [op, val] of whereOperators(ops, refusal)) {
343
341
  // `$elemMatch`'s value is itself a condition, so the operators inside it need the same
344
342
  // mapping - passing it through raw sends UQL-only operators (`$startsWith`, `$between`, ...)
345
343
  // straight to the server, which rejects them as unknown.
@@ -349,7 +347,7 @@ export class MongoDialect extends AbstractDialect {
349
347
  }
350
348
  // `$not` wraps a condition too, so a uql-only operator inside it (`$isNull`, `$startsWith`) is mapped.
351
349
  if (op === '$not' && isOperatorObject(val)) {
352
- result[op] = this.transformOperators(val);
350
+ result[op] = this.transformOperators(val, refusal);
353
351
  continue;
354
352
  }
355
353
  // An object or an array is matched by what it holds, as the SQL engines read it, where native `$all`
@@ -374,7 +372,7 @@ export class MongoDialect extends AbstractDialect {
374
372
  // Structural transforms
375
373
  switch (op) {
376
374
  case '$between': {
377
- const [min, max] = val;
375
+ const [min, max] = betweenBounds(val);
378
376
  result['$gte'] = min;
379
377
  result['$lte'] = max;
380
378
  break;
@@ -390,9 +388,6 @@ export class MongoDialect extends AbstractDialect {
390
388
  // reads: converting a distance would mean guessing the metric, so this refuses.
391
389
  throw new UqlUsageError('$near is not supported on MongoDB: Atlas scores by index-defined similarity, not distance. ' +
392
390
  "Project the score with $sort's $project and filter on it instead.");
393
- default:
394
- result[op] = val;
395
- break;
396
391
  }
397
392
  }
398
393
  return result;
@@ -1072,7 +1067,7 @@ export class MongoDialect extends AbstractDialect {
1072
1067
  if (columnFamily(field.type) === 'string') {
1073
1068
  return;
1074
1069
  }
1075
- throw new TypeError(`'${entityName(meta)}.${meta.ids[0]}' is declared '${declaredTypeName(field.type)}' and left to the ` +
1070
+ throw new UqlUsageError(`'${entityName(meta)}.${meta.ids[0]}' is declared '${declaredTypeName(field.type)}' and left to the ` +
1076
1071
  'database, which MongoDB cannot do: the only key it generates is an ObjectId, read back as a string. ' +
1077
1072
  "Declare the key as a string, or give it an 'onInsert' generator.");
1078
1073
  }
@@ -1282,7 +1277,7 @@ export class MongoDialect extends AbstractDialect {
1282
1277
  // on identical input. Keeping only numbers and objects dropped a string or boolean without a
1283
1278
  // word, handing back every group instead of the filtered ones.
1284
1279
  if (isOperatorMap(condition)) {
1285
- filter[alias] = this.transformOperators(condition);
1280
+ filter[alias] = this.transformOperators(condition, 'unsupported HAVING operator');
1286
1281
  }
1287
1282
  else {
1288
1283
  filter[alias] = Array.isArray(condition) ? { $in: condition } : condition;
@@ -1368,6 +1363,6 @@ function sortNulls(value) {
1368
1363
  function assertReadable(meta, key) {
1369
1364
  const field = meta.fields[key];
1370
1365
  if (field?.computed && !aggregateOf(field)) {
1371
- throw new TypeError(`cannot read '${meta.entity.name}.${key}' on MongoDB: a 'computed' field writing SQL is not something a document engine evaluates`);
1366
+ throw new UqlUsageError(`cannot read '${meta.entity.name}.${key}' on MongoDB: a 'computed' field writing SQL is not something a document engine evaluates`);
1372
1367
  }
1373
1368
  }
@@ -2,7 +2,7 @@ import { AGGREGATE_VALUE_ALIAS } from '../dialect/aliases.js';
2
2
  import { hasRequiredJoin } from '../dialect/queryJoins.js';
3
3
  import { fieldOf, getMeta, namesKey, soleIdOf } from '../entity/index.js';
4
4
  import { AbstractQuerier, enrichError } from '../querier/index.js';
5
- import { clone, getKeys, getSoftDeleteValue, hasKeys, hasTriggers, populatesRelations, textSortOf, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
5
+ import { clone, getKeys, getSoftDeleteValue, hasKeys, hasTriggers, populatesRelations, textSortOf, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, whereEach, } from '../util/index.js';
6
6
  import { UqlUsageError } from '../util/uqlError.js';
7
7
  /**
8
8
  * `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
@@ -223,11 +223,7 @@ export class MongodbQuerier extends AbstractQuerier {
223
223
  return update;
224
224
  }
225
225
  buildConflictFilter(entity, conflictPaths, item) {
226
- const where = getKeys(conflictPaths).reduce((acc, key) => {
227
- acc[key] = item[key];
228
- return acc;
229
- }, {});
230
- return this.dialect.where(entity, where);
226
+ return this.dialect.where(entity, whereEach(getKeys(conflictPaths), (key) => item[key]));
231
227
  }
232
228
  async internalUpsertOne(entity, conflictPaths, payload) {
233
229
  refuseTriggers(entity);
@@ -314,7 +310,7 @@ export class MongodbQuerier extends AbstractQuerier {
314
310
  /** Every read and write goes through here, which makes it where a released querier is caught. */
315
311
  collection(entity) {
316
312
  if (this.released) {
317
- throw new TypeError('querier already released');
313
+ throw new UqlUsageError('querier already released');
318
314
  }
319
315
  const { name } = getMeta(entity);
320
316
  return this.db.collection(name);
@@ -87,7 +87,7 @@ export declare abstract class AbstractQuerier implements Querier {
87
87
  * The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
88
88
  * same rows the statement wrote.
89
89
  */
90
- insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
90
+ insertMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
91
91
  /** Writes `rows`, and onto each one the key the database generated for it, where it can tell. */
92
92
  protected abstract internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
93
93
  updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
@@ -118,9 +118,14 @@ export declare abstract class AbstractQuerier implements Querier {
118
118
  protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
119
119
  restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
120
120
  restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
121
+ /**
122
+ * An update the library writes of its own, hooked as a caller's is but carrying no version: a restore's
123
+ * cleared stamp, or a row pointed at the relation just inserted for it.
124
+ */
125
+ private unversionedUpdate;
121
126
  /** Fires `beforeUpsert`/`afterUpsert`: which branch a row takes is the database's to decide, so neither the insert's nor the update's pair fits. */
122
127
  upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
123
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
128
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: readonly EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
124
129
  protected abstract internalUpsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
125
130
  protected abstract internalUpsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
126
131
  deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
@@ -137,7 +142,7 @@ export declare abstract class AbstractQuerier implements Querier {
137
142
  * upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
138
143
  * inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
139
144
  */
140
- saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
145
+ saveMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
141
146
  /** Writes each inserted row's relations, one set of statements per relation whatever the number of rows. */
142
147
  protected insertRelations<E extends object>(entity: Type<E>, rows: E[]): Promise<void>;
143
148
  /** `EntityId` because a settled composite row is an object, which {@link childrenOf} reads each foreign key column out of. */
@@ -182,7 +187,7 @@ export declare abstract class AbstractQuerier implements Querier {
182
187
  /** Runs `task` after everything already queued, one at a time. Not re-entrant: never nest `serialize` calls. */
183
188
  protected serialize<T>(task: () => Promise<T>): Promise<T>;
184
189
  /** Runs `task`, logs `query` with its duration, and tags a failure with it: a method, since a decorator would lose the generics. */
185
- protected timed<T>(query: string, values: unknown[] | undefined, task: () => Promise<T>): Promise<T>;
190
+ protected timed<T>(query: string, values: readonly unknown[] | undefined, task: () => Promise<T>): Promise<T>;
186
191
  abstract beginTransaction(opts?: TransactionOptions): Promise<void>;
187
192
  /** Strict: this is the check that catches a forgotten `beginTransaction`. */
188
193
  abstract commitTransaction(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { assertSoleId, getMeta, idOf, namesKey, relationOf } from '../entity/index.js';
2
+ import { namesRows } from '../dialect/operators.js';
2
3
  import { parseQueryLock } from '../type/index.js';
3
- import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, hasKeys, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, keySet, isPagedQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereAnyOf, whereEach, whereIds, whereWith, withoutSoftDeleteFilter, } from '../util/index.js';
4
5
  import { UqlOptimisticLockError, UqlUsageError } from '../util/uqlError.js';
5
6
  import { enrichError } from './queryError.js';
6
7
  /**
@@ -35,7 +36,7 @@ function soleParentColumn(relOpts) {
35
36
  * every table look narrowed, which is the case this exists to catch.
36
37
  */
37
38
  function assertNamesRows(entity, method, q, opts) {
38
- if (opts?.unfiltered || hasKeys(q?.$where) || q?.$limit !== undefined) {
39
+ if (opts?.unfiltered || namesRows(q?.$where) || q?.$limit !== undefined) {
39
40
  return;
40
41
  }
41
42
  throw new UqlUsageError(`'${method}' over '${entity.name}' names no rows, so it would address every one: pass '{ unfiltered: true }' to mean it`);
@@ -53,7 +54,7 @@ function lockVersion(meta, key, q, row) {
53
54
  const next = typeof expected === 'bigint' ? expected + 1n : expected + 1;
54
55
  // Spread, as every other added predicate here is: one flat `AND`, and a caller already filtering on
55
56
  // the version contradicts itself into matching nothing, which is what they asked for.
56
- return { expected, next, q: { ...q, $where: { ...q.$where, [key]: expected } } };
57
+ return { expected, next, q: { ...q, $where: whereWith(key, expected, q.$where) } };
57
58
  }
58
59
  /**
59
60
  * Refuses a write that cannot carry the lock, rather than writing over whatever the row holds now.
@@ -72,8 +73,8 @@ function assertUnversioned(meta, what) {
72
73
  * an `UPDATE` - reads the ids and writes them separately, putting the race back in the gap between.
73
74
  */
74
75
  function assertLockableUpdate(meta, q, settles) {
75
- const where = q.$where;
76
- const namesOneRow = meta.ids.every((key) => where?.[key] !== undefined && isScalarId(where[key]));
76
+ const where = { ...q.$where };
77
+ const namesOneRow = meta.ids.every((key) => where[key] !== undefined && isScalarId(where[key]));
77
78
  if (!namesOneRow || settles) {
78
79
  throw new UqlUsageError(`cannot update '${entityName(meta)}' this way: a versioned row is matched and written in one statement, so it is named by its ${meta.ids.map((id) => `'${id}'`).join(', ')}, takes no '$sort', '$limit' or '$skip', writes no relation, and filters by none`);
79
80
  }
@@ -290,9 +291,7 @@ export class AbstractQuerier {
290
291
  */
291
292
  async throwStaleVersion(entity, key, q, expected, opts) {
292
293
  const meta = getMeta(entity);
293
- const where = q.$where;
294
- const byId = Object.fromEntries(meta.ids.map((id) => [id, where[id]]));
295
- const row = await this.findOne(entity, { $select: { [key]: true }, $where: byId }, opts);
294
+ const row = await this.findOne(entity, { $select: keySet([key]), $where: whereIds(meta, idOf(meta, { ...q.$where })) }, opts);
296
295
  const actual = row?.[key];
297
296
  const message = actual === undefined
298
297
  ? `no row of '${entityName(meta)}' has that id any more: it is gone`
@@ -329,10 +328,17 @@ export class AbstractQuerier {
329
328
  if (!meta.softDelete) {
330
329
  throw new UqlUsageError(`'${entity.name}' has not enabled 'softDelete'`);
331
330
  }
332
- const $where = { ...q.$where, [meta.softDelete]: { $ne: null } };
331
+ const $where = whereWith(meta.softDelete, { $ne: null }, q.$where);
333
332
  // No version: a restore only undoes the stamp a delete left, which takes none either, and two of
334
333
  // them racing agree on the result anyway. A lock is for content, and a restore writes none.
335
- return this.hooked(entity, 'Update', [{ [meta.softDelete]: null }], ([row]) => this.updateRows(entity, { ...q, $where }, row, { filters: { softDelete: false } }, undefined));
334
+ return this.unversionedUpdate(entity, { ...q, $where }, { [meta.softDelete]: null }, { filters: { softDelete: false } });
335
+ }
336
+ /**
337
+ * An update the library writes of its own, hooked as a caller's is but carrying no version: a restore's
338
+ * cleared stamp, or a row pointed at the relation just inserted for it.
339
+ */
340
+ unversionedUpdate(entity, q, payload, opts) {
341
+ return this.hooked(entity, 'Update', [payload], ([row]) => this.updateRows(entity, q, row, opts, undefined));
336
342
  }
337
343
  /** Fires `beforeUpsert`/`afterUpsert`: which branch a row takes is the database's to decide, so neither the insert's nor the update's pair fits. */
338
344
  async upsertOne(entity, conflictPaths, payload) {
@@ -426,7 +432,7 @@ export class AbstractQuerier {
426
432
  }
427
433
  }
428
434
  if (toUpsert.length) {
429
- const conflictPaths = Object.fromEntries(meta.ids.map((key) => [key, true]));
435
+ const conflictPaths = keySet(meta.ids);
430
436
  const { ids: upserted } = await this.upsertMany(entity, conflictPaths, toUpsert.map((index) => payload[index]));
431
437
  for (let position = 0; position < toUpsert.length; position++) {
432
438
  ids[toUpsert[position]] = upserted[position];
@@ -492,7 +498,7 @@ export class AbstractQuerier {
492
498
  const savedIds = await this.saveMany(relEntity, children.map(({ row }) => row));
493
499
  // A link needs the target's id, which a MySQL batch mixing supplied and generated keys cannot report.
494
500
  if (savedIds.includes(undefined)) {
495
- throw new TypeError(`'${relEntity.name}' rows saved through '${holder.name}' reported no id, so they cannot be linked. ` +
501
+ throw new UqlUsageError(`'${relEntity.name}' rows saved through '${holder.name}' reported no id, so they cannot be linked. ` +
496
502
  'Insert them with their own ids, or save the relation in its own statement.');
497
503
  }
498
504
  const [targetColumn] = targetKeyColumns(relOpts, 1);
@@ -506,7 +512,8 @@ export class AbstractQuerier {
506
512
  const pointing = writes.filter(({ value }) => value);
507
513
  const referenceIds = await this.insertMany(relEntity, pointing.map(({ value }) => value));
508
514
  for (const [index, { id }] of pointing.entries()) {
509
- await this.updateOneById(entity, id, { [localColumn]: referenceIds[index] });
515
+ assertIdValue(entity, id);
516
+ await this.unversionedUpdate(entity, { $where: whereIds(getMeta(entity), id) }, { [localColumn]: referenceIds[index] });
510
517
  }
511
518
  }
512
519
  /**
@@ -573,8 +580,8 @@ export class AbstractQuerier {
573
580
  const [idKey] = meta.ids;
574
581
  const keys = getKeys(conflictPaths);
575
582
  const q = {
576
- $select: Object.fromEntries([idKey, ...keys].map((key) => [key, true])),
577
- $where: { $or: rows.map((row) => Object.fromEntries(keys.map((key) => [key, row[key]]))) },
583
+ $select: keySet([idKey, ...keys]),
584
+ $where: whereAnyOf(rows.map((row) => whereEach(keys, (key) => row[key]))),
578
585
  };
579
586
  const found = await this.internalFindMany(entity, q, { filters: withoutSoftDeleteFilter(undefined) });
580
587
  const byConflict = new Map();
@@ -590,12 +597,12 @@ export class AbstractQuerier {
590
597
  * the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
591
598
  */
592
599
  async hooked(entity, event, payloads, write) {
593
- // The one place a caller's write becomes the row the rest of the library handles. They are the
594
- // same object: a write is the entity's data without the keys the database fills, which
595
- // TypeScript cannot relate across an entity it has not resolved.
600
+ // The one place a write becomes the row the rest of the library handles. They are the same object:
601
+ // a write is the entity's data without the keys the database fills, which TypeScript cannot relate
602
+ // across an entity it has not resolved.
596
603
  const asRows = payloads;
597
604
  await this.emitHook(entity, `before${event}`, asRows);
598
- const rows = clone(asRows);
605
+ const rows = asRows.map((row) => clone(row));
599
606
  const result = await write(rows);
600
607
  await this.emitHook(entity, `after${event}`, rows);
601
608
  return result;
@@ -38,13 +38,13 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
38
38
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
39
39
  estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
40
40
  insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
41
- insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
41
+ insertMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
42
42
  updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
43
43
  updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
44
44
  upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
45
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
45
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: readonly EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
46
46
  saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
47
- saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
47
+ saveMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
48
48
  deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
49
49
  deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
50
50
  restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
@@ -32,8 +32,8 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
32
32
  * caught for every SQL backend.
33
33
  */
34
34
  protected lazyConnect(): Promise<void>;
35
- all<T>(query: string, values?: unknown[]): Promise<T[]>;
36
- run(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
35
+ all<T>(query: string, values?: readonly unknown[]): Promise<T[]>;
36
+ run(query: string, values?: readonly unknown[]): Promise<QueryUpdateResult>;
37
37
  /** The rows of a statement the dialect builds. */
38
38
  private query;
39
39
  /** Runs a statement the dialect builds. */
@@ -91,7 +91,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
91
91
  */
92
92
  async lazyConnect() {
93
93
  if (this.released) {
94
- throw new TypeError('querier already released');
94
+ throw new UqlUsageError('querier already released');
95
95
  }
96
96
  }
97
97
  async all(query, values) {
@@ -6,6 +6,6 @@ import { AbstractQuerierPool } from './abstractQuerierPool.js';
6
6
  * the connection-per-call semantics.
7
7
  */
8
8
  export declare abstract class AbstractSqlQuerierPool<Q extends SqlQuerier, D extends AbstractSqlDialect> extends AbstractQuerierPool<Q, D> implements SqlQuerierPool<Q, D> {
9
- all<T>(query: string, values?: unknown[]): Promise<T[]>;
10
- run(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
9
+ all<T>(query: string, values?: readonly unknown[]): Promise<T[]>;
10
+ run(query: string, values?: readonly unknown[]): Promise<QueryUpdateResult>;
11
11
  }
@@ -6,7 +6,7 @@ import { type QueryErrorKind } from '../util/uqlError.js';
6
6
  */
7
7
  export interface QueryError extends Error {
8
8
  query?: string;
9
- values?: unknown[];
9
+ values?: readonly unknown[];
10
10
  }
11
11
  /**
12
12
  * Names what `err` ran into on any engine, or `undefined` for anything else. Pure: the error is only
@@ -18,4 +18,4 @@ export declare function queryErrorKind(err: unknown): QueryErrorKind | undefined
18
18
  * control flow it is. `values` are attached only when `logger?.willLogValues()`: they already surface
19
19
  * in the logs then, so this opens no new leak.
20
20
  */
21
- export declare function enrichError(err: unknown, logger: LoggerWrapper | undefined, query: string, values?: unknown[]): unknown;
21
+ export declare function enrichError(err: unknown, logger: LoggerWrapper | undefined, query: string, values?: readonly unknown[]): unknown;