uql-orm 0.24.5 → 0.24.7

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 (62) hide show
  1. package/README.md +9 -8
  2. package/dist/cockroachdb/crdbQuerierPool.d.ts +1 -1
  3. package/dist/cockroachdb/crdbQuerierPool.js +5 -4
  4. package/dist/dialect/abstractSqlDialect.d.ts +78 -28
  5. package/dist/dialect/abstractSqlDialect.js +196 -147
  6. package/dist/dialect/hydrateColumn.d.ts +16 -0
  7. package/dist/dialect/hydrateColumn.js +66 -0
  8. package/dist/dialect/jsonSql.d.ts +24 -0
  9. package/dist/dialect/jsonSql.js +39 -0
  10. package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -0
  11. package/dist/dialect/mysqlLikeSqlDialect.js +10 -1
  12. package/dist/dialect/pgLikeSqlDialect.d.ts +3 -7
  13. package/dist/dialect/pgLikeSqlDialect.js +2 -14
  14. package/dist/dialect/vectorCast.d.ts +15 -0
  15. package/dist/dialect/vectorCast.js +58 -0
  16. package/dist/entity/metadata/definition.d.ts +0 -1
  17. package/dist/entity/metadata/definition.js +1 -1
  18. package/dist/maria/mariaDialect.d.ts +3 -2
  19. package/dist/maria/mariaDialect.js +3 -18
  20. package/dist/maria/mariadbQuerierPool.js +6 -1
  21. package/dist/migrate/builder/migrationBuilder.d.ts +12 -16
  22. package/dist/migrate/builder/migrationBuilder.js +24 -59
  23. package/dist/migrate/builder/tableBuilder.js +0 -12
  24. package/dist/migrate/cli.d.ts +0 -1
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +0 -3
  27. package/dist/migrate/drift/driftDetector.js +17 -15
  28. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +8 -2
  29. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +10 -9
  30. package/dist/migrate/introspection/mysqlIntrospector.d.ts +0 -3
  31. package/dist/migrate/introspection/mysqlIntrospector.js +0 -9
  32. package/dist/migrate/introspection/postgresIntrospector.d.ts +0 -3
  33. package/dist/migrate/introspection/postgresIntrospector.js +0 -12
  34. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  35. package/dist/migrate/introspection/sqliteIntrospector.js +1 -9
  36. package/dist/migrate/migrator.d.ts +8 -0
  37. package/dist/migrate/migrator.js +19 -29
  38. package/dist/migrate/schemaGenerator.js +0 -12
  39. package/dist/mongo/mongoDialect.d.ts +7 -0
  40. package/dist/mongo/mongoDialect.js +37 -1
  41. package/dist/mongo/mongodbQuerier.js +8 -3
  42. package/dist/neon/neonQuerierPool.d.ts +1 -1
  43. package/dist/neon/neonQuerierPool.js +6 -4
  44. package/dist/postgres/abstractPgQuerierPool.d.ts +6 -0
  45. package/dist/postgres/abstractPgQuerierPool.js +3 -0
  46. package/dist/postgres/pgNumericTypes.d.ts +41 -0
  47. package/dist/postgres/pgNumericTypes.js +35 -0
  48. package/dist/postgres/pgQuerierPool.d.ts +1 -1
  49. package/dist/postgres/pgQuerierPool.js +5 -4
  50. package/dist/querier/abstractQuerier.d.ts +0 -3
  51. package/dist/querier/abstractQuerier.js +10 -9
  52. package/dist/querier/abstractSqlQuerier.d.ts +9 -2
  53. package/dist/querier/abstractSqlQuerier.js +31 -24
  54. package/dist/schema/canonicalType.js +2 -12
  55. package/dist/schema/schemaAST.js +0 -24
  56. package/dist/schema/schemaASTBuilder.js +0 -3
  57. package/dist/type/query.d.ts +2 -0
  58. package/dist/type/queryAggregate.d.ts +3 -0
  59. package/dist/util/field.util.d.ts +4 -0
  60. package/dist/util/field.util.js +12 -0
  61. package/dist/util/sqlLiteral.js +18 -15
  62. package/package.json +5 -5
