uql-orm 0.51.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/bunSql/bunSql.util.d.ts +33 -10
  3. package/dist/bunSql/bunSql.util.js +57 -42
  4. package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
  5. package/dist/bunSql/bunSqlQuerier.js +17 -8
  6. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
  7. package/dist/bunSql/bunSqlQuerierPool.js +38 -25
  8. package/dist/bunSql/index.d.ts +1 -3
  9. package/dist/bunSql/index.js +0 -3
  10. package/dist/dialect/abstractSqlDialect.d.ts +58 -4
  11. package/dist/dialect/abstractSqlDialect.js +85 -24
  12. package/dist/dialect/aliases.d.ts +2 -0
  13. package/dist/dialect/aliases.js +2 -0
  14. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  15. package/dist/dialect/mergeSqlDialect.js +89 -0
  16. package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
  17. package/dist/dialect/pgLikeSqlDialect.js +4 -1
  18. package/dist/migrate/builder/expressions.js +5 -0
  19. package/dist/migrate/introspection/index.d.ts +2 -0
  20. package/dist/migrate/introspection/index.js +2 -0
  21. package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
  22. package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
  23. package/dist/migrate/introspection/registry.d.ts +3 -0
  24. package/dist/migrate/introspection/registry.js +28 -0
  25. package/dist/migrate/migrator.js +2 -21
  26. package/dist/mongo/mongoDialect.js +4 -1
  27. package/dist/mssql/index.d.ts +3 -0
  28. package/dist/mssql/index.js +3 -0
  29. package/dist/mssql/mssqlDialect.d.ts +144 -0
  30. package/dist/mssql/mssqlDialect.js +328 -0
  31. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  32. package/dist/mssql/mssqlQuerier.js +137 -0
  33. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  34. package/dist/mssql/mssqlQuerierPool.js +32 -0
  35. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  36. package/dist/mssql/mssqlWireTypes.js +44 -0
  37. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  38. package/dist/pglite/pgliteQuerier.js +7 -2
  39. package/dist/postgres/pgCursorStream.d.ts +20 -0
  40. package/dist/postgres/pgCursorStream.js +49 -0
  41. package/dist/postgres/pgDialect.d.ts +1 -1
  42. package/dist/postgres/pgDialect.js +1 -1
  43. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  44. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  45. package/dist/schema/canonicalType.js +96 -113
  46. package/dist/sqlite/sqliteDialect.js +4 -1
  47. package/dist/type/dialect.d.ts +31 -4
  48. package/dist/type/migratorDialect.d.ts +1 -1
  49. package/dist/type/migratorDialect.js +1 -0
  50. package/package.json +13 -3
  51. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  52. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  53. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  54. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  55. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  56. package/dist/bunSql/bunSqliteDialect.js +0 -6
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Runs one statement of the cursor protocol. `internalAll` for every caller so far: the cursor holds
3
+ * a connection's session state, so it must be the querier's own connection, and it must not go
4
+ * through `all()`, whose `serialize` is not re-entrant.
5
+ */
6
+ export type CursorExecutor<T> = (query: string, values?: unknown[]) => Promise<T[]>;
7
+ /**
8
+ * Stream a Postgres-wire result through a server-side cursor, for a driver whose client exposes none:
9
+ * `bun:sql` (no cursor API at all, [oven-sh/bun#17181](https://github.com/oven-sh/bun/issues/17181))
10
+ * and PGlite. `pg` has `pg-query-stream` and keeps using it.
11
+ *
12
+ * `DECLARE` is only legal inside a transaction, so one is opened here when the caller has none - and
13
+ * then committed, or rolled back if the stream failed. That `BEGIN` goes straight to the connection
14
+ * rather than through `beginTransaction`, so the querier's own transaction state stays untouched:
15
+ * this one is the generator's, and ends with it.
16
+ *
17
+ * The cleanup lives in `finally` because a consumer that stops early (`break`, a `throw` downstream)
18
+ * ends the generator there and nowhere else, and an abandoned cursor holds its transaction open.
19
+ */
20
+ export declare function streamViaCursor<T>(exec: CursorExecutor<T>, query: string, values?: unknown[], inTransaction?: boolean): AsyncIterable<T>;
@@ -0,0 +1,49 @@
1
+ /** The cursor a stream declares. Suffixed per call, so two streams on one connection cannot collide. */
2
+ const CURSOR_ALIAS = '_uql_cursor';
3
+ /** Rows per round trip, matching `pg-query-stream`'s own default so both paths read the same. */
4
+ const FETCH_SIZE = 100;
5
+ let cursorSeq = 0;
6
+ /**
7
+ * Stream a Postgres-wire result through a server-side cursor, for a driver whose client exposes none:
8
+ * `bun:sql` (no cursor API at all, [oven-sh/bun#17181](https://github.com/oven-sh/bun/issues/17181))
9
+ * and PGlite. `pg` has `pg-query-stream` and keeps using it.
10
+ *
11
+ * `DECLARE` is only legal inside a transaction, so one is opened here when the caller has none - and
12
+ * then committed, or rolled back if the stream failed. That `BEGIN` goes straight to the connection
13
+ * rather than through `beginTransaction`, so the querier's own transaction state stays untouched:
14
+ * this one is the generator's, and ends with it.
15
+ *
16
+ * The cleanup lives in `finally` because a consumer that stops early (`break`, a `throw` downstream)
17
+ * ends the generator there and nowhere else, and an abandoned cursor holds its transaction open.
18
+ */
19
+ export async function* streamViaCursor(exec, query, values, inTransaction = false) {
20
+ const cursor = `${CURSOR_ALIAS}_${++cursorSeq}`;
21
+ const ownsTransaction = !inTransaction;
22
+ if (ownsTransaction) {
23
+ await exec('BEGIN');
24
+ }
25
+ let failed = false;
26
+ try {
27
+ await exec(`DECLARE ${cursor} CURSOR FOR ${query}`, values);
28
+ for (;;) {
29
+ const rows = await exec(`FETCH FORWARD ${FETCH_SIZE} FROM ${cursor}`);
30
+ yield* rows;
31
+ if (rows.length < FETCH_SIZE) {
32
+ return;
33
+ }
34
+ }
35
+ }
36
+ catch (err) {
37
+ failed = true;
38
+ throw err;
39
+ }
40
+ finally {
41
+ // Best-effort once the stream has failed: an error raised here would replace the one that brought
42
+ // us here, which is the one worth reporting, and the rollback discards the cursor either way.
43
+ const end = failed ? (sql) => exec(sql).catch(() => []) : exec;
44
+ await end(`CLOSE ${cursor}`);
45
+ if (ownsTransaction) {
46
+ await end(failed ? 'ROLLBACK' : 'COMMIT');
47
+ }
48
+ }
49
+ }
@@ -4,7 +4,7 @@ import { PostgresDialect } from './postgresDialect.js';
4
4
  *
