uql-orm 0.50.0 → 0.52.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 (70) 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 +2 -2
  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/abstractSqlDialect.d.ts +61 -7
  13. package/dist/dialect/abstractSqlDialect.js +88 -27
  14. package/dist/dialect/aliases.d.ts +2 -0
  15. package/dist/dialect/aliases.js +2 -0
  16. package/dist/dialect/jsonSql.d.ts +2 -2
  17. package/dist/dialect/jsonSql.js +2 -2
  18. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  19. package/dist/dialect/mergeSqlDialect.js +89 -0
  20. package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
  21. package/dist/dialect/pgLikeSqlDialect.d.ts +3 -3
  22. package/dist/dialect/pgLikeSqlDialect.js +15 -12
  23. package/dist/migrate/builder/expressions.js +5 -0
  24. package/dist/migrate/introspection/index.d.ts +2 -0
  25. package/dist/migrate/introspection/index.js +2 -0
  26. package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
  27. package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
  28. package/dist/migrate/introspection/postgresIntrospector.js +3 -3
  29. package/dist/migrate/introspection/registry.d.ts +3 -0
  30. package/dist/migrate/introspection/registry.js +28 -0
  31. package/dist/migrate/migrator.js +2 -21
  32. package/dist/mongo/mongoDialect.js +4 -1
  33. package/dist/mongo/mongodbQuerier.d.ts +2 -2
  34. package/dist/mongo/mongodbQuerier.js +8 -6
  35. package/dist/mssql/index.d.ts +3 -0
  36. package/dist/mssql/index.js +3 -0
  37. package/dist/mssql/mssqlDialect.d.ts +144 -0
  38. package/dist/mssql/mssqlDialect.js +328 -0
  39. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  40. package/dist/mssql/mssqlQuerier.js +137 -0
  41. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  42. package/dist/mssql/mssqlQuerierPool.js +32 -0
  43. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  44. package/dist/mssql/mssqlWireTypes.js +44 -0
  45. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  46. package/dist/pglite/pgliteQuerier.js +7 -2
  47. package/dist/postgres/pgCursorStream.d.ts +20 -0
  48. package/dist/postgres/pgCursorStream.js +49 -0
  49. package/dist/postgres/pgDialect.d.ts +1 -1
  50. package/dist/postgres/pgDialect.js +1 -1
  51. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  52. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  53. package/dist/querier/abstractQuerier.d.ts +3 -7
  54. package/dist/querier/abstractQuerier.js +22 -2
  55. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  56. package/dist/schema/canonicalType.js +96 -113
  57. package/dist/sqlite/sqliteDialect.d.ts +7 -7
  58. package/dist/sqlite/sqliteDialect.js +21 -18
  59. package/dist/type/dialect.d.ts +31 -4
  60. package/dist/type/migratorDialect.d.ts +1 -1
  61. package/dist/type/migratorDialect.js +1 -0
  62. package/dist/type/query.d.ts +29 -6
  63. package/dist/type/universalQuerier.d.ts +3 -3
  64. package/package.json +13 -3
  65. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  66. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  67. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  68. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  69. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  70. 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
