uql-orm 0.24.6 → 0.25.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 (66) 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 -27
  5. package/dist/dialect/abstractSqlDialect.js +194 -130
  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/generator/mongoSchemaGenerator.d.ts +9 -1
  29. package/dist/migrate/generator/mongoSchemaGenerator.js +18 -0
  30. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +8 -2
  31. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +10 -9
  32. package/dist/migrate/introspection/mysqlIntrospector.d.ts +0 -3
  33. package/dist/migrate/introspection/mysqlIntrospector.js +0 -9
  34. package/dist/migrate/introspection/postgresIntrospector.d.ts +0 -3
  35. package/dist/migrate/introspection/postgresIntrospector.js +0 -12
  36. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  37. package/dist/migrate/introspection/sqliteIntrospector.js +1 -9
  38. package/dist/migrate/migrator.d.ts +17 -0
  39. package/dist/migrate/migrator.js +51 -50
  40. package/dist/migrate/schemaGenerator.d.ts +19 -8
  41. package/dist/migrate/schemaGenerator.js +48 -17
  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/abstractSqlQuerier.d.ts +9 -2
  51. package/dist/querier/abstractSqlQuerier.js +34 -25
  52. package/dist/schema/canonicalType.js +2 -12
  53. package/dist/schema/schemaAST.js +0 -24
  54. package/dist/schema/schemaASTBuilder.d.ts +1 -1
  55. package/dist/schema/schemaASTBuilder.js +4 -5
  56. package/dist/sqlite/nodeSqliteQuerierPool.js +1 -0
  57. package/dist/sqlite/sqliteQuerierPool.d.ts +5 -0
  58. package/dist/sqlite/sqliteQuerierPool.js +7 -0
  59. package/dist/turso/tursoLocalQuerierPool.js +1 -0
  60. package/dist/type/entity.d.ts +16 -2
  61. package/dist/type/migration.d.ts +30 -9
  62. package/dist/type/queryAggregate.d.ts +3 -0
  63. package/dist/util/field.util.d.ts +4 -0
  64. package/dist/util/field.util.js +12 -0
  65. package/dist/util/sqlLiteral.js +18 -15
  66. package/package.json +6 -6
@@ -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) {
@@ -224,10 +231,12 @@ export class AbstractSqlQuerier extends AbstractQuerier {
224
231
  return 0;
225
232
  }
226
233
  const ids = founds.map((it) => it[meta.id]);
234
+ // Children first: they hold the foreign key, so deleting the parent ahead of them is rejected
235
+ // outright by any schema that declares the constraint without `ON DELETE CASCADE`.
236
+ await this.deleteRelations(entity, ids, opts);
227
237
  const deleteCtx = this.dialect.createContext();
228
238
  this.dialect.delete(deleteCtx, entity, { $where: ids }, opts);
229
239
  const { changes = 0 } = await this.run(deleteCtx.sql, deleteCtx.values);
230
- await this.deleteRelations(entity, ids, opts);
231
240
  return changes;
232
241
  }
233
242
  get hasOpenTransaction() {
@@ -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
  */
@@ -41,7 +41,7 @@ export declare class SchemaASTBuilder {
41
41
  /**
42
42
  * Build AST from entity classes (decorated with @Entity, @Field, etc.)
43
43
  */
44
- fromEntities(entities: Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
44
+ fromEntities(entities: readonly Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
45
45
  /**
46
46
  * Resolve the canonical type for a field, inheriting from the referenced
47
47
  * entity's primary key when the field is a foreign-key reference
@@ -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
  */
@@ -184,8 +181,10 @@ export class SchemaASTBuilder {
184
181
  type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
185
182
  from: { table, columns: [localColumn] },
186
183
  to: { table: relatedTable, columns: [foreignColumn] },
187
- onDelete: options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
188
- onUpdate: options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
184
+ // The relation's own action wins over the global default, so one relation can cascade in the
185
+ // database while the rest stay on `NO ACTION`.
186
+ onDelete: relation.onDelete ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
187
+ onUpdate: relation.onUpdate ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
189
188
  confidence: 1.0,
190
189
  inferredFrom: 'entity_decorator',
191
190
  };
@@ -24,6 +24,7 @@ export class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool {
24
24
  ...(extensions?.length ? { allowExtension: true } : undefined),
25
25
  });
26
26
  nodeDb.exec('PRAGMA journal_mode = WAL');
27
+ nodeDb.exec('PRAGMA foreign_keys = ON');
27
28
  return adaptNodeSqlite(nodeDb);
28
29
  }
29
30
  }
@@ -11,5 +11,10 @@ export type Sqlite3PoolOptions = Options & LocalSqlitePoolOptions;
11
11
  export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<Sqlite3PoolOptions> {
12
12
  readonly filename: string | Buffer;
13
13
  constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions, extra?: ExtraOptions);
14
+ /**
15
+ * SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
16
+ * constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
17
+ * nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
18
+ */
14
19
  protected createDb(): Promise<SqliteDatabase>;
15
20
  }
