uql-orm 0.51.0 → 0.53.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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +5 -5
  4. package/dist/bunSql/bunSql.util.d.ts +33 -10
  5. package/dist/bunSql/bunSql.util.js +57 -42
  6. package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
  7. package/dist/bunSql/bunSqlQuerier.js +17 -8
  8. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
  9. package/dist/bunSql/bunSqlQuerierPool.js +38 -25
  10. package/dist/bunSql/index.d.ts +1 -3
  11. package/dist/bunSql/index.js +0 -3
  12. package/dist/dialect/abstractDialect.d.ts +5 -5
  13. package/dist/dialect/abstractDialect.js +7 -6
  14. package/dist/dialect/abstractSqlDialect.d.ts +59 -6
  15. package/dist/dialect/abstractSqlDialect.js +90 -32
  16. package/dist/dialect/aliases.d.ts +2 -0
  17. package/dist/dialect/aliases.js +2 -0
  18. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  19. package/dist/dialect/mergeSqlDialect.js +89 -0
  20. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -1
  21. package/dist/dialect/mysqlLikeSqlDialect.js +3 -3
  22. package/dist/dialect/pgLikeSqlDialect.js +3 -2
  23. package/dist/dialect/vectorSqlDialect.js +2 -1
  24. package/dist/http/handler.js +3 -12
  25. package/dist/http/query.js +4 -1
  26. package/dist/migrate/builder/expressions.js +5 -0
  27. package/dist/migrate/ddl/index.d.ts +8 -0
  28. package/dist/migrate/ddl/index.js +11 -0
  29. package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
  30. package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
  31. package/dist/migrate/ddl/tableDdl.d.ts +34 -0
  32. package/dist/migrate/ddl/tableDdl.js +71 -0
  33. package/dist/migrate/introspection/index.d.ts +2 -0
  34. package/dist/migrate/introspection/index.js +2 -0
  35. package/dist/migrate/introspection/mssqlIntrospector.d.ts +60 -0
  36. package/dist/migrate/introspection/mssqlIntrospector.js +205 -0
  37. package/dist/migrate/introspection/registry.d.ts +3 -0
  38. package/dist/migrate/introspection/registry.js +28 -0
  39. package/dist/migrate/migrator.js +2 -21
  40. package/dist/migrate/schemaGenerator.d.ts +8 -10
  41. package/dist/migrate/schemaGenerator.js +29 -74
  42. package/dist/mongo/mongoDialect.js +12 -14
  43. package/dist/mssql/index.d.ts +3 -0
  44. package/dist/mssql/index.js +3 -0
  45. package/dist/mssql/mssqlDialect.d.ts +155 -0
  46. package/dist/mssql/mssqlDialect.js +341 -0
  47. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  48. package/dist/mssql/mssqlQuerier.js +137 -0
  49. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  50. package/dist/mssql/mssqlQuerierPool.js +32 -0
  51. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  52. package/dist/mssql/mssqlWireTypes.js +44 -0
  53. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  54. package/dist/pglite/pgliteQuerier.js +7 -2
  55. package/dist/postgres/pgCursorStream.d.ts +20 -0
  56. package/dist/postgres/pgCursorStream.js +49 -0
  57. package/dist/postgres/pgDialect.d.ts +1 -1
  58. package/dist/postgres/pgDialect.js +1 -1
  59. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  60. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  61. package/dist/querier/abstractQuerier.js +13 -11
  62. package/dist/querier/abstractSqlQuerier.js +3 -3
  63. package/dist/schema/canonicalType.js +96 -113
  64. package/dist/sqlite/sqliteDialect.js +3 -2
  65. package/dist/type/dialect.d.ts +24 -5
  66. package/dist/type/migratorDialect.d.ts +1 -1
  67. package/dist/type/migratorDialect.js +1 -0
  68. package/dist/type/query.d.ts +1 -1
  69. package/dist/type/queryWhere.d.ts +7 -19
  70. package/dist/type/vector.d.ts +3 -1
  71. package/dist/util/dialect.util.d.ts +10 -7
  72. package/dist/util/dialect.util.js +16 -29
  73. package/dist/util/object.util.d.ts +2 -0
  74. package/dist/util/object.util.js +4 -0
  75. package/package.json +13 -3
  76. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  77. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  78. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  79. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  80. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  81. package/dist/bunSql/bunSqliteDialect.js +0 -6