5
5
  * @remarks Uses base {@link PostgresDialect} capabilities: native JS arrays for `ANY` / `ALL`
6
6
  * (`nativeArrays: true`) and `$n::jsonb` without a text re-cast. Bun SQL Postgres needs
7
- * `BunSqlPostgresDialect` from `uql-orm/bunSql` instead (wire array literals + text json cast).
7
+ * `BunSqlQuerierPool` from `uql-orm/bunSql` instead (wire array literals + text json cast).
8
8
  */
9
9
  export declare class PgDialect extends PostgresDialect {
10
10
  }
@@ -4,7 +4,7 @@ import { PostgresDialect } from './postgresDialect.js';
4
4
  *
5
5
  * @remarks Uses base {@link PostgresDialect} capabilities: native JS arrays for `ANY` / `ALL`
6
6
  * (`nativeArrays: true`) and `$n::jsonb` without a text re-cast. Bun SQL Postgres needs
7
- * `BunSqlPostgresDialect` from `uql-orm/bunSql` instead (wire array literals + text json cast).
7
+ * `BunSqlQuerierPool` from `uql-orm/bunSql` instead (wire array literals + text json cast).
8
8
  */
9
9
  export class PgDialect extends PostgresDialect {
10
10
  }
@@ -1,19 +1,21 @@
1
1
  /**
2
- * Wire-style parameter shaping for **Bun SQL** (and similar clients) on any Postgres-wire dialect:
3
- * arrays are sent as string literals (`nativeArrays: false`) via {@link PgLikeSqlDialect}'s
4
- * `toPgArray` path.
2
+ * Driver-shaped parameter handling for **Bun SQL** (and any client like it) on a Postgres-wire
3
+ * dialect: arrays go as string literals (`nativeArrays: false`, {@link PgLikeSqlDialect}'s `toPgArray`
4
+ * path) and a JSON bind is re-cast through text (`explicitJsonCast: true`). Both are measured, on a
5
+ * live server: `bun:sql` binds neither `sql.array(...)` nor a plain JS array through `unsafe()`
6
+ * (verified again on Bun 1.4.2), and without the text re-cast a `$set`/`$push` on a JSONB column
7
+ * silently writes the wrong value or throws - on Postgres and, identically, on CockroachDB, which
8
+ * `bun:sql` reaches through its own Postgres wire implementation.
5
9
  *
6
- * `PgDialect` does **not** use this constant - it keeps base {@link PgLikeSqlDialect} defaults
7
- * (`nativeArrays: true`, `explicitJsonCast: false`), since node-`pg` doesn't need the fix.
8
- * `BunSqlPostgresDialect` and `BunSqlCockroachDialect` both spread this and set
9
- * `explicitJsonCast: true` - `bun:sql` routes CockroachDB through its own Postgres wire-protocol
10
- * implementation (see `bunSql.util.ts#normalizeBunOpts`), so it needs the identical fix: verified
11
- * directly that without it, `$set`/`$push` on a JSONB column silently produce the wrong value
12
- * or throw on a live CockroachDB instance.
10
+ * The pair is one constant because it is one driver's shape, and `BunSqlQuerierPool` hands it to
11
+ * `PostgresDialect`/`CockroachDialect` as their `driverCapabilities` rather than subclassing either:
12
+ * Bun changes how a parameter binds, never the SQL. `PgDialect` uses neither, keeping the base
13
+ * {@link PgLikeSqlDialect} defaults, since node-`pg` needs no fix.
13
14
  *
14
15
  * @remarks Optional import for custom pools. Neon uses its own serverless driver (not `bun:sql`),
15
16
  * so `NeonDialect` is a separate, unverified case - do not assume it needs this without testing.
16
17
  */