+ // A 2025 server has a native `VECTOR`; below that the column is JSON text, which stays queryable.
217
+ mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'NVARCHAR(MAX)'), 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.
@@ -46,24 +46,24 @@ export declare class SqliteDialect extends AbstractSqlDialect {
46
46
  * element includes these keys" - `$elemMatch` always expands to per-field conditions.
47
47
  */
48
48
  protected readonly jsonContainmentIsPartial = false;
49
- /** `json_each` exposes a JSON boolean as `0`/`1` and a number as a number - already comparable. */
49
+ /** `JSON_EACH` exposes a JSON boolean as `0`/`1` and a number as a number - already comparable. */
50
50
  protected readonly jsonScalarElemKeepsType = true;
51
51
  /**
52
52
  * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
53
- * correctly whatever its type. `json_each`'s `value` column would not: it unquotes strings (`a`
53
+ * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
54
54
  * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
55
55
  */
56
56
  protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
57
57
  protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
58
- /** `json_each` yields both scalar and object elements, so one form covers each case. */
58
+ /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
59
59
  protected jsonElemFrom(jsonField: string, _fields: readonly string[], alias: string): string;
60
60
  protected jsonElemRef(alias: string, field?: string, asJson?: boolean): string;
61
61
  protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
62
62
  protected numericCast(expr: string): string;
63
63
  protected jsonCast(operand: string): string;
64
64
  /**
65
- * `json_replace` leaves an absent key (and a NULL column) untouched. Elements are read back
66
- * through `->` at their own `fullkey` so each keeps its JSON type - `json_each`'s `value` would
65
+ * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
66
+ * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
67
67
  * flatten booleans to 0/1 and stringify objects.
68
68
  */
69
69
  protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
@@ -71,8 +71,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
71
71
  /**
72
72
  * `[#]` appends, creating the array when the key is absent.
73
73
  *
74
- * @remarks `json_set` rather than `json_insert`: the two are equivalent here because `[#]` always
75
- * resolves past the end of the array, and Turso's engine implements `json_insert` as create-only,
74
+ * @remarks `JSON_SET` rather than `JSON_INSERT`: the two are equivalent here because `[#]` always
75
+ * resolves past the end of the array, and Turso's engine implements `JSON_INSERT` as create-only,
76
76
  * so it silently drops the element when the array already exists.
77
77
  */
78
78
  protected jsonPush(ctx: QueryContext, expr: string, push: Record<string, unknown>): string;
@@ -19,7 +19,10 @@ export class SqliteDialect extends AbstractSqlDialect {
19
19
  vectorIndexRequiresNotNull: false,
20
20
  vectorSupportsLength: false,
21
21
  supportsTimestamptz: false,
22
- defaultStringAsText: true,
22
+ stringSizing: 'text',
23
+ supportsUnsigned: false,
24
+ multipleCascadePaths: true,
25
+ serverSideCursors: false,
23
26
  };
24
27
  dialectName = 'sqlite';
25
28
  escapeIdChar = '`';
@@ -94,11 +97,11 @@ export class SqliteDialect extends AbstractSqlDialect {
94
97
  * element includes these keys" - `$elemMatch` always expands to per-field conditions.
95
98
  */
96
99
  jsonContainmentIsPartial = false;
97
- /** `json_each` exposes a JSON boolean as `0`/`1` and a number as a number - already comparable. */
100
+ /** `JSON_EACH` exposes a JSON boolean as `0`/`1` and a number as a number - already comparable. */
98
101
  jsonScalarElemKeepsType = true;
99
102
  /**
100
103
  * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
101
- * correctly whatever its type. `json_each`'s `value` column would not: it unquotes strings (`a`
104
+ * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
102
105
  * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
103
106
  */
104
107
  jsonAll(ctx, jsonField, value) {
@@ -108,52 +111,52 @@ export class SqliteDialect extends AbstractSqlDialect {
108
111
  return `(${conditions.join(' AND ')})`;
109
112
  }
110
113
  jsonSize(ctx, jsonField, value) {
111
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`json_array_length(${jsonField})`), value));
114
+ return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSON_ARRAY_LENGTH(${jsonField})`), value));
112
115
  }
113
- /** `json_each` yields both scalar and object elements, so one form covers each case. */
116
+ /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
114
117
  jsonElemFrom(jsonField, _fields, alias) {
115
- return `json_each(${jsonField}) ${alias}`;
118
+ return `JSON_EACH(${jsonField}) ${alias}`;
116
119
  }
117
120
  jsonElemRef(alias, field, asJson = false) {
118
121
  if (field === undefined) {
119
122
  return `${alias}.value`;
120
123
  }
121
- return asJson ? `${alias}.value -> ${jsonPath(field)}` : `json_extract(${alias}.value, ${jsonPath(field)})`;
124
+ return asJson ? `${alias}.value -> ${jsonPath(field)}` : `JSON_EXTRACT(${alias}.value, ${jsonPath(field)})`;
122
125
  }
123
126
  getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
124
- return `json_extract(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
127
+ return `JSON_EXTRACT(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
125
128
  }
126
129
  numericCast(expr) {
127
130
  return `CAST(${expr} AS REAL)`;
128
131
  }
129
132
  jsonCast(operand) {
130
- return `json(${operand})`;
133
+ return `JSON(${operand})`;
131
134
  }
132
135
  /**
133
- * `json_replace` leaves an absent key (and a NULL column) untouched. Elements are read back
134
- * through `->` at their own `fullkey` so each keeps its JSON type - `json_each`'s `value` would
136
+ * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
137
+ * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
135
138
  * flatten booleans to 0/1 and stringify objects.
136
139
  */
137
140
  jsonPullKey(ctx, expr, escapedCol, key, value) {
138
141
  const path = jsonPath(key);
139
142
  const elem = `${escapedCol} -> ${JSON_PULL_ALIAS}.fullkey`;
140
- const kept = `SELECT json_group_array(json(${elem})) FROM json_each(${escapedCol}, ${path}) ${JSON_PULL_ALIAS} WHERE ${elem} <> ${this.jsonScalarParam(ctx, value)}`;
141
- return `json_replace(${expr}, ${path}, (${kept}))`;
143
+ const kept = `SELECT JSON_GROUP_ARRAY(JSON(${elem})) FROM JSON_EACH(${escapedCol}, ${path}) ${JSON_PULL_ALIAS} WHERE ${elem} <> ${this.jsonScalarParam(ctx, value)}`;
144
+ return `JSON_REPLACE(${expr}, ${path}, (${kept}))`;
142
145
  }
143
146
  jsonSet(ctx, expr, set, field) {
144
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'json_set', jsonSetTarget(expr, field, `'{}'`), set);
147
+ return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set);
145
148
  }
146
149
  /**
147
150
  * `[#]` appends, creating the array when the key is absent.
148
151
  *
149
- * @remarks `json_set` rather than `json_insert`: the two are equivalent here because `[#]` always
150
- * resolves past the end of the array, and Turso's engine implements `json_insert` as create-only,
152
+ * @remarks `JSON_SET` rather than `JSON_INSERT`: the two are equivalent here because `[#]` always
153
+ * resolves past the end of the array, and Turso's engine implements `JSON_INSERT` as create-only,
151
154
  * so it silently drops the element when the array already exists.
152
155
  */
153
156
  jsonPush(ctx, expr, push) {
154
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'json_set', expr, push, '[#]');
157
+ return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, '[#]');
155
158
  }
156
159
  jsonUnset(_ctx, expr, unset) {
157
- return jsonRemoveCall('json_remove', expr, unset);
160
+ return jsonRemoveCall('JSON_REMOVE', expr, unset);
158
161
  }
159
162
  }
