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
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <h3>The smartest TypeScript ORM</h3>
11
11
 
12
- <p>Type-safe to the leaf, serializable queries, no codegen, <a href="https://uql-orm.dev/benchmark">extremely fast</a>, and unified API across Postgres, CockroachDB, MySQL, SQLite, MongoDB, MariaDB, and the edge.</p>
12
+ <p>Type-safe to the leaf, serializable queries, no codegen, <a href="https://uql-orm.dev/benchmark">extremely fast</a>, and one API across every SQL database, MongoDB, and every runtime.</p>
13
13
 
14
14
  <p>
15
15
  <a href="https://uql-orm.dev"><b>Website</b></a> ·
@@ -37,7 +37,7 @@ and no compiler flag to turn on ([setup](https://uql-orm.dev/getting-started)),
37
37
  [imperative API](https://uql-orm.dev/entities/imperative) skips decorators altogether.
38
38
 
39
39
  ```ts
40
- await querier.findMany(User, {
40
+ await pool.findMany(User, {
41
41
  $select: { id: true, email: true },
42
42
  $populate: { posts: { $select: { title: true } } },
43
43
  $where: { email: { $endsWith: '@uql-orm.dev' } },
@@ -50,12 +50,13 @@ from the browser to the server. The same object runs on every supported database
50
50
 
51
51
  ## Why UQL?
52
52
 
53
- - **The fastest.** Wins [all 8 categories](https://uql-orm.dev/benchmark) of our [open benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), beating even query builders like Knex and Kysely: ~2.4× faster than the runner-up on average, over 4.6M ops/s on simple SELECTs.
54
- - **Light.** Zero runtime dependencies, 288 kB on the wire, every dialect included.
55
- - **Queries are data, not method chains.** Plain JSON in, typed rows out. There's no DSL to learn and nothing to compile.
56
- - **Type-safe to the leaf.** Operators are gated per field type, and JSON/JSONB dot-paths resolve each path's value type, so `{ age: { $like: 'x' } }` or a typo'd path is a compile error instead of a runtime surprise.
57
- - **No codegen.** Entities are TypeScript classes, so your code *is* the schema. No `.prisma` file to regenerate, no generated client to keep in sync.
58
- - **One API everywhere.** PostgreSQL, CockroachDB, MySQL, MariaDB, SQLite, Turso, LibSQL, Neon, Cloudflare D1, Bun SQL, and MongoDB.
53
+ - **The fastest.** Wins [all 8 categories](https://uql-orm.dev/benchmark) of our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), beating even query builders like Knex and Kysely: ~2.4× faster than the runner-up on average, reaching over 4.6M ops/s on simple SELECTs.
54
+ - **Light.** Zero runtime dependencies, 305 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
55
+ - **Queries are data (JSON), not method chains.** Plain JSON in, typed rows out. There's no DSL to learn and nothing to compile.
56
+ - **Type-safe to the leaf.** Every key is autocompleted and checked against your entity, down to the fields of a populated relation. Operators are gated per field type, and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths resolve each path's value type, so `$like` on a numeric column, or a typo'd path, is a compile error instead of a runtime surprise.
57
+ - **No codegen, no build step.** Entities are TypeScript classes, so your code *is* the schema. There's no `.prisma` file to regenerate and no generated client to keep in sync.
58
+ - **One API everywhere.** PostgreSQL, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun's native SQL, and even MongoDB!
59
+ - **Runs on every runtime.** Node 24+, Bun, Deno, [Cloudflare Workers](https://uql-orm.dev/cloudflare-d1), [AWS Lambda and Vercel](https://uql-orm.dev/serverless), and [the browser](https://uql-orm.dev/browser). ESM-only with no native binaries on the `fetch`-based drivers, so an edge bundle needs no special build.
59
60
  - **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [non-bypassable multi-tenant filters](https://uql-orm.dev/multi-tenancy), [entity-first migrations](https://uql-orm.dev/migrations), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
60
61
 
61
62
  ## Get started
@@ -9,5 +9,5 @@ import { CrdbQuerier } from './crdbQuerier.js';
9
9
  export declare class CrdbQuerierPool extends AbstractPgQuerierPool<PoolClient, CrdbQuerier, CockroachDialect> {
10
10
  readonly pool: Pool;
11
11
  constructor(opts: PoolConfig, extra?: ExtraOptions);
12
- getQuerier(): Promise<CrdbQuerier>;
12
+ protected buildQuerier(connect: () => Promise<PoolClient>): CrdbQuerier;
13
13
  }
@@ -1,5 +1,6 @@
1
- import { Pool } from 'pg';
1
+ import { Pool, types } from 'pg';
2
2
  import { AbstractPgQuerierPool } from '../postgres/abstractPgQuerierPool.js';
3
+ import { numericTypes } from '../postgres/pgNumericTypes.js';
3
4
  import { CockroachDialect } from './cockroachDialect.js';
4
5
  import { CrdbQuerier } from './crdbQuerier.js';
5
6
  /**
@@ -7,9 +8,9 @@ import { CrdbQuerier } from './crdbQuerier.js';
7
8
  */
8
9
  export class CrdbQuerierPool extends AbstractPgQuerierPool {
9
10
  constructor(opts, extra) {
10
- super(new CockroachDialect({ namingStrategy: extra?.namingStrategy }), new Pool({ keepAlive: true, ...opts }), extra);
11
+ super(new CockroachDialect({ namingStrategy: extra?.namingStrategy }), new Pool({ keepAlive: true, types: numericTypes(types), ...opts }), extra);
11
12
  }
12
- async getQuerier() {
13
- return new CrdbQuerier(() => this.pool.connect(), this.dialect, this.extra);
13
+ buildQuerier(connect) {
14
+ return new CrdbQuerier(connect, this.dialect, this.extra);
14
15
  }
15
16
  }
@@ -1,7 +1,11 @@
1
1
  import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, type QueryPopulate, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelect, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
2
+ import type { HydrateKind } from './hydrateColumn.js';
2
3
  import { IndexSqlDialect } from './indexSqlDialect.js';
3
4
  /** How a column's values are bound: see {@link AbstractSqlDialect.persistKind}. */
4
5
  type PersistKind = 'plain' | 'json' | 'vector';
6
+ /** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
7
+ type HydratableField = readonly [string, HydrateKind];
8
+ export type { HydrateKind };
5
9
  export declare abstract class AbstractSqlDialect extends IndexSqlDialect implements QueryDialect, SqlQueryDialect {
6
10
  abstract readonly dialectName: SqlDialectName;
7
11
  abstract readonly escapeIdChar: '"' | '`';
@@ -99,6 +103,20 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
99
103
  protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string | undefined;
100
104
  private appendFieldSql;
101
105
  compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, op: K, val: QueryWhereFieldOperatorMap<E>[K], opts?: QueryOptions): void;
106
+ /**
107
+ * Render `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, and
108
+ * report whether `op` was one of them.
109
+ *
110
+ * One implementation for three callers that each had their own: a WHERE column, a HAVING aggregate
111
+ * expression, and a `$size` count (which passes no operand, since its expression is already in the
112
+ * context). They previously disagreed - HAVING carried a second comparison-operator map and threw
113
+ * `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of the
114
+ * other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
115
+ *
116
+ * The operators kept out are the ones that need more than an operand: `$not` recurses through the
117
+ * entity, and `$all`/`$size`/`$elemMatch` address a JSON document.
118
+ */
119
+ protected appendOperatorCondition(ctx: QueryContext, operand: string | undefined, op: string, val: unknown): boolean;
102
120
  private appendLikeOp;
103
121
  private appendEqNe;
104
122
  private appendInNin;
@@ -116,19 +134,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
116
134
  * {@link jsonCompareMode} for why each mode exists.
117
135
  */
118
136
  protected jsonComparand(jsonField: string, value: unknown): string;
119
- /**
120
- * How a JSON scalar has to be compared against `value` (or, for `$in`/`$nin`, against every element
121
- * of it). Extracting a JSON value yields *text*, which loses the type, so each operand type is
122
- * compared in the representation every engine agrees on:
123
- * - `numeric` - cast the accessor. Keeps `1` equal to a stored `1.0`, which strict JSON equality
124
- * would not, and satisfies drivers that send typed parameters (`text = integer` otherwise).
125
- * - `json` - compare the JSON value against a JSON-encoded parameter. No cast recovers a boolean
126
- * portably: PostgreSQL raises `text = boolean` and MySQL matches `'true'` against `1`.
127
- * - `text` - compare as extracted, which is also what the string operators need.
128
- *
129
- * Mixed operand types fall back to `text`, since one comparison cannot be two shapes at once.
130
- */
131
- protected jsonCompareMode(value: unknown): 'json' | 'numeric' | 'text';
132
137
  /** `$all`: the JSON array at `jsonField` contains every value (also serves element containment). */
133
138
  protected abstract jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
134
139
  /** `$size`: the length of the JSON array at `jsonField`, compared against `value`. */
@@ -171,17 +176,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
171
176
  * - otherwise - per-field conditions over the exploded objects.
172
177
  */
173
178
  protected jsonElemMatch(ctx: QueryContext, jsonField: string, match: Record<string, unknown>): string;
174
- /**
175
- * Whether the operator reads the JSON *value* instead of its text form. The array operators always
176
- * do. Equality joins them for boolean operands, because extracting JSON as text loses the type in
177
- * a way no cast recovers portably: PostgreSQL raises `operator does not exist: text = boolean`,
178
- * MySQL compares `'true'` to `1` and silently matches nothing, and SQLite's `json_extract` yields
179
- * `1`. Comparing the JSON value against a JSON-encoded parameter is exact on every dialect.
180
- *
181
- * Numbers stay on the text accessor with a numeric cast ({@link jsonComparand}), which keeps
182
- * `1` equal to `1.0` - JSON equality would not.
183
- */
184
- protected isJsonbOp(op: string, value?: unknown): boolean;
185
179
  /**
186
180
  * A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
187
181
  * dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
@@ -204,7 +198,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
204
198
  private static readonly SORT_DIRECTION_MAP;
205
199
  private resolveSortDirection;
206
200
  /** Scalar comparison operators shared by `HAVING` conditions and `$size` comparisons. */
207
- private static readonly comparisonOpMap;
208
201
  protected havingCondition(ctx: QueryContext, expr: string, condition: QueryHavingMap[string]): void;
209
202
  find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions): void;
210
203
  insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
@@ -232,6 +225,15 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
232
225
  * bind into the main context and skip the second one.
233
226
  */
234
227
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
228
+ /**
229
+ * Whether the upsert's update assignments can bind straight into the statement's own context.
230
+ *
231
+ * They cannot on a `?`-placeholder dialect: the assignments are built before the insert but read
232
+ * after it, so their values have to be pushed afterwards to land in the right positional order.
233
+ * A `$n` placeholder carries its own index, so there is nothing to reorder - but it also cannot use
234
+ * the scratch context, whose numbering would restart at `$1` and collide with the insert's.
235
+ */
236
+ protected readonly upsertUpdateBindsInPlace: boolean;
235
237
  /** How an `ON CONFLICT` assignment reads the row that was being inserted. */
236
238
  protected readonly upsertExcluded: (columnName: string) => string;
237
239
  protected getUpsertUpdateAssignments<E>(ctx: QueryContext, meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], callback?: (columnName: string) => string): string;
