uql-orm 0.81.0 → 0.83.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 (113) 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/cockroachdb/crdbQuerierPool.js +2 -2
  9. package/dist/dialect/abstractSqlDialect.d.ts +16 -6
  10. package/dist/dialect/abstractSqlDialect.js +110 -41
  11. package/dist/dialect/hydrateColumn.js +2 -12
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
  14. package/dist/dialect/operators.d.ts +7 -1
  15. package/dist/dialect/operators.js +13 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  17. package/dist/dialect/pgLikeSqlDialect.js +13 -4
  18. package/dist/entity/metadata/definition.d.ts +1 -2
  19. package/dist/entity/metadata/definition.js +37 -39
  20. package/dist/http/handler.js +5 -4
  21. package/dist/http/query.d.ts +1 -1
  22. package/dist/http/query.js +2 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/maria/mariadbQuerierPool.js +4 -2
  26. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  27. package/dist/migrate/assertCliConfig.js +7 -6
  28. package/dist/migrate/bin.js +0 -0
  29. package/dist/migrate/builder/expressions.d.ts +2 -0
  30. package/dist/migrate/builder/expressions.js +20 -10
  31. package/dist/migrate/builder/tableBuilder.js +1 -1
  32. package/dist/migrate/cli-config.js +5 -4
  33. package/dist/migrate/ddl/indexDdl.js +4 -3
  34. package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
  35. package/dist/migrate/ddl/pgIndexDdl.js +2 -1
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  37. package/dist/migrate/ddl/tableDdl.js +2 -1
  38. package/dist/migrate/generator/mongoCommand.js +2 -1
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
  41. package/dist/migrate/indexPredicate.js +2 -1
  42. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
  43. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  44. package/dist/migrate/introspection/mongoIntrospector.js +3 -2
  45. package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
  46. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
  47. package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
  48. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  49. package/dist/migrate/migrationTarget.js +2 -1
  50. package/dist/migrate/migrator.js +2 -1
  51. package/dist/migrate/schemaGenerator.js +5 -4
  52. package/dist/migrate/storage/databaseStorage.js +1 -1
  53. package/dist/migrate/triggerSql.d.ts +1 -1
  54. package/dist/migrate/triggerSql.js +77 -61
  55. package/dist/mongo/mongoDialect.d.ts +1 -3
  56. package/dist/mongo/mongoDialect.js +9 -14
  57. package/dist/mongo/mongodbQuerier.js +7 -10
  58. package/dist/mssql/mssqlQuerier.d.ts +2 -0
  59. package/dist/mssql/mssqlQuerier.js +8 -5
  60. package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
  61. package/dist/mysql/mysql2QuerierPool.js +20 -2
  62. package/dist/neon/neonQuerierPool.js +2 -2
  63. package/dist/pglite/pgliteQuerierPool.js +10 -4
  64. package/dist/postgres/pgQuerierPool.js +2 -2
  65. package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
  66. package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
  67. package/dist/querier/abstractQuerier.d.ts +9 -4
  68. package/dist/querier/abstractQuerier.js +26 -19
  69. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  70. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  71. package/dist/querier/abstractSqlQuerier.js +1 -1
  72. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  73. package/dist/querier/queryError.d.ts +2 -2
  74. package/dist/schema/canonicalType.d.ts +3 -0
  75. package/dist/schema/canonicalType.js +31 -9
  76. package/dist/schema/schemaASTBuilder.js +2 -1
  77. package/dist/schema/schemaASTDiffer.js +4 -2
  78. package/dist/sqlite/sqliteDialect.d.ts +1 -3
  79. package/dist/sqlite/sqliteDialect.js +3 -6
  80. package/dist/type/dialect.d.ts +23 -1
  81. package/dist/type/entity.d.ts +16 -12
  82. package/dist/type/logger.d.ts +2 -2
  83. package/dist/type/querier.d.ts +3 -3
  84. package/dist/type/query.d.ts +3 -13
  85. package/dist/type/queryAggregate.d.ts +4 -10
  86. package/dist/type/queryRaw.d.ts +17 -3
  87. package/dist/type/queryRaw.js +2 -1
  88. package/dist/type/queryWhere.d.ts +7 -7
  89. package/dist/type/universalQuerier.d.ts +3 -3
  90. package/dist/type/vector.d.ts +2 -1
  91. package/dist/type/vector.js +2 -1
  92. package/dist/util/date.d.ts +11 -0
  93. package/dist/util/date.js +19 -0
  94. package/dist/util/dialect.util.d.ts +13 -5
  95. package/dist/util/dialect.util.js +28 -20
  96. package/dist/util/field.util.d.ts +4 -4
  97. package/dist/util/field.util.js +10 -2
  98. package/dist/util/fieldOption.util.d.ts +5 -3
  99. package/dist/util/fieldOption.util.js +6 -5
  100. package/dist/util/hook.util.d.ts +1 -1
  101. package/dist/util/hook.util.js +8 -1
  102. package/dist/util/index.d.ts +1 -0
  103. package/dist/util/index.js +1 -0
  104. package/dist/util/logger.d.ts +3 -3
  105. package/dist/util/object.util.js +3 -2
  106. package/dist/util/raw.d.ts +6 -7
  107. package/dist/util/raw.js +10 -12
  108. package/dist/util/sqlLiteral.d.ts +8 -1
  109. package/dist/util/sqlLiteral.js +14 -9
  110. package/dist/util/triggerWrite.d.ts +15 -0
  111. package/dist/util/triggerWrite.js +20 -0
  112. package/package.json +1 -1
  113. package/skills/uql-orm/SKILL.md +4 -4