@@ -9,6 +9,11 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
9
9
  super(opts, extra);
10
10
  this.filename = filename;
11
11
  }
12
+ /**
13
+ * SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
14
+ * constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
15
+ * nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
16
+ */
12
17
  async createDb() {
13
18
  // `bun:sqlite` rejects option keys it does not know, and rejects an options object carrying no
14
19
  // open flags, so `extensions` is stripped out and what remains of it collapses back to nothing.
@@ -19,11 +24,13 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
19
24
  const { adaptBunSqlite } = await import('./bunSqliteAdapter.bun.js');
20
25
  const bunDb = new BunDatabase(this.filename, opts);
21
26
  bunDb.run('PRAGMA journal_mode = WAL');
27
+ bunDb.run('PRAGMA foreign_keys = ON');
22
28
  return adaptBunSqlite(bunDb);
23
29
  }
24
30
  const { default: BetterSqlite3 } = await import('better-sqlite3');
25
31
  const db = new BetterSqlite3(this.filename, opts);
26
32
  db.pragma('journal_mode = WAL');
33
+ db.pragma('foreign_keys = ON');
27
34
  return db;
28
35
  }
29
36
  }
@@ -30,6 +30,7 @@ export class TursoLocalQuerierPool extends AbstractSqlQuerierPool {
30
30
  // Annotated rather than cast, so the structural contract is checked against the real driver.
31
31
  const db = await connect(this.filename, this.opts);
32
32
  await db.pragma('journal_mode = WAL');
33
+ await db.pragma('foreign_keys = ON');
33
34
  return db;
34
35
  }