@@ -251,6 +253,49 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
251
253
  * `isJsonType` and `isVectorFieldType` 120 times to get the same six answers.
252
254
  */
253
255
  protected persistKind(field: FieldOptions | undefined): PersistKind;
256
+ /**
257
+ * Which of an entity's columns need decoding on READ, and how: the inverse of {@link persistKind},
258
+ * cached per entity for the same reason it classifies per column. A 1000-row read of a 10-field
259
+ * entity otherwise asks `isJsonType` (which lowercases a string on every call) 10,000 times to get
260
+ * the same ten answers. Most entities land here for their numeric columns alone, where the per-row
261
+ * cost is one `typeof` against a value the driver usually decoded already.
262
+ *
263
+ * Dialect-aware exactly like {@link supportedVectorType}, because it has to be: a `sparsevec` field
264
+ * is written as a plain dense vector everywhere but Postgres, so reading it back by the field's own
265
+ * declared cast would look for a sparse literal that was never stored.
266
+ *
267
+ * A type lands here rather than at the driver when the wire type alone cannot decide it, and only
268
+ * the declaration can: `Boolean` is 0/1 in a SQLite INTEGER and a MySQL `TINYINT(1)`, both
269
+ * indistinguishable from a genuine small integer; a decimal is text from pg *and* mysql2, and only
270
+ * the field says it was meant as a number; and `type: BigInt` shares BIGINT with `type: Number`, so
271
+ * the wire decode has to be undone for it. All are no-ops where the driver already decoded.
272
+ *
273
+ * Classified through the same `isNumericType`/`isBooleanType`/`isJsonType` the rest of the library
274
+ * uses, not against the constructors: `type` accepts a string logical type for every one of these
275
+ * (`@Field({ type: 'decimal' })`), and matching `=== Number` alone left those reading back as text.
276
+ */
277
+ hydratableFields<E>(entity: Type<E>): readonly HydratableField[];
278
+ /**
279
+ * The same classification for an aggregate row. Not cached, because these columns are a shape of the
280
+ * query rather than of the entity, and it is computed once per call either way.
281
+ *
282
+ * Mirrors `QueryAggregateFnResult`, which is the contract callers already compile against:
283
+ * `$count`/`$sum`/`$avg` are a number whatever they aggregate, while `$min`/`$max` and every
284
+ * `$group` column keep the aggregated field's own type, so they decode as that field would. Without
285
+ * it a `$sum` over a BIGINT column came back as `'500'` from a result type that says `number`, since
286
+ * Postgres widens that sum to NUMERIC and no driver can know it was meant as a JS number.
287
+ */
288
+ hydratableAggregates<E, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>): readonly HydratableField[];
289
+ /**
290
+ * The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
291
+ *
292
+ * Ordered for correctness, not for speed - this runs once per entity, cached, never per row. The
293
+ * one order that is load-bearing is `BigInt` before {@link isNumericType}, which answers true for
294
+ * `BigInt` as well as `Number`: swap them and every `type: BigInt` property silently decodes to a
295
+ * JS number again.
296
+ */
297
+ protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
298
+ private readonly hydratable;
254
299
  /** The one type dispatch for a persisted value, over a column kind decided by the caller. */