@@ -44,6 +44,9 @@ const SQL_TO_CANONICAL = {
44
44
  char: { category: 'string' },
45
45
  character: { category: 'string' },
46
46
  varchar: { category: 'string' },
47
+ varchar2: { category: 'string' },
48
+ nvarchar: { category: 'string' },
49
+ nchar: { category: 'string' },
47
50
  'character varying': { category: 'string' },
48
51
  text: { category: 'string', size: 'small' },
49
52
  tinytext: { category: 'string', size: 'tiny' },
@@ -64,11 +67,15 @@ const SQL_TO_CANONICAL = {
64
67
  'timestamp with time zone': { category: 'timestamp', withTimezone: true },
65
68
  timestamptz: { category: 'timestamp', withTimezone: true },
66
69
  datetime: { category: 'timestamp' },
70
+ datetime2: { category: 'timestamp' },
71
+ smalldatetime: { category: 'timestamp' },
72
+ datetimeoffset: { category: 'timestamp', withTimezone: true },
67
73
  // === JSON ===
68
74
  json: { category: 'json' },
69
75
  jsonb: { category: 'json' },
70
76
  // === UUID ===
71
77
  uuid: { category: 'uuid' },
78
+ uniqueidentifier: { category: 'uuid' },
72
79
  // === Binary ===
73
80
  blob: { category: 'blob' },
74
81
  bytea: { category: 'blob' },
@@ -77,15 +84,16 @@ const SQL_TO_CANONICAL = {
77
84
  tinyblob: { category: 'blob', size: 'tiny' },
78
85
  mediumblob: { category: 'blob', size: 'medium' },
79
86
  longblob: { category: 'blob', size: 'big' },
87
+ image: { category: 'blob', size: 'big' },
80
88
  // === Vector (for AI/embeddings) ===
81
89
  vector: { category: 'vector' },
82
90
  halfvec: { category: 'halfvec' },
83
91
  sparsevec: { category: 'sparsevec' },
84
92
  };
85
93
  /**
86
- * pgvector is the only engine with three vector column types, so every other dialect maps all three
87
- * canonical categories onto the single type it does have (see `hasNarrowVectorTypes` on the dialect, the
88
- * dialect-side half of the same fact).
94
+ * pgvector is the only engine with three vector column types, so every other engine maps all three
95
+ * canonical categories onto the single type it does have (see `hasNarrowVectorTypes` on the dialect,
96
+ * the dialect-side half of the same fact).
89
97
  */
90
98
  function withVectorType(scalars, vector) {
91
99
  return { ...scalars, vector, halfvec: vector, sparsevec: vector };
@@ -103,11 +111,9 @@ const PG_SCALAR_MAP = {
103
111
  uuid: 'UUID',
104
112
  blob: 'BYTEA',
105
113
  };
106
- const PG_TYPE_MAP = {
107
- ...PG_SCALAR_MAP,
108
- vector: 'VECTOR',
109
- halfvec: 'HALFVEC',
110
- sparsevec: 'SPARSEVEC',
114
+ const PG_SIZES = {
115
+ integer: { tiny: 'SMALLINT', small: 'SMALLINT', medium: 'INTEGER', big: 'BIGINT' },
116
+ float: { tiny: 'REAL', small: 'REAL', medium: 'DOUBLE PRECISION', big: 'DOUBLE PRECISION' },
111
117
  };
112
118
  const MYSQL_SCALAR_MAP = {
113
119
  integer: 'INT',
@@ -122,6 +128,13 @@ const MYSQL_SCALAR_MAP = {
122
128
  uuid: 'CHAR(36)',
123
129
  blob: 'BLOB',
124
130
  };
131
+ /** MariaDB is a MySQL fork and spells every one of these the same way, so both take the one map. */
132
+ const MYSQL_SIZES = {
133
+ integer: { tiny: 'TINYINT', small: 'SMALLINT', medium: 'MEDIUMINT', big: 'BIGINT' },
134
+ float: { tiny: 'FLOAT', small: 'FLOAT', medium: 'DOUBLE', big: 'DOUBLE' },
135
+ string: { tiny: 'TINYTEXT', small: 'TEXT', medium: 'MEDIUMTEXT', big: 'LONGTEXT' },
136
+ blob: { tiny: 'TINYBLOB', small: 'BLOB', medium: 'MEDIUMBLOB', big: 'LONGBLOB' },
137
+ };
125
138
  const SQLITE_SCALAR_MAP = {
126
139
  integer: 'INTEGER',
127
140
  float: 'REAL',
@@ -135,6 +148,35 @@ const SQLITE_SCALAR_MAP = {
135
148
  uuid: 'TEXT',
136
149
  blob: 'BLOB',
137
150
  };
151
+ /**
152
+ * Every string column is `NVARCHAR`: `VARCHAR` is a codepage type on SQL Server and silently destroys
153
+ * anything outside it on write. UQL never exposes the choice, so there is nothing to weigh per column.
154
+ */
155
+ const MSSQL_SCALAR_MAP = {
156
+ integer: 'INT',
157
+ float: 'REAL',
158
+ decimal: 'DECIMAL',
159
+ string: 'NVARCHAR',
160
+ boolean: 'BIT',
161
+ date: 'DATE',
162
+ time: 'TIME',
163
+ timestamp: 'DATETIME2',
164
+ json: 'NVARCHAR(MAX)',
165
+ uuid: 'UNIQUEIDENTIFIER',
166
+ blob: 'VARBINARY(MAX)',
167
+ };
168
+ /** `FLOAT` is eight bytes here and `REAL` four - the opposite of the MySQL family's spelling. */
169
+ const MSSQL_SIZES = {
170
+ integer: { tiny: 'TINYINT', small: 'SMALLINT', medium: 'INT', big: 'BIGINT' },
171
+ float: { tiny: 'REAL', small: 'REAL', medium: 'FLOAT', big: 'FLOAT' },
172
+ string: { tiny: 'NVARCHAR(255)', small: 'NVARCHAR(MAX)', medium: 'NVARCHAR(MAX)', big: 'NVARCHAR(MAX)' },
173
+ blob: {
174
+ tiny: 'VARBINARY(255)',
175
+ small: 'VARBINARY(MAX)',
176
+ medium: 'VARBINARY(MAX)',
177
+ big: 'VARBINARY(MAX)',
178
+ },
179
+ };
138
180
  /** MongoDB uses BSON types, not SQL types. These are placeholders for compatibility. */
139
181
  const MONGO_SCALAR_MAP = {
140
182
  integer: 'int',
@@ -149,73 +191,31 @@ const MONGO_SCALAR_MAP = {
149
191
  uuid: 'binData',
150
192
  blob: 'binData',
151
193
  };
152
- /** Every engine's scalars, and the one spelling it gives all three vector widths. */
153
- const CANONICAL_TO_SQL = {
154
- postgres: PG_TYPE_MAP,
194
+ /** Every engine's spelling of the canonical types. A new engine is one entry here and nothing else. */
195
+ const ENGINE_TYPES = {
196
+ postgres: {
197
+ scalars: { ...PG_SCALAR_MAP, vector: 'VECTOR', halfvec: 'HALFVEC', sparsevec: 'SPARSEVEC' },
198
+ sizes: PG_SIZES,
199
+ },
155
200
  // CockroachDB's VECTOR is native, no extension needed.
156
- cockroachdb: withVectorType(PG_SCALAR_MAP, 'VECTOR'),
201
+ cockroachdb: { scalars: withVectorType(PG_SCALAR_MAP, 'VECTOR'), sizes: PG_SIZES },
157
202
  // MySQL does have a `VECTOR` type (26.7), but no distance function outside HeatWave and no vector
158
203
  // index, so JSON keeps the column queryable with the JSON operators and needs no conversion.
159
- mysql: withVectorType(MYSQL_SCALAR_MAP, 'JSON'),
160
- mariadb: withVectorType(MYSQL_SCALAR_MAP, 'VECTOR'),
161
- sqlite: withVectorType(SQLITE_SCALAR_MAP, 'TEXT'),
162
- mongodb: withVectorType(MONGO_SCALAR_MAP, 'array'),
163
- };
164
- /**
165
- * Size variant modifiers for SQL types.
166
- */
167
- const PG_SIZE_MODIFIERS = {
168
- integer: {
169
- tiny: 'SMALLINT',
170
- small: 'SMALLINT',
171
- medium: 'INTEGER',
172
- big: 'BIGINT',
173
- },
174
- float: {
175
- tiny: 'REAL',
176
- small: 'REAL',
177
- medium: 'DOUBLE PRECISION',
178
- big: 'DOUBLE PRECISION',
179
- },
180
- };
181
- /**
182
- * MariaDB is a MySQL fork and spells every one of these the same way, so both take the one map.
183
- * Listed per-dialect, MariaDB's had only `integer`: a `double` column was created `FLOAT` there, four
184
- * bytes where the entity asked for eight, and a `big` string or blob lost its `LONG` prefix.
185
- */
186
- const MYSQL_SIZE_MODIFIERS = {
187
- integer: {
188
- tiny: 'TINYINT',
189
- small: 'SMALLINT',
190
- medium: 'MEDIUMINT',
191
- big: 'BIGINT',
204
+ mysql: {
205
+ scalars: withVectorType(MYSQL_SCALAR_MAP, 'JSON'),
206
+ sizes: MYSQL_SIZES,
207
+ decimal: { precision: 10, scale: 2 },
192
208
  },
193
- float: {
194
- tiny: 'FLOAT',
195
- small: 'FLOAT',
196
- medium: 'DOUBLE',
197
- big: 'DOUBLE',
209
+ mariadb: {
210
+ scalars: withVectorType(MYSQL_SCALAR_MAP, 'VECTOR'),
211
+ sizes: MYSQL_SIZES,
212
+ decimal: { precision: 10, scale: 2 },
198
213
  },
199
- string: {
200
- tiny: 'TINYTEXT',
201
- small: 'TEXT',
202
- medium: 'MEDIUMTEXT',
203
- big: 'LONGTEXT',
204
- },
205
- blob: {
206
- tiny: 'TINYBLOB',
207
- small: 'BLOB',
208
- medium: 'MEDIUMBLOB',
209
- big: 'LONGBLOB',
210
- },
211
- };
212
- const SIZE_MODIFIERS = {
213
- postgres: PG_SIZE_MODIFIERS,
214
- cockroachdb: PG_SIZE_MODIFIERS,
215
- mysql: MYSQL_SIZE_MODIFIERS,
216
- sqlite: {}, // SQLite uses affinity, no size modifiers
217
- mariadb: MYSQL_SIZE_MODIFIERS,
218
- mongodb: {},
214
+ // SQLite uses affinity, so no size variants.
215
+ sqlite: { scalars: withVectorType(SQLITE_SCALAR_MAP, 'TEXT') },
216
+ // 2025 and up; below that the server refuses the type rather than storing it as text.
217
+ mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'VECTOR'), sizes: MSSQL_SIZES },
218
+ mongodb: { scalars: withVectorType(MONGO_SCALAR_MAP, 'array') },
219
219
  };
220
220
  /**
221
221
  * Maps canonical types to TypeScript types for entity generation.
@@ -317,61 +317,44 @@ export function canonicalColumnType(sqlType, reported = {}) {
317
317
  export function canonicalToSql(type, dialect) {
318
318
  if (type.raw)
319
319
  return type.raw;
320
- const dialectName = dialect.dialectName;
321
- let sqlType = getBaseSqlType(type, dialectName);
322
- if (type.category === 'string') {
323
- sqlType = formatStringSqlType(type, dialect);
320
+ const engine = ENGINE_TYPES[dialect.dialectName];
321
+ const { features } = dialect;
322
+ // A `size` the engine spells out wins outright; anything else falls through to the rules below.
323
+ // `TEXT` canonicalizes to `size: 'small'`, so a string counts as unsized only where the engine
324
+ // declares no variant for it, which is how Postgres reaches `TEXT` rather than its base `VARCHAR`.
325
+ const sized = engine.sizes?.[type.category]?.[type.size];
326
+ let sqlType = sized ?? engine.scalars[type.category];
327
+ if (type.category === 'string' && !sized) {
328
+ sqlType = formatStringSqlType(type, engine.scalars.string, features.stringSizing);
324
329
  }
325
330
  else if (type.category === 'decimal') {
326
- sqlType = formatDecimalSqlType(type, dialectName, sqlType);
331
+ sqlType = formatDecimalSqlType(type, engine.decimal, sqlType);
327
332
  }
328
- else if (isVectorCategory(type.category) && type.length && dialect.features.vectorSupportsLength) {
333
+ else if (isVectorCategory(type.category) && type.length && features.vectorSupportsLength) {
329
334
  sqlType = `${sqlType}(${type.length})`;
330
335
  }
331
- if (type.category === 'timestamp' && type.withTimezone && dialect.features.supportsTimestamptz) {
336
+ if (type.category === 'timestamp' && type.withTimezone && features.supportsTimestamptz) {
332
337
  sqlType = 'TIMESTAMPTZ';
333
338
  }
334
- if (type.unsigned && (dialectName === 'mysql' || dialectName === 'mariadb')) {
335
- sqlType = `${sqlType} UNSIGNED`;
336
- }
337
- return sqlType;
339
+ return type.unsigned && features.supportsUnsigned ? `${sqlType} UNSIGNED` : sqlType;
338
340
  }
339
- function getBaseSqlType(type, dialect) {
340
- let sqlType = CANONICAL_TO_SQL[dialect][type.category];
341
- if (type.size) {
342
- const sizeMap = SIZE_MODIFIERS[dialect][type.category];
343
- if (sizeMap?.[type.size]) {
344
- sqlType = sizeMap[type.size];
345
- }
341
+ /** See {@link EngineFeatures.stringSizing} for what each mode means. */
342
+ function formatStringSqlType(type, base, sizing) {
343
+ if (sizing === 'text') {
344
+ return base;
346
345
  }
347
- return sqlType;
348
- }
349
- function formatStringSqlType(type, dialect) {
350
- const { dialectName, features } = dialect;
351
- if (dialectName === 'sqlite')
352
- return 'TEXT';
353
- if (features.defaultStringAsText)
354
- return type.length ? `VARCHAR(${type.length})` : 'TEXT';
355
- if (dialectName === 'mysql' || dialectName === 'mariadb') {
356
- if (type.size === 'tiny')
357
- return 'TINYTEXT';
358
- if (type.size === 'small')
359
- return 'TEXT';
360
- if (type.size === 'medium')
361
- return 'MEDIUMTEXT';
362
- if (type.size === 'big')
363
- return 'LONGTEXT';
364
- return type.length ? `VARCHAR(${type.length})` : 'VARCHAR(255)';
346
+ if (type.length) {
347
+ return `${base}(${type.length})`;
365
348
  }
366
- return type.length ? `VARCHAR(${type.length})` : 'VARCHAR(255)';
349
+ return sizing === 'bounded-text' ? 'TEXT' : `${base}(255)`;
367
350
  }
368
- function formatDecimalSqlType(type, dialect, baseType) {
369
- const p = type.precision ?? (dialect === 'mysql' || dialect === 'mariadb' ? 10 : undefined);
370
- const s = type.scale ?? (dialect === 'mysql' || dialect === 'mariadb' ? 2 : undefined);
371
- if (p !== undefined) {
372
- return s !== undefined ? `${baseType}(${p}, ${s})` : `${baseType}(${p})`;
351
+ function formatDecimalSqlType(type, fallback, baseType) {
352
+ const p = type.precision ?? fallback?.precision;
353
+ const s = type.scale ?? fallback?.scale;
354
+ if (p === undefined) {
355
+ return baseType;
373
356
  }
374
- return baseType;
357
+ return s === undefined ? `${baseType}(${p})` : `${baseType}(${p}, ${s})`;
375
358
  }
376
359
  /**
377
360
  * Convert a canonical type to a TypeScript type string.
@@ -11,7 +11,6 @@ export class SqliteDialect extends AbstractSqlDialect {
11
11
  indexIfNotExists: true,
12
12
  schemas: false, // SQLite's namespaces are attached database files, not declared objects
13
13
  dropTableCascade: false,
14
- renameColumn: true,
15
14
  foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
16
15
  primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
17
16
  generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
@@ -19,7 +18,9 @@ export class SqliteDialect extends AbstractSqlDialect {
19
18
  vectorIndexRequiresNotNull: false,
20
19
  vectorSupportsLength: false,
21
20
  supportsTimestamptz: false,
22
- defaultStringAsText: true,
21
+ stringSizing: 'text',
22
+ supportsUnsigned: false,
23
+ serverSideCursors: false,
23
24
  };
24
25
  dialectName = 'sqlite';
25
26
  escapeIdChar = '`';
@@ -89,7 +89,6 @@ export interface EngineFeatures {
89
89
  */
90
90
  readonly schemas: boolean;
91
91
  readonly dropTableCascade: boolean;
92
- readonly renameColumn: boolean;
93
92
  readonly foreignKeyAlter: boolean;
94
93
  /**
95
94
  * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
@@ -128,8 +127,28 @@ export interface EngineFeatures {
128
127
  readonly vectorSupportsLength: boolean;
129
128
  /** Whether the dialect natively supports the TIMESTAMPTZ alias/type. */
130
129
  readonly supportsTimestamptz: boolean;
131
- /** Whether the dialect defaults to TEXT for strings when no length is specified (e.g. Postgres). */
132
- readonly defaultStringAsText: boolean;
130
+ /**
131
+ * How a `string` column is sized: `text` is always the unbounded type whatever length was asked
132
+ * for (SQLite, whose affinity makes a bound meaningless), `bounded-text` takes the bound when there
133
+ * is one and the unbounded type otherwise (the Postgres family), `varchar` is always bounded and
134
+ * falls back to 255 (the MySQL family, SQL Server).
135
+ *
136
+ * Three-way for the reason {@link commentSyntax} is: a boolean puts SQLite and Postgres on the
137
+ * same branch, and they need different answers.
138
+ */
139
+ readonly stringSizing: 'text' | 'bounded-text' | 'varchar';
140
+ /** Whether the engine has unsigned integers, so `@Field({ unsigned: true })` reaches the column. */
141
+ readonly supportsUnsigned: boolean;
142
+ /**
143
+ * Whether the engine has SQL-level cursors (`DECLARE`/`FETCH FORWARD`/`CLOSE`), which is how a
144
+ * driver with no cursor API of its own still streams a result set instead of buffering it - see
145
+ * `postgres/pgCursorStream.ts`. True across the Postgres wire family, whose spelling that helper
146
+ * speaks; SQL Server and Oracle have cursors of their own but not this syntax.
147
+ *
148
+ * The node-`pg` family reports `true` and goes on using `pg-query-stream`: the flag says the engine
149
+ * has cursors, not that the driver needs them.
150
+ */
151
+ readonly serverSideCursors: boolean;
133
152
  }
134
153
  export interface DialectFeatures extends EngineFeatures, DriverCapabilities {
135
154
  }
@@ -219,13 +238,13 @@ export interface QueryDialect {
219
238
  /**
220
239
  * Supported SQL dialect identifiers.
221
240
  */
222
- export type SqlDialectName = 'postgres' | 'cockroachdb' | 'mysql' | 'mariadb' | 'sqlite';
241
+ export type SqlDialectName = 'postgres' | 'cockroachdb' | 'mysql' | 'mariadb' | 'sqlite' | 'mssql';
223
242
  /**
224
243
  * Minimal dialect interface exposing escapeIdChar for SQL operations
225
244
  */
226
245
  export interface SqlQueryDialect extends QueryDialect {
227
246
  /**
228
- * The SQL dialect name (postgres, mysql, mariadb, sqlite).
247
+ * The SQL dialect name (postgres, mysql, mariadb, sqlite, mssql).
229
248
  */
230
249
  readonly dialectName: SqlDialectName;
231
250
  /**
@@ -1,5 +1,5 @@
1
1
  import type { DialectName } from './querier.js';
2
- declare const KNOWN_MIGRATOR_DIALECTS: readonly ["postgres", "cockroachdb", "mysql", "mariadb", "sqlite", "mongodb"];
2
+ declare const KNOWN_MIGRATOR_DIALECTS: readonly ["postgres", "cockroachdb", "mysql", "mariadb", "sqlite", "mssql", "mongodb"];
3
3
  export type KnownMigratorDialect = (typeof KNOWN_MIGRATOR_DIALECTS)[number];
4
4
  /**
5
5
  * Whether `d` is supported by built-in migrator introspection / schema generators.
@@ -4,6 +4,7 @@ const KNOWN_MIGRATOR_DIALECTS = [
4
4
  'mysql',
5
5
  'mariadb',
6
6
  'sqlite',
7
+ 'mssql',
7
8
  'mongodb',
8
9
  ];
9
10
  /**
@@ -143,7 +143,7 @@ type ToOneRelationKey<E> = {
143
143
  type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
144
144
  /**
145
145
  * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,
146
- * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on
146
+ * like `QueryWhere`), relation sort via nested objects, and vector similarity search on
147
147
  * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:
148
148
  * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank
149
149
  * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.
@@ -1,4 +1,4 @@
1
- import type { EntityId, FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
1
+ import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryRaw } from './queryRaw.js';
3
3
  import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
4
4
  import type { QueryVectorQuery } from './vector.js';
@@ -20,12 +20,6 @@ export type QueryTextSearchOptions<E> = {
20
20
  */
21
21
  $config?: string;
22
22
  };
23
- /**
24
- * comparison by fields.
25
- */
26
- export type QueryWhereFieldMap<E> = {
27
- [K in FieldKey<E>]?: QueryWhereFieldValue<E[K]>;
28
- };
29
23
  /**
30
24
  * Field comparison, JSON dot-path access, and relation filtering - all fully typed.
31
25
  * JSON dot-paths are restricted to real JSON fields, and typed payloads type each path's value
@@ -37,9 +31,12 @@ export type QueryWhereFieldMap<E> = {
37
31
  * {@link QuerySortMap} is: the sets are disjoint, and an assignability check against an
38
32
  * intersection is repeated per constituent, which every `$where` in a codebase pays. The root
39
33
  * operators stay a separate member - they are a fixed shape, not keyed off the entity.
34
+ *
35
+ * An object and nothing else: in a union with ids or lists, TypeScript reports a wrong value against
36
+ * the whole `$where` instead of the key holding it. Ids are `{ id: 1 }`, or the by-id methods.
40
37
  */
41
- export type QueryWhereMap<E> = QueryWhereRootOperator<E> & {
42
- [K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhereMap<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
38
+ export type QueryWhere<E> = QueryWhereRootOperator<E> & {
39
+ [K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhere<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
43
40
  };
44
41
  /**
45
42
  * Filter a to-many relation by its row count.
@@ -332,14 +329,5 @@ export type QueryWhereFieldValue<T> = T | (undefined extends T ? null : never) |
332
329
  /**
333
330
  * query filter array - the value every {@link QueryGroupOp} takes.
334
331
  */
335
- export type QueryWhereArray<E> = (QueryWhereMap<E> | QueryRaw)[];
336
- /**
337
- * query filter.
338
- */
339
- /**
340
- * `EntityId` rather than `IdValue`: a by-id method reduces to `$where: id`, and a composite key is
341
- * addressed by an object carrying every key. That object is a where map naming those columns, so the
342
- * two spellings meet here rather than needing a conversion.
343
- */
344
- export type QueryWhere<E> = EntityId<E> | EntityId<E>[] | QueryWhereMap<E> | QueryWhereArray<E> | QueryRaw;
332
+ export type QueryWhereArray<E> = (QueryWhere<E> | QueryRaw)[];
345
333
  export {};
@@ -58,13 +58,15 @@ export type WithDistance<E, K extends string = '_distance'> = E & Record<K, numb
58
58
  *
59
59
  * `opsSuffix` rides along on the operator form because pgvector's index operator class is named from
60
60
  * the same metric (`vector_cosine_ops`): keeping them together is what stops a dialect from having
61
- * the operator but not the class it indexes with.
61
+ * the operator but not the class it indexes with. `metricArg` is for the engine with one function
62
+ * taking the metric by name: SQL Server's `VECTOR_DISTANCE('cosine', a, b)`.
62
63
  */
63
64
  export type VectorMetric = {
64
65
  readonly op: string;
65
66
  readonly opsSuffix: string;
66
67
  } | {
67
68
  readonly fn: string;
69
+ readonly metricArg?: string;
68
70
  };
69
71
  /** The operator form, for the pgvector-family dialects whose index DDL also needs `opsSuffix`. */
70
72
  export type VectorOperatorMetric = Extract<VectorMetric, {
@@ -1,4 +1,4 @@
1
- import { type CascadeType, type EntityData, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type RelationKey } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
3
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
4
  /** Appends `record`'s not-yet-`seen` insertable keys (real, caller-written, defined value) to `keys`. */
@@ -90,13 +90,16 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
90
90
  export declare function hasVectorNear(where: unknown): boolean;
91
91
  /** Type guard: checks whether an update payload value is a JSON operator object. */
92
92
  export declare function isJsonUpdateOp(value: unknown): value is JsonUpdateOp;
93
- export declare function augmentWhere<E>(meta: EntityMeta<E>, target?: QueryWhere<E>, source?: QueryWhere<E>): QueryWhere<E>;
94
93
  /**
95
- * Normalizes any `$where` shape (id, id[], raw, or map) to a `QueryWhereMap`. Read-only: for a map
96
- * input it returns that same object by reference (no copy), so callers must not mutate the result -
97
- * {@link applyFilters} and {@link augmentWhere} return new objects instead.
94
+ * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
95
+ * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
98
96
  */
99
- export declare function buildQueryWhereAsMap<E>(meta: EntityMeta<E>, filter?: QueryWhere<E>): QueryWhereMap<E>;
97
+ export declare function whereIds<E>(meta: EntityMeta<E>, ids: EntityId<E> | EntityId<E>[]): QueryWhere<E>;
98
+ /**
99
+ * Refuses a `$where` that is not a map. Untyped JS and parsed JSON can still pass an id or a list of
100
+ * them, and a scalar read as a map has no keys: the statement would address every row.
101
+ */
102
+ export declare function assertWhere<E>(meta: EntityMeta<E>, where: unknown): void;
100
103
  /** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
101
104
  export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']): QueryOptions['filters'];
102
105
  /**
@@ -112,7 +115,7 @@ export declare function withoutSoftDeleteFilter(filters: QueryOptions['filters']
112
115
  * (`{}`) resolved to "no restriction" and adds nothing - the escape hatch for trusted cross-tenant
113
116
  * work (e.g. a maintenance job running under a `system` context).
114
117
  */
115
- export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhereMap<E>, opts?: QueryOptions): QueryWhereMap<E>;
118
+ export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWhere<E>, opts?: QueryOptions): QueryWhere<E>;
116
119
  /**
117
120
  * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
118
121
  */
@@ -3,7 +3,7 @@ import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { VECTOR_INDEX_TYPES } from '../type/vector.js';
5
5
  import { isDatabaseWritten } from './field.util.js';
6
- import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, someKey } from './object.util.js';
6
+ import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, isWhereMap, someKey } from './object.util.js';
7
7
  export function filterFieldKeys(meta, payload, callbackKey) {
8
8
  return getKeys(payload).filter((key) => {
9
9
  const fieldOpts = meta.fields[key];
@@ -234,37 +234,24 @@ const JSON_UPDATE_OPS = [
234
234
  export function isJsonUpdateOp(value) {
235
235
  return value !== null && typeof value === 'object' && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
236
236
  }
237
- export function augmentWhere(meta, target = {}, source = {}) {
238
- const targetComparison = buildQueryWhereAsMap(meta, target);
239
- const sourceComparison = buildQueryWhereAsMap(meta, source);
240
- return {
241
- ...targetComparison,
242
- ...sourceComparison,
243
- };
244
- }
245
237
  /**
246
- * Normalizes any `$where` shape (id, id[], raw, or map) to a `QueryWhereMap`. Read-only: for a map
247
- * input it returns that same object by reference (no copy), so callers must not mutate the result -
248
- * {@link applyFilters} and {@link augmentWhere} return new objects instead.
238
+ * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
239
+ * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
249
240
  */
250
- export function buildQueryWhereAsMap(meta, filter = {}) {
251
- if (filter instanceof QueryRaw) {
252
- return { $and: [filter] };
253
- }
254
- if (Array.isArray(filter)) {
255
- // A list of bare ids is an `IN` over the one key column; a list of anything else is a list of
256
- // `$where`s, which is an OR - and that is how a composite's id objects name a settled set of rows.
257
- return filter.every(isScalarId)
258
- ? { [soleIdOf(meta, 'addressing by a bare id value')]: filter }
259
- : { $or: filter };
241
+ export function whereIds(meta, ids) {
242
+ if (Array.isArray(ids) ? ids.every(isScalarId) : isScalarId(ids)) {
243
+ return { [soleIdOf(meta, 'addressing by a bare id value')]: ids };
260
244
  }
261
- if (isScalarId(filter)) {
262
- // A scalar can only name one column, so on a composite it would address every row agreeing on
263
- // that one. A composite is addressed by a map, which falls through below as the `$where` it
264
- // already is - an id object and a where map are the same shape by design.
265
- return { [soleIdOf(meta, 'addressing by a bare id value')]: filter };
245
+ return (Array.isArray(ids) ? { $or: ids } : ids);
246
+ }
247
+ /**
248
+ * Refuses a `$where` that is not a map. Untyped JS and parsed JSON can still pass an id or a list of
249
+ * them, and a scalar read as a map has no keys: the statement would address every row.
250
+ */
251
+ export function assertWhere(meta, where) {
252
+ if (!isWhereMap(where)) {
253
+ throw new TypeError(`$where on '${entityName(meta)}' must be a map of conditions, such as { id: 1 }`);
266
254
  }
267
- return filter;
268
255
  }
269
256
  /** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
270
257
  export function withoutSoftDeleteFilter(filters) {
@@ -314,7 +301,7 @@ export function applyFilters(meta, whereMap, opts) {
314
301
  }
315
302
  continue;
316
303
  }
317
- const conditionMap = buildQueryWhereAsMap(meta, condition);
304
+ const conditionMap = condition;
318
305
  if (!hasKeys(conditionMap)) {
319
306
  continue; // resolved to "no restriction" (e.g. a trusted system context) - nothing to merge
320
307
  }
@@ -35,3 +35,5 @@ export declare function getFieldKeys<E>(fields: {
35
35
  * is what a `$where` map and a composite key's id object both are; an array is a list of either.
36
36
  */
37
37
  export declare function isScalarId(value: unknown): boolean;
38
+ /** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */
39
+ export declare function isWhereMap(value: unknown): value is Record<string, unknown>;
@@ -81,3 +81,7 @@ export function isScalarId(value) {
81
81
  const proto = Object.getPrototypeOf(value);
82
82
  return proto !== Object.prototype && proto !== null;
83
83
  }
84
+ /** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */
85
+ export function isWhereMap(value) {
86
+ return !Array.isArray(value) && !isScalarId(value);
87
+ }