@@ -1,3 +1,4 @@
1
+ import { decodeColumn } from '../dialect/hydrateColumn.js';
1
2
  import { getMeta } from '../entity/index.js';
2
3
  import { buildUpdateResult, clone, getInsertFieldKeys, getRelationRequestSummary, isAutoIncrement, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
3
4
  import { AbstractQuerier } from './abstractQuerier.js';
@@ -54,7 +55,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
54
55
  const ctx = this.dialect.createContext();
55
56
  this.dialect.find(ctx, entity, q, opts);
56
57
  const res = await this.all(ctx.sql, ctx.values);
57
- const founds = unflatObjects(res).map((row) => this.hydrateJsonFields(entity, row));
58
+ const founds = unflatObjects(res).map((row) => this.hydrateFields(entity, row));
58
59
  await this.fillToManyRelations(entity, founds, q.$populate);
59
60
  return founds;
60
61
  }
@@ -74,7 +75,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
74
75
  try {
75
76
  for await (const row of this.internalStream(ctx.sql, normalizedParams)) {
76
77
  attrsPaths ??= obtainAttrsPaths(row);
77
- yield this.hydrateJsonFields(entity, unflatObject(row, attrsPaths));
78
+ yield this.hydrateFields(entity, unflatObject(row, attrsPaths));
78
79
  }
79
80
  }