17
18
  export declare const POSTGRES_WIRE_DRIVER_CAPABILITIES: {
18
19
  readonly nativeArrays: false;
20
+ readonly explicitJsonCast: true;
19
21
  };
@@ -1,19 +1,21 @@
1
1
  /**
2
- * Wire-style parameter shaping for **Bun SQL** (and similar clients) on any Postgres-wire dialect:
3
- * arrays are sent as string literals (`nativeArrays: false`) via {@link PgLikeSqlDialect}'s
4
- * `toPgArray` path.
2
+ * Driver-shaped parameter handling for **Bun SQL** (and any client like it) on a Postgres-wire
3
+ * dialect: arrays go as string literals (`nativeArrays: false`, {@link PgLikeSqlDialect}'s `toPgArray`
4
+ * path) and a JSON bind is re-cast through text (`explicitJsonCast: true`). Both are measured, on a
5
+ * live server: `bun:sql` binds neither `sql.array(...)` nor a plain JS array through `unsafe()`
6
+ * (verified again on Bun 1.4.2), and without the text re-cast a `$set`/`$push` on a JSONB column
7
+ * silently writes the wrong value or throws - on Postgres and, identically, on CockroachDB, which
8
+ * `bun:sql` reaches through its own Postgres wire implementation.
5
9
  *
6
- * `PgDialect` does **not** use this constant - it keeps base {@link PgLikeSqlDialect} defaults
7
- * (`nativeArrays: true`, `explicitJsonCast: false`), since node-`pg` doesn't need the fix.
8
- * `BunSqlPostgresDialect` and `BunSqlCockroachDialect` both spread this and set
9
- * `explicitJsonCast: true` - `bun:sql` routes CockroachDB through its own Postgres wire-protocol
10
- * implementation (see `bunSql.util.ts#normalizeBunOpts`), so it needs the identical fix: verified
11
- * directly that without it, `$set`/`$push` on a JSONB column silently produce the wrong value
12
- * or throw on a live CockroachDB instance.
10
+ * The pair is one constant because it is one driver's shape, and `BunSqlQuerierPool` hands it to
11
+ * `PostgresDialect`/`CockroachDialect` as their `driverCapabilities` rather than subclassing either:
12
+ * Bun changes how a parameter binds, never the SQL. `PgDialect` uses neither, keeping the base
13
+ * {@link PgLikeSqlDialect} defaults, since node-`pg` needs no fix.
13
14
  *
14
15
  * @remarks Optional import for custom pools. Neon uses its own serverless driver (not `bun:sql`),
15
16
  * so `NeonDialect` is a separate, unverified case - do not assume it needs this without testing.
16
17
  */