@@ -128,8 +128,35 @@ export interface EngineFeatures {
128
128
  readonly vectorSupportsLength: boolean;
129
129
  /** Whether the dialect natively supports the TIMESTAMPTZ alias/type. */
130
130
  readonly supportsTimestamptz: boolean;
131
- /** Whether the dialect defaults to TEXT for strings when no length is specified (e.g. Postgres). */
132
- readonly defaultStringAsText: boolean;
131
+ /**
132
+ * How a `string` column is sized: `text` is always the unbounded type whatever length was asked
133
+ * for (SQLite, whose affinity makes a bound meaningless), `bounded-text` takes the bound when there
134
+ * is one and the unbounded type otherwise (the Postgres family), `varchar` is always bounded and
135
+ * falls back to 255 (the MySQL family, SQL Server).
136
+ *
137
+ * Three-way for the reason {@link commentSyntax} is: a boolean puts SQLite and Postgres on the
138
+ * same branch, and they need different answers.
139
+ */
140
+ readonly stringSizing: 'text' | 'bounded-text' | 'varchar';
141
+ /** Whether the engine has unsigned integers, so `@Field({ unsigned: true })` reaches the column. */
142
+ readonly supportsUnsigned: boolean;
143
+ /**
144
+ * Whether one table can be reached by two cascading foreign-key paths. SQL Server refuses the
145
+ * constraint outright ("may cause cycles or multiple cascade paths", error 1785) and Oracle
146
+ * likewise, so a cascade is downgraded to `NO ACTION` there rather than emitting DDL the engine
147
+ * rejects - which would otherwise fail on any diamond-shaped schema at create time.
148
+ */
149
+ readonly multipleCascadePaths: boolean;
150
+ /**
151
+ * Whether the engine has SQL-level cursors (`DECLARE`/`FETCH FORWARD`/`CLOSE`), which is how a
152
+ * driver with no cursor API of its own still streams a result set instead of buffering it - see
153
+ * `postgres/pgCursorStream.ts`. True across the Postgres wire family, whose spelling that helper
154
+ * speaks; SQL Server and Oracle have cursors of their own but not this syntax.
155
+ *
156
+ * The node-`pg` family reports `true` and goes on using `pg-query-stream`: the flag says the engine
157
+ * has cursors, not that the driver needs them.
158
+ */
159
+ readonly serverSideCursors: boolean;
133
160
  }