80
81
  catch (err) {
@@ -90,31 +91,25 @@ export class AbstractSqlQuerier extends AbstractQuerier {
90
91
  const rows = await this.internalAll(query, this.dialect.normalizeValues(values));
91
92
  yield* rows;
92
93
  }
93
- hydrateJsonFields(entity, dto) {
94
- this.hydrateJsonFieldsRecursive(entity, dto, new WeakSet());
95
- return dto;
96
- }
97
- hydrateJsonFieldsRecursive(entity, dto, visited) {
94
+ /**
95
+ * Turn what a driver returned back into the types the entity declares, for the row and everything
96
+ * populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
97
+ * `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
98
+ *
99
+ * `visited` guards a populated graph that points back at itself; it defaults rather than living in
100
+ * a separate entry-point wrapper, because the wrapper's whole body was seeding it.
101
+ */
102
+ hydrateFields(entity, dto, visited = new WeakSet()) {
98
103
  if (!dto || typeof dto !== 'object' || visited.has(dto)) {
99
- return;
104
+ return dto;
100
105
  }
101
106
  visited.add(dto);
102
107
  const meta = getMeta(entity);
103
108
  const row = dto;
104
- for (const key in meta.fields) {
105
- const field = meta.fields[key];
106
- if (!field || (field.type !== 'json' && field.type !== 'jsonb')) {
107
- continue;
108
- }
109
+ for (const [key, kind] of this.dialect.hydratableFields(entity)) {
109
110
  const value = row[key];
110
- if (typeof value !== 'string') {
111
- continue;
112
- }
113
- try {
114
- row[key] = JSON.parse(value);
115
- }
116
- catch {
117
- // Keep the original value when the driver returns non-JSON text.
111
+ if (value != null) {
112
+ row[key] = decodeColumn(value, kind);
118
113
  }
119
114
  }
120
115
  for (const key in meta.relations) {
@@ -125,26 +120,38 @@ export class AbstractSqlQuerier extends AbstractQuerier {
125
120
  const value = row[key];
126
121
  if (Array.isArray(value)) {
127
122
  for (const it of value) {
128
- this.hydrateJsonFieldsRecursive(relEntity, it, visited);
123
+ this.hydrateFields(relEntity, it, visited);
129
124
  }
130
125
  continue;
131
126
  }
132
127
  if (value && typeof value === 'object') {
133
- this.hydrateJsonFieldsRecursive(relEntity, value, visited);
128
+ this.hydrateFields(relEntity, value, visited);
134
129
  }
135
130
  }
131
+ return dto;
136
132
  }
137
133
  async internalCount(entity, q = {}, opts) {
138
134
  const ctx = this.dialect.createContext();
139
135
  this.dialect.count(ctx, entity, q, opts);
140
136
  const res = await this.all(ctx.sql, ctx.values);
137
+ // `COUNT(*)` is BIGINT, which the pools decode at the wire - but a caller who supplies their own
138
+ // `types` replaces that, and the signature promises a number here regardless.
141
139
  return Number(res[0].count);
142
140
  }
143
141
  async internalAggregate(entity, q, opts) {
144
142
  const ctx = this.dialect.createContext();
145
143
  this.dialect.aggregate(ctx, entity, q, opts);
146
144
  // biome-ignore lint/suspicious/noExplicitAny: raw DB rows satisfy QueryAggregateResult at runtime but TS can't verify
147
- return this.all(ctx.sql, ctx.values);
145
+ const res = await this.all(ctx.sql, ctx.values);
146
+ const hydratable = this.dialect.hydratableAggregates(entity, q);
147
+ for (const row of res) {
148
+ for (const [alias, kind] of hydratable) {
149
+ if (row[alias] != null) {
150
+ row[alias] = decodeColumn(row[alias], kind);
151
+ }
152
+ }
153
+ }
154
+ return res;
148
155
  }
149
156
  async internalInsertMany(entity, payload) {
150
157
  if (!payload?.length) {
@@ -6,15 +6,10 @@
6
6
  * - Canonical types (dialect-agnostic)
7
7
  * - TypeScript types (for entity generation)
8
8
  */
9
- // ============================================================================
10
- // Vector Category Helpers
11
- // ============================================================================
12
9
  /** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
13
10
  export function isVectorCategory(category) {
14
11
  return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
15
12
  }
16
- // Type Mapping Tables
17
- // ============================================================================
18
13
  /**
19
14
  * Maps SQL type strings to canonical type categories.
20
15
  * Handles variations across dialects (PostgreSQL, MySQL, SQLite).
@@ -250,9 +245,6 @@ const CANONICAL_TO_TS = {
250
245
  halfvec: 'number[]',
251
246
  sparsevec: 'number[]',
252
247
  };
253
- // ============================================================================
254
- // Type Conversion Functions
255
- // ============================================================================
256
248
  /**
257
249
  * Parse a SQL type string into a canonical type.
258
250
  * Handles complex types like VARCHAR(255), DECIMAL(10,2), etc.
@@ -426,10 +418,8 @@ export function fieldOptionsToCanonical(options, tsType) {
426
418
  scale: options.scale,
427
419
  };
428
420
  }
429
- // Infer bigint for Number if autoIncrement is true or if it's a primary key
430
- if (options.autoIncrement || options.isId) {
431
- return { category: 'integer', size: 'big' };
432
- }
421
+ // BIGINT for every `Number`, key or not: a 32-bit column is a migration waiting to happen, and
422
+ // the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
433
423
  return { category: 'integer', size: 'big' };
434
424
  }
435
425
  if (type === Boolean) {
@@ -18,9 +18,6 @@ export class SchemaAST {
18
18
  tables = new Map();
19
19
  relationships = [];
20
20
  indexes = [];
21
- // ============================================================================
22
- // Table Operations
23
- // ============================================================================
24
21
  /**
25
22
  * Get a table by name.
26
23
  */
@@ -68,9 +65,6 @@ export class SchemaAST {
68
65
  getTableNames() {
69
66
  return Array.from(this.tables.keys());
70
67
  }
71
- // ============================================================================
72
- // Graph Navigation
73
- // ============================================================================
74
68
  /**
75
69
  * Get all tables that depend on this table (have FKs pointing to it).
76
70
  * These are tables that reference this table's primary key.
@@ -103,9 +97,6 @@ export class SchemaAST {
103
97
  getReferencedColumn(fkColumn) {
104
98
  return fkColumn.references?.to.columns[0];
105
99
  }
106
- // ============================================================================
107
- // Graph Analysis
108
- // ============================================================================
109
100
  /**
110
101
  * Detect circular foreign key dependencies.
111
102
  * Returns arrays of tables that form cycles.
@@ -179,9 +170,6 @@ export class SchemaAST {
179
170
  }
180
171
  return result;
181
172
  }
182
- // ============================================================================
183
- // Validation
184
- // ============================================================================
185
173
  /**
186
174
  * Validate schema integrity.
187
175
  * Checks for:
@@ -233,9 +221,6 @@ export class SchemaAST {
233
221
  isValid() {
234
222
  return this.validate().length === 0;
235
223
  }
236
- // ============================================================================
237
- // Smart Relation Detection
238
- // ============================================================================
239
224
  /**
240
225
  * Check if a table looks like a junction table (ManyToMany through).
241
226
  * Junction tables typically have:
@@ -292,9 +277,6 @@ export class SchemaAST {
292
277
  return 'ManyToMany';
293
278
  }
294
279
  }
295
- // ============================================================================
296
- // Index Operations
297
- // ============================================================================
298
280
  /**
299
281
  * Add an index to the schema.
300
282
  */
@@ -317,9 +299,6 @@ export class SchemaAST {
317
299
  getIndex(name) {
318
300
  return this.indexes.find((i) => i.name === name);
319
301
  }
320
- // ============================================================================
321
- // Relationship Operations
322
- // ============================================================================
323
302
  /**
324
303
  * Add a relationship to the schema.
325
304
  */
@@ -365,9 +344,6 @@ export class SchemaAST {
365
344
  this.relationships.splice(index, 1);
366
345
  return true;
367
346
  }
368
- // ============================================================================
369
- // Utility Methods
370
- // ============================================================================
371
347
  /**
372
348
  * Create a deep clone of this schema.
373
349
  */
@@ -34,9 +34,6 @@ export class SchemaASTBuilder {
34
34
  getAST() {
35
35
  return this.ast;
36
36
  }
37
- // ============================================================================
38
- // Build from Entities
39
- // ============================================================================
40
37
  /**
41
38
  * Build AST from entity classes (decorated with @Entity, @Field, etc.)
42
39
  */
@@ -172,6 +172,8 @@ export type Query<E> = {
172
172
  $populate?: QueryPopulate<E>;
173
173
  /**
174
174
  * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.
175
+ * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept
176
+ * regardless, since subtracting them would leave the relation unfilled.
175
177
  */
176
178
  $exclude?: QueryExclude<E>;
177
179
  /**
@@ -107,6 +107,9 @@ type FieldValueType<E, F> = F extends keyof E ? E[F] : unknown;
107
107
  /**
108
108
  * Resolves a single computed column's type from its aggregate function: `$count`/`$sum`/`$avg` are
109
109
  * always `number`; `$min`/`$max` keep the aggregated field's own type.
110
+ *
111
+ * `$sum`/`$avg` are exact to 2^53: Postgres widens a sum over BIGINT to NUMERIC, and decoding that
112
+ * text to satisfy this `number` drops the digits past that bound. Use `raw()` for a wider total.
110
113
  * @internal
111
114
  */
112
115
  type QueryAggregateFnResult<E, Fn> = Fn extends QueryAggregateNumericFn ? number : Fn extends {
@@ -3,6 +3,10 @@ import type { FieldOptions } from '../type/index.js';
3
3
  * Checks if a field type is numeric (Number, BigInt, or explicit numeric logical types)
4
4
  */
5
5
  export declare function isNumericType(type: unknown): boolean;
6
+ /**
7
+ * Checks if a field type is boolean (Boolean, or an explicit boolean logical type)
8
+ */
9
+ export declare function isBooleanType(type: unknown): boolean;
6
10
  /**
7
11
  * Checks if a field type is JSON
8
12
  */
@@ -31,6 +31,18 @@ export function isNumericType(type) {
31
31
  }
32
32
  return false;
33
33
  }
34
+ /**
35
+ * Checks if a field type is boolean (Boolean, or an explicit boolean logical type)
36
+ */
37
+ export function isBooleanType(type) {
38
+ if (type === Boolean)
39
+ return true;
40
+ if (typeof type === 'string') {
41
+ const lowered = type.toLowerCase();
42
+ return lowered === 'bool' || lowered === 'boolean';
43
+ }
44
+ return false;
45
+ }
34
46
  /**
35
47
  * Checks if a field type is JSON
36
48
  */
@@ -64,6 +64,22 @@ function createEscaper(escapeString) {
64
64
  }
65
65
  return sql;
66
66
  };
67
+ /** Split out so the `typeof` switch below stays a flat one-line-per-type dispatch. */
68
+ const escapeObject = (value) => {
69
+ if (value instanceof Date) {
70
+ return Number.isNaN(value.getTime()) ? 'NULL' : dateLiteral(value);
71
+ }
72
+ if (Array.isArray(value)) {
73
+ return sqlList(value);
74
+ }
75
+ if (isByteSource(value)) {
76
+ return bytesToHexLiteral(value);
77
+ }
78
+ if ('toSqlString' in value && typeof value.toSqlString === 'function') {
79
+ return String(value.toSqlString());
80
+ }
81
+ throw new TypeError('escapeSqlLiteral: plain objects are not supported; use bound parameters or JSON.stringify + a string column.');
82
+ };
67
83
  const escapeValue = (value) => {
68
84
  if (value === undefined || value === null) {
69
85
  return 'NULL';
@@ -77,24 +93,11 @@ function createEscaper(escapeString) {
77
93
  return String(value);
78
94
  case 'string':
79
95
  return escapeString(value);
96
+ case 'object':
97
+ return escapeObject(value);
80
98
  case 'symbol':
81
99
  case 'function':
82
100
  throw new TypeError('escapeSqlLiteral: symbol and function values are not supported; use bound parameters.');
83
- case 'object': {
84
- if (value instanceof Date) {
85
- return Number.isNaN(value.getTime()) ? 'NULL' : dateLiteral(value);
86
- }
87
- if (Array.isArray(value)) {
88
- return sqlList(value);
89
- }
90
- if (isByteSource(value)) {
91
- return bytesToHexLiteral(value);
92
- }
93
- if ('toSqlString' in value && typeof value.toSqlString === 'function') {
94
- return String(value.toSqlString());
95
- }
96
- throw new TypeError('escapeSqlLiteral: plain objects are not supported; use bound parameters or JSON.stringify + a string column.');
97
- }
98
101
  default:
99
102
  // Unreachable today; throwing keeps a future JS type from silently becoming SQL.
100
103
  throw new TypeError(`escapeSqlLiteral: unsupported value type '${typeof value}'; use bound parameters.`);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
5
5
  "license": "MIT",
6
- "version": "0.24.5",
6
+ "version": "0.24.7",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -134,9 +134,9 @@
134
134
  "@tursodatabase/serverless": "^1.4.0",
135
135
  "@types/better-sqlite3": "^9.6.0",
136
136
  "@types/express": "^5.0.6",
137
- "@types/pg": "^8.20.3",
137
+ "@types/pg": "^8.21.0",
138
138
  "@types/ws": "^8.18.1",
139
- "better-sqlite3": "^13.0.2",
139
+ "better-sqlite3": "^13.0.3",
140
140
  "express": "^5.2.1",
141
141
  "mariadb": "^3.5.3",
142
142
  "mongodb": "^7.5.0",
@@ -145,7 +145,7 @@
145
145
  "pg-query-stream": "^4.16.0",
146
146
  "rxjs": "^7.8.2",
147
147
  "sqlite-vec": "^0.1.9",
148
- "ws": "^8.21.1"
148
+ "ws": "^8.21.3"
149
149
  },
150
150
  "author": "Roger Padilla",
151
151
  "repository": {
@@ -198,5 +198,5 @@
198
198
  "publishConfig": {
199
199
  "access": "public"
200
200
  },
201
- "gitHead": "1c763ccfdb4431afa49716b99512d9df4e0ad4b7"
201
+ "gitHead": "26ffc7e2968761bc95a0bb1ffe0aeb37600ba1f1"
202
202
  }