35
36
  async end() {
@@ -1,4 +1,4 @@
1
- import type { IndexType } from '../schema/types.js';
1
+ import type { ForeignKeyAction, IndexType } from '../schema/types.js';
2
2
  import type { FilterOptions } from './query.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { DistributiveOmit, Json, Scalar, Type, Unpacked } from './utility.js';
@@ -362,6 +362,20 @@ export type RelationOptions<E = any> = {
362
362
  entity: EntityGetter<E>;
363
363
  cardinality: RelationCardinality;
364
364
  readonly cascade?: boolean | CascadeType;
365
+ /**
366
+ * Referential actions for the generated foreign key, letting the database do the work instead of the
367
+ * ORM: `onDelete: 'CASCADE'` makes deleting the parent a single statement rather than the graph walk
368
+ * `cascade` performs. Defaults to the migrator's `defaultForeignKeyAction`.
369
+ *
370
+ * Read only from the side that owns the key (`@ManyToOne`, or a `@OneToOne` without `mappedBy`),
371
+ * matching where TypeORM, Prisma and Drizzle put it. Not available on a bare `@Field({ references })`
372
+ * with no relation, because `FieldOptions.onUpdate` already means a value callback there.
373
+ *
374
+ * This overlaps `cascade` rather than conflicting with it: declaring both deletes the children in JS
375
+ * and then leaves the foreign key nothing to cascade, so pick one.
376
+ */
377
+ readonly onDelete?: ForeignKeyAction;
378
+ readonly onUpdate?: ForeignKeyAction;
365
379
  mappedBy?: RelationMappedBy<E>;
366
380
  /**
367
381
  * The pivot entity of a many-to-many. Unconstrained by `E`: a pivot holds foreign keys to both
@@ -392,7 +406,7 @@ type RelationOwnerJoin<E> = Required<Pick<RelationOptions<E>, 'through'>> | Requ
392
406
  * without one of the three, resolution has no columns to join on and throws.
393
407
  */
394
408
  type RelationJoin<E> = RelationOwnerJoin<E> | Required<Pick<RelationOptions<E>, 'mappedBy'>>;
395
- type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade'>;
409
+ type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade' | 'onDelete' | 'onUpdate'>;
396
410
  type RelationOptionsInverseSide<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & Required<Pick<RelationOptions<E>, 'mappedBy'>>;
397
411
  type RelationOptionsThroughOwner<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & RelationOwnerJoin<E>;
398
412
  /**
@@ -174,22 +174,43 @@ export interface SchemaDiff {
174
174
  readonly foreignKeysToAdd?: ForeignKeySchema[];
175
175
  readonly foreignKeysToDrop?: string[];
176
176
  }
177
+ export interface CreateSchemaOptions {
178
+ readonly ifNotExists?: boolean;
179
+ /**
180
+ * Restrict which tables are created, for an incremental migration adding one table to a schema that
181
+ * already exists. Constraints still resolve against the full entity graph.
182
+ */
183
+ readonly only?: readonly string[];
184
+ /**
185
+ * Emit the tables without their foreign keys. Only the integration fixtures want this, and only until
186
+ * their data stops relying on dangling references; a migration always wants the constraints.
187
+ */
188
+ readonly foreignKeys?: boolean;
189
+ }
190
+ export interface DropSchemaOptions {
191
+ readonly ifExists?: boolean;
192
+ readonly cascade?: boolean;
193
+ }
177
194
  /**
178
195
  * Interface for generating DDL statements from entity metadata
179
196
  */
180
197
  export interface SchemaGenerator {
181
198
  /**
182
- * DDL to create an entity’s table: one element per `querier.run` (optional `CREATE EXTENSION`, `CREATE TABLE`, each `CREATE INDEX`, …).
183
- * Join with `'\n'` if you need a single script blob.
199
+ * The whole schema for `entities`: every table, then the foreign keys between them.
200
+ *
201
+ * There is deliberately no per-entity counterpart. One entity means an AST holding one table, so every
202
+ * cross-entity foreign key has nothing to resolve against and is dropped: all three call sites that
203
+ * used to work that way emitted schemas with no referential integrity.
184
204
  */
185
- generateCreateTable<E>(entity: Type<E>, options?: {
186
- ifNotExists?: boolean;
187
- }): string[];
205
+ generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
206
+ /**
207
+ * Every `DROP TABLE` for `entities`, dependents first. The inverse of {@link generateCreateSchema},
208
+ * and the reason it takes the whole set: dropping in any order that ignores the relation graph is
209
+ * rejected once the foreign keys are really there.
210
+ */
211
+ generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
188
212
  /** Generate DROP TABLE statement. */
189
- generateDropTable(tableName: string, options?: {
190
- ifExists?: boolean;
191
- cascade?: boolean;
192
- }): string;
213
+ generateDropTable(tableName: string, options?: DropSchemaOptions): string;
193
214
  /**
194
215
  * Generate ALTER TABLE statements based on schema diff
195
216
  */
@@ -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.6",
6
+ "version": "0.25.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -134,18 +134,18 @@
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.4",
137
+ "@types/pg": "^8.21.0",
138
138
  "@types/ws": "^8.18.1",
139
139
  "better-sqlite3": "^13.0.3",
140
140
  "express": "^5.2.1",
141
141
  "mariadb": "^3.5.3",
142
142
  "mongodb": "^7.5.0",
143
143
  "mysql2": "^3.23.2",
144
- "pg": "^8.22.0",
145
- "pg-query-stream": "^4.16.0",
144
+ "pg": "^8.23.0",
145
+ "pg-query-stream": "^4.17.0",
146
146
  "rxjs": "^7.8.2",
147
147
  "sqlite-vec": "^0.1.9",
148
- "ws": "^8.21.2"
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": "2610226954cdf152705dc47ca130f6908a91825d"
201
+ "gitHead": "cfe2b005308b808d2e6e3d6e062c13791c883d13"
202
202
  }