134
161
  export interface DialectFeatures extends EngineFeatures, DriverCapabilities {
135
162
  }
@@ -219,13 +246,13 @@ export interface QueryDialect {
219
246
  /**
220
247
  * Supported SQL dialect identifiers.
221
248
  */
222
- export type SqlDialectName = 'postgres' | 'cockroachdb' | 'mysql' | 'mariadb' | 'sqlite';
249
+ export type SqlDialectName = 'postgres' | 'cockroachdb' | 'mysql' | 'mariadb' | 'sqlite' | 'mssql';
223
250
  /**
224
251
  * Minimal dialect interface exposing escapeIdChar for SQL operations
225
252
  */
226
253
  export interface SqlQueryDialect extends QueryDialect {
227
254
  /**
228
- * The SQL dialect name (postgres, mysql, mariadb, sqlite).
255
+ * The SQL dialect name (postgres, mysql, mariadb, sqlite, mssql).
229
256
  */
230
257
  readonly dialectName: SqlDialectName;
231
258
  /**
@@ -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
  /**
@@ -1,4 +1,4 @@
1
- import type { FieldKey, IdKey, JsonFieldPaths, RelationKey, RelationTarget } from './entity.js';
1
+ import type { FieldKey, IdKey, JsonFieldPaths, RelationKey, RelationTarget, WrittenId } from './entity.js';
2
2
  import type { QueryLock } from './queryLock.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
@@ -407,7 +407,29 @@ export type QueryStringified = {
407
407
  [K in keyof Query<unknown>]?: string;
408
408
  };
409
409
  /**
410
- * result of an update operation.
410
+ * What upserting one row reports, against the entity rather than the driver.
411
+ *
412
+ * `created` is here and not on {@link QueryUpsertManyResult} because it is only ever knowable for a
413
+ * single statement: a batch's `affectedRows` is a weighted sum on the dialects that report one at
414
+ * all, and a batch of mixed shapes is several statements.
415
+ */
416
+ export type QueryUpsertOneResult<E> = {
417
+ readonly id?: WrittenId<E>;
418
+ readonly changes?: number;
419
+ /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */
420
+ readonly created?: boolean;
421
+ };
422
+ /**
423
+ * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the
424
+ * rows that were passed, and carries a composite key as the map naming it.
425
+ */
426
+ export type QueryUpsertManyResult<E> = {
427
+ readonly ids: (WrittenId<E> | undefined)[];
428
+ readonly changes?: number;
429
+ };
430
+ /**
431
+ * result of an update operation, as the driver reports it - which is what `run` hands back, where
432
+ * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.
411
433
  */
412
434
  export type QueryUpdateResult = {
413
435
  /**
@@ -415,11 +437,12 @@ export type QueryUpdateResult = {
415
437
  */
416
438
  changes?: number;
417
439
  /**
418
- * the inserted IDs, in insertion order. Exact on `'returning'` dialects; inferred from the
419
- * driver header on the others (see {@link InsertIdSource}), and empty when the header
420
- * reports no generated ID.
440
+ * the IDs the statement reported, in payload order, `undefined` where it reported none for that
441
+ * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;
442
+ * inferred from the driver header on the others (see {@link InsertIdSource}), and absent
443
+ * altogether when the header reports nothing.
421
444
  */
422
- ids?: PrimaryKey[];
445
+ ids?: (PrimaryKey | undefined)[];
423
446
  /**
424
447
  * first inserted ID.
425
448
  */
@@ -1,5 +1,5 @@
1
1
  import type { EntityData, EntityId, FieldKey, RelationKey, UpdatePayload, WrittenId } from './entity.js';
2
- import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult } from './query.js';
2
+ import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpsertOneResult, QueryUpsertManyResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
5
5
  import type { QuerierCountedResult, QuerierResult, QuerierTransport } from './wire.js';
@@ -139,7 +139,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
139
139
  * @param payload the data to be persisted
140
140
  * @return operation metadata; see {@link QueryUpdateResult}
141
141
  */
142
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
142
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpsertOneResult<E>>;
143
143
  /**
144
144
  * Insert or update many records based on the conflict paths.
145
145
  * @param entity the entity to persist on
@@ -147,7 +147,7 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
147
147
  * @param payload the data to be persisted
148
148
  * @return operation metadata; see {@link QueryUpdateResult}
149
149
  */
150
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
150
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpsertManyResult<E>>;
151
151
  /**
152
152
  * insert or update a record.
153
153
  * @param entity the entity to persist on
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "uql-orm",
3
3
  "homepage": "https://uql-orm.dev",
4
- "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
4
+ "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.50.0",
6
+ "version": "0.52.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -49,7 +49,8 @@
49
49
  "./pglite": "./dist/pglite/index.js",
50
50
  "./d1": "./dist/d1/index.js",
51
51
  "./bunSql": "./dist/bunSql/index.js",
52
- "./package.json": "./package.json"
52
+ "./package.json": "./package.json",
53
+ "./mssql": "./dist/mssql/index.js"
53
54
  },
54
55
  "files": [
55
56
  "dist",
@@ -75,6 +76,7 @@
75
76
  "express": ">=5.0.0",
76
77
  "mariadb": ">=3.0.0",
77
78
  "mongodb": ">=6.0.0",
79
+ "mssql": ">=11.0.0",
78
80
  "mysql2": ">=3.0.0",
79
81
  "pg": ">=8.0.0",
80
82
  "pg-query-stream": ">=4.0.0",
@@ -125,6 +127,9 @@
125
127
  },
126
128
  "rxjs": {
127
129
  "optional": true
130
+ },
131
+ "mssql": {
132
+ "optional": true
128
133
  }
129
134
  },
130
135
  "devDependencies": {
@@ -139,12 +144,14 @@
139
144
  "@tursodatabase/serverless": "^1.4.0",
140
145
  "@types/better-sqlite3": "^9.6.0",
141
146
  "@types/express": "^5.0.6",
147
+ "@types/mssql": "^12.3.0",
142
148
  "@types/pg": "^8.23.1",
143
149
  "@types/ws": "^8.18.1",
144
150
  "better-sqlite3": "^13.0.3",
145
151
  "express": "^5.2.1",
146
152
  "mariadb": "^3.5.4",
147
153
  "mongodb": "^7.6.0",
154
+ "mssql": "^11",
148
155
  "mysql2": "^3.24.4",
149
156
  "pg": "^8.23.0",
150
157
  "pg-query-stream": "^4.17.0",
@@ -185,6 +192,9 @@
185
192
  "sqlite",
186
193
  "sqlite3",
187
194
  "cockroachdb",
195
+ "mssql",
196
+ "sqlserver",
197
+ "sql-server",
188
198
  "mongodb",
189
199
  "mongo",
190
200
  "libsql",