@@ -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)`
@@ -13,12 +13,13 @@ function asksForNoRows(q) {
13
13
  return q.$limit === 0;
14
14
  }
15
15
  /**
16
- * MongoDB has no triggers, so a write to an entity declaring one - a stamp included - would skip it
17
- * silently. Refused instead, as a query naming SQL is.
16
+ * MongoDB runs no trigger within a write (Atlas Database Triggers fire after the commit), so a write to
17
+ * an entity declaring one - a stamp included - would skip it silently. Refused instead, as a query naming
18
+ * SQL is. The why, in `architecture/triggers.md`.
18
19
  */
19
20
  function refuseTriggers(entity) {
20
21
  if (hasTriggers(getMeta(entity))) {
21
- throw new UqlUsageError(`'${entity.name}' declares triggers, which MongoDB has none of: a write here would skip them. ` +
22
+ throw new UqlUsageError(`'${entity.name}' declares triggers, which MongoDB cannot run within a write: a write here would skip them. ` +
22
23
  'Keep the entity on a SQL engine, or drop its triggers and stamps.');
23
24
  }
24
25
  }
@@ -223,11 +224,7 @@ export class MongodbQuerier extends AbstractQuerier {
223
224
  return update;
224
225
  }
225
226
  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);
227
+ return this.dialect.where(entity, whereEach(getKeys(conflictPaths), (key) => item[key]));
231
228
  }
232
229
  async internalUpsertOne(entity, conflictPaths, payload) {
233
230
  refuseTriggers(entity);
@@ -314,7 +311,7 @@ export class MongodbQuerier extends AbstractQuerier {
314
311
  /** Every read and write goes through here, which makes it where a released querier is caught. */
315
312
  collection(entity) {
316
313
  if (this.released) {
317
- throw new TypeError('querier already released');
314
+ throw new UqlUsageError('querier already released');
318
315
  }
319
316
  const { name } = getMeta(entity);
320
317
  return this.db.collection(name);
@@ -1,3 +1,4 @@
1
+ import { DateTime2 } from 'mssql';
1
2
  import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
2
3
  import type { QueryUpdateResult, RawRow, TransactionOptions } from '../type/index.js';
3
4
  /** What `tedious` hands back for one statement, whichever shape it took. */
@@ -17,6 +18,7 @@ type MsSqlRowStream = AsyncIterable<unknown> & {
17
18
  /** The part of an `mssql` `Request` a querier drives. */
18
19
  type MsSqlRequest = {
19
20
  input(name: string, value: unknown): unknown;
21
+ input(name: string, type: typeof DateTime2, value: unknown): unknown;
20
22
  query(command: string): Promise<MsSqlResult>;
21
23
  toReadableStream(): MsSqlRowStream;
22
24
  cancel(): unknown;
@@ -1,4 +1,4 @@
1
- import { ISOLATION_LEVEL } from 'mssql';
1
+ import { DateTime2, ISOLATION_LEVEL } from 'mssql';
2
2
  import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
3
3
  import { decodeWireTypes } from './mssqlWireTypes.js';
4
4
  /**
@@ -9,13 +9,16 @@ import { decodeWireTypes } from './mssqlWireTypes.js';
9
9
  export class MsSqlQuerier extends AbstractPoolQuerier {
10
10
  #transaction;
11
11
  /**
12
- * Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `tedious` infers
13
- * a type from the JS value, which is why a `Date` and a `Uint8Array` reach it unconverted - the
14
- * inference is right for both, and wrong only for a bare `null`, which it calls `NVarChar`.
12
+ * Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `mssql` infers a type
13
+ * from the JS value, right for a `Uint8Array` and harmlessly wrong for a bare `null` (`NVarChar`), but
14
+ * a `Date` it binds as the legacy `DATETIME`, whose 1/300 s steps no `DATETIME2` column compares equal to.
15
15
  */
16
16
  #request(values) {
17
17
  const request = this.#transaction ? this.#transaction.request() : this.getConn().request();
18
- values?.forEach((value, index) => request.input(`p${index + 1}`, value));
18
+ values?.forEach((value, index) => {
19
+ const name = `p${index + 1}`;
20
+ return value instanceof Date ? request.input(name, DateTime2, value) : request.input(name, value);
21
+ });
19
22
  return request;
20
23
  }
21
24
  async internalAll(query, values) {
@@ -4,6 +4,7 @@ import type { ExtraOptions } from '../type/index.js';
4
4
  import { MySql2Querier } from './mysql2Querier.js';
5
5
  import { MySqlDialect } from './mysqlDialect.js';
6
6
  export declare class MySql2QuerierPool extends AbstractSqlQuerierPool<MySql2Querier, MySqlDialect> {
7
+ #private;
7
8
  readonly pool: Pool;
8
9
  constructor(opts: PoolOptions, extra?: ExtraOptions);
9
10
  getQuerier(): Promise<MySql2Querier>;
@@ -5,14 +5,32 @@ import { MySql2Querier } from './mysql2Querier.js';
5
5
  import { MySqlDialect } from './mysqlDialect.js';
6
6
  export class MySql2QuerierPool extends AbstractSqlQuerierPool {
7
7
  pool;
8
+ #utcSessions = new WeakSet();
8
9
  constructor(opts, extra) {
9
10
  super(new MySqlDialect(dialectOptionsFrom(extra)), extra);
10
11
  // A BIGINT past 2^53 as its exact text rather than a rounded number, the rule every driver here
11
12
  // decodes by (`decodeWideNumber`); within that range it stays a number, and DECIMAL is untouched.
12
- this.pool = createPool({ supportBigNumbers: true, ...opts });
13
+ // A date reads as the UTC it holds, whichever zone the process runs in.
14
+ this.pool = createPool({ supportBigNumbers: true, timezone: 'Z', ...opts });
13
15
  }
14
16
  async getQuerier() {
15
- return new MySql2Querier(() => this.pool.getConnection(), this.dialect, this.extra);
17
+ return new MySql2Querier(() => this.#connection(), this.dialect, this.extra);
18
+ }
19
+ /** A connection whose session is UTC too, so `NOW()` agrees with a bound date: set once per connection. */
20
+ async #connection() {
21
+ const connection = await this.pool.getConnection();
22
+ if (this.#utcSessions.has(connection.connection)) {
23
+ return connection;
24
+ }
25
+ try {
26
+ await connection.query("SET time_zone = '+00:00'");
27
+ }
28
+ catch (error) {
29
+ connection.release();
30
+ throw error;
31
+ }
32
+ this.#utcSessions.add(connection.connection);
33
+ return connection;
16
34
  }
17
35
  async end() {
18
36
  await this.pool.end();
@@ -1,11 +1,11 @@
1
1
  import { Pool, types } from '@neondatabase/serverless';
2
2
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
3
3
  import { AbstractPgQuerierPool } from '../postgres/abstractPgQuerierPool.js';
4
- import { numericTypes } from '../postgres/pgNumericTypes.js';
4
+ import { wireTypes } from '../postgres/pgWireTypes.js';
5
5
  import { PostgresDialect } from '../postgres/postgresDialect.js';
6
6
  export class NeonQuerierPool extends AbstractPgQuerierPool {
7
7
  constructor(opts, extra) {
8
8
  // Neon's own `types`, not `pg`'s: this entry has to load on an edge runtime where `pg` is absent.
9
- super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ types: numericTypes(types), ...opts }), extra);
9
+ super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ types: wireTypes(types), ...opts }), extra);
10
10
  }
11
11
  }
@@ -1,6 +1,7 @@
1
1
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
2
  import { PostgresDialect } from '../postgres/postgresDialect.js';
3
3
  import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
4
+ import { decodeDate } from '../util/date.js';
4
5
  import { decodeWideNumber } from '../util/wideNumber.js';
5
6
  import { PgliteQuerier } from './pgliteQuerier.js';
6
7
  /**
@@ -18,12 +19,17 @@ export class PgliteQuerierPool extends AbstractSharedHandleQuerierPool {
18
19
  }
19
20
  async openDb() {
20
21
  const { PGlite, types } = await import('@electric-sql/pglite');
21
- // INT8 by the one wide-integer rule, where PGlite's own answers a `bigint` past 2^53; a caller's own
22
- // `parsers` still win. The declared return type is what checks {@link PgliteDatabase} against the
23
- // real driver, so no cast is needed here or anywhere below it.
22
+ // INT8 by the one wide-integer rule, where PGlite's own answers a `bigint` past 2^53, and a zoneless
23
+ // TIMESTAMP or a DATE as UTC, as every pool reads one; a caller's own `parsers` still win. The declared
24
+ // return type is what checks {@link PgliteDatabase} against the real driver, so no cast is needed below it.
24
25
  return PGlite.create(this.dataDir, {
25
26
  ...this.opts,
26
- parsers: { [types.INT8]: decodeWideNumber, ...this.opts?.parsers },
27
+ parsers: {
28
+ [types.INT8]: decodeWideNumber,
29
+ [types.TIMESTAMP]: decodeDate,
30
+ [types.DATE]: decodeDate,
31
+ ...this.opts?.parsers,
32
+ },
27
33
  });
28
34
  }
29
35
  buildQuerier(db) {
@@ -1,12 +1,12 @@
1
1
  import { Pool, types } from 'pg';
2
2
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
3
3
  import { AbstractPgQuerierPool } from './abstractPgQuerierPool.js';
4
- import { numericTypes } from './pgNumericTypes.js';
4
+ import { wireTypes } from './pgWireTypes.js';
5
5
  import { PostgresDialect } from './postgresDialect.js';
6
6
  export class PgQuerierPool extends AbstractPgQuerierPool {
7
7
  constructor(opts, extra) {
8
8
  // keepAlive reduces (but can't eliminate) idle connections being silently
9
9
  // dropped by NATs/firewalls on long-lived remote connections.
10
- super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: numericTypes(types), ...opts }), extra);
10
+ super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: wireTypes(types), ...opts }), extra);
11
11
  }
12
12
  }
@@ -11,8 +11,9 @@ type PgTypes = {
11
11
  };
12
12
  /**
13
13
  * Decodes `INT8` by `decodeWideNumber` and `FLOAT8` as the float64 it is, since `type: Number` maps to
14
- * BIGINT. At the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field.
15
- * Per pool, never a global parser, and a caller's own `types` win.
14
+ * BIGINT, and a zoneless `TIMESTAMP` or a `DATE` as UTC, where `pg` reads both in the process's zone. At
15
+ * the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field. Per pool,
16
+ * never a global parser, and a caller's own `types` win.
16
17
  */
17
- export declare function numericTypes(types: PgTypes): CustomTypesConfig;
18
+ export declare function wireTypes(types: PgTypes): CustomTypesConfig;
18
19
  export {};
@@ -1,14 +1,18 @@
1
+ import { decodeDate } from '../util/date.js';
1
2
  import { decodeWideNumber } from '../util/wideNumber.js';
2
3
  /**
3
4
  * Decodes `INT8` by `decodeWideNumber` and `FLOAT8` as the float64 it is, since `type: Number` maps to
4
- * BIGINT. At the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field.
5
- * Per pool, never a global parser, and a caller's own `types` win.
5
+ * BIGINT, and a zoneless `TIMESTAMP` or a `DATE` as UTC, where `pg` reads both in the process's zone. At
6
+ * the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field. Per pool,
7
+ * never a global parser, and a caller's own `types` win.
6
8
  */
7
- export function numericTypes(types) {
9
+ export function wireTypes(types) {
8
10
  // Text only: in binary mode an INT8 arrives as an 8-byte Buffer, and `Number(buffer)` is `NaN`.
9
11
  const decoders = new Map([
10
12
  [types.builtins['INT8'], decodeWideNumber],
11
13
  [types.builtins['FLOAT8'], Number],
14
+ [types.builtins['TIMESTAMP'], decodeDate],
15
+ [types.builtins['DATE'], decodeDate],
12
16
  ]);
13
17
  return {
14
18
  getTypeParser: (oid, format) => (format === 'text' && decoders.get(oid)) || types.getTypeParser(oid, format),
@@ -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;
@@ -1,6 +1,7 @@
1
1
  import type { AbstractDialect } from '../dialect/abstractDialect.js';
2
2
  import type { VectorCast } from '../dialect/vectorCast.js';
3
3
  import type { ColumnType, EntityGetter, FieldMeta, FieldOptions } from '../type/entity.js';
4
+ import { type DialectName } from '../type/index.js';
4
5
  import type { CanonicalType, TypeCategory } from './types.js';
5
6
  /** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
6
7
  export declare function isVectorCategory(category: TypeCategory | undefined): category is VectorCast;
@@ -29,6 +30,8 @@ export declare function canonicalToSql(type: CanonicalType, dialect: AbstractDia
29
30
  * Convert a canonical type to a TypeScript type string.
30
31
  */
31
32
  export declare function canonicalToTypeScript(type: CanonicalType): string;
33
+ /** The fractional-second digits an engine's timestamp holds when its type states none; `undefined` where it counts none. */
34
+ export declare function defaultTimestampPrecision(dialectName: DialectName): number | undefined;
32
35
  /**
33
36
  * A type as `dialect` stores it, rendered and read back: several types share one storage type, and only
34
37
  * the engine settles an unstated bound. Migrations and drift both compare through it.