17
18
  export const POSTGRES_WIRE_DRIVER_CAPABILITIES = {
18
19
  nativeArrays: false,
20
+ explicitJsonCast: true,
19
21
  };
@@ -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.
@@ -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 = '`';
@@ -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
  /**
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.51.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",
@@ -1,12 +0,0 @@
1
- import { CockroachDialect } from '../cockroachdb/cockroachDialect.js';
2
- import type { DialectFeatures } from '../type/index.js';
3
- /**
4
- * CockroachDB Dialect specialization for the `bun:sql` driver, which routes CockroachDB
5
- * connections through its own Postgres wire-protocol implementation (see
6
- * `bunSql.util.ts#normalizeBunOpts`), so it needs the identical fix as {@link BunSqlPostgresDialect}:
7
- * without it, `$set`/`$push` on a JSONB column silently produce the wrong value or throw
8
- * (verified directly against a live CockroachDB instance via `bun:sql`).
9
- */
10
- export declare class BunSqlCockroachDialect extends CockroachDialect {
11
- protected readonly featureOverrides: Partial<DialectFeatures>;
12
- }
@@ -1,15 +0,0 @@
1
- import { CockroachDialect } from '../cockroachdb/cockroachDialect.js';
2
- import { POSTGRES_WIRE_DRIVER_CAPABILITIES } from '../postgres/postgresWireDriverCapabilities.js';
3
- /**
4
- * CockroachDB Dialect specialization for the `bun:sql` driver, which routes CockroachDB
5
- * connections through its own Postgres wire-protocol implementation (see
6
- * `bunSql.util.ts#normalizeBunOpts`), so it needs the identical fix as {@link BunSqlPostgresDialect}:
7
- * without it, `$set`/`$push` on a JSONB column silently produce the wrong value or throw
8
- * (verified directly against a live CockroachDB instance via `bun:sql`).
9
- */
10
- export class BunSqlCockroachDialect extends CockroachDialect {
11
- featureOverrides = {
12
- ...POSTGRES_WIRE_DRIVER_CAPABILITIES,
13
- explicitJsonCast: true,
14
- };
15
- }
@@ -1,11 +0,0 @@
1
- import { PostgresDialect } from '../postgres/postgresDialect.js';
2
- import type { DialectFeatures } from '../type/index.js';
3
- /**
4
- * Postgres Dialect specialization for the `bun:sql` driver.
5
- *
6
- * @remarks Reuses wire array encoding plus `explicitJsonCast` so JSON merge/push binds
7
- * reliably; `PgDialect` omits the text re-cast.
8
- */
9
- export declare class BunSqlPostgresDialect extends PostgresDialect {
10
- protected readonly featureOverrides: Partial<DialectFeatures>;
11
- }