255
300
  private writePersistableValue;
256
301
  protected appendJsonValue(ctx: QueryContext, value: unknown, _type: JsonColumnType): void;
@@ -348,8 +393,15 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
348
393
  * @param sizeExprFn - function that appends the size expression to ctx (e.g. `jsonb_array_length("col")`)
349
394
  */
350
395
  protected buildSizeComparison(ctx: QueryContext, sizeExprFn: () => void, sizeVal: number | QuerySizeComparisonOps): void;
396
+ /** The runtime half of {@link QuerySizeComparisonOps}: what a count can sensibly be compared with. */
397
+ private static readonly SIZE_COMPARE_OPS;
351
398
  /**
352
- * Append a single size comparison operator and value to the context.
399
+ * Append a single size comparison operator and value. No operand: the count expression is already
400
+ * in the context, so this contributes only the ` <op> <value>` tail.
401
+ *
402
+ * Gated on {@link SIZE_COMPARE_OPS} rather than on whatever the shared renderer accepts, because
403
+ * that renderer also knows `$like`, `$regex` and `$in`, none of which mean anything against a
404
+ * count. `$size: { $like: 5 }` has to stay the error it always was.
353
405
  */
354
406
  private appendSizeOp;
355
407
  /** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
@@ -371,4 +423,3 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
371
423
  protected numericCast(expr: string): string;
372
424
  toString(): string;
373
425
  }
374
- export {};