uql-orm 0.55.0 → 0.57.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 (109) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/bunSql/bunSql.util.js +1 -1
  5. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  6. package/dist/cockroachdb/cockroachDialect.js +2 -10
  7. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  8. package/dist/d1/d1SqliteDialect.js +2 -0
  9. package/dist/dialect/abstractSqlDialect.d.ts +195 -32
  10. package/dist/dialect/abstractSqlDialect.js +406 -199
  11. package/dist/dialect/aliases.d.ts +10 -7
  12. package/dist/dialect/aliases.js +12 -7
  13. package/dist/dialect/hydrateColumn.d.ts +8 -2
  14. package/dist/dialect/hydrateColumn.js +33 -1
  15. package/dist/dialect/jsonSql.d.ts +13 -5
  16. package/dist/dialect/jsonSql.js +24 -7
  17. package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
  18. package/dist/dialect/mysqlLikeSqlDialect.js +58 -7
  19. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  20. package/dist/dialect/pgLikeSqlDialect.js +25 -50
  21. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  22. package/dist/dialect/pgVectorMetrics.js +17 -0
  23. package/dist/dialect/queryContext.d.ts +3 -7
  24. package/dist/dialect/queryContext.js +13 -8
  25. package/dist/dialect/queryJoins.d.ts +8 -4
  26. package/dist/dialect/queryJoins.js +27 -15
  27. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  28. package/dist/dialect/vectorSqlDialect.js +2 -3
  29. package/dist/entity/index.d.ts +1 -1
  30. package/dist/entity/index.js +1 -1
  31. package/dist/entity/metadata/definition.d.ts +4 -2
  32. package/dist/entity/metadata/definition.js +27 -29
  33. package/dist/maria/mariaDialect.d.ts +13 -6
  34. package/dist/maria/mariaDialect.js +29 -9
  35. package/dist/migrate/builder/splitSqlStatements.js +2 -2
  36. package/dist/migrate/cli.d.ts +2 -3
  37. package/dist/migrate/cli.js +4 -11
  38. package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
  39. package/dist/migrate/ddl/index.d.ts +1 -5
  40. package/dist/migrate/ddl/index.js +14 -25
  41. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  42. package/dist/migrate/ddl/indexDdl.js +17 -1
  43. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  44. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  45. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  46. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  47. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -3
  48. package/dist/migrate/ddl/pgIndexDdl.js +29 -3
  49. package/dist/migrate/drift/driftDetector.js +21 -8
  50. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  51. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
  52. package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
  53. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  54. package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
  55. package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
  56. package/dist/migrate/migrator.d.ts +7 -4
  57. package/dist/migrate/migrator.js +9 -14
  58. package/dist/migrate/schemaGenerator.d.ts +3 -3
  59. package/dist/migrate/schemaGenerator.js +7 -13
  60. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  61. package/dist/mongo/mongoDialect.d.ts +31 -18
  62. package/dist/mongo/mongoDialect.js +147 -108
  63. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  64. package/dist/mongo/mongodbQuerier.js +34 -108
  65. package/dist/mssql/mssqlDialect.d.ts +16 -0
  66. package/dist/mssql/mssqlDialect.js +26 -4
  67. package/dist/mysql/mysqlDialect.d.ts +2 -0
  68. package/dist/mysql/mysqlDialect.js +4 -0
  69. package/dist/querier/abstractQuerier.d.ts +20 -36
  70. package/dist/querier/abstractQuerier.js +44 -143
  71. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  72. package/dist/querier/abstractSqlQuerier.d.ts +11 -22
  73. package/dist/querier/abstractSqlQuerier.js +49 -53
  74. package/dist/schema/canonicalType.js +4 -6
  75. package/dist/schema/dependencyGraph.js +2 -4
  76. package/dist/schema/indexDifferences.js +5 -5
  77. package/dist/schema/schemaASTBuilder.js +34 -12
  78. package/dist/schema/schemaASTDiffer.d.ts +10 -2
  79. package/dist/schema/schemaASTDiffer.js +17 -16
  80. package/dist/schema/types.d.ts +1 -1
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +40 -8
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +2 -2
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +2 -1
  88. package/dist/type/migratorDialect.d.ts +4 -0
  89. package/dist/type/querier.d.ts +6 -6
  90. package/dist/type/query.d.ts +25 -48
  91. package/dist/type/query.js +10 -5
  92. package/dist/type/queryAggregate.d.ts +10 -10
  93. package/dist/type/queryAggregate.js +1 -1
  94. package/dist/type/universalQuerier.d.ts +4 -4
  95. package/dist/util/dialect.util.d.ts +8 -2
  96. package/dist/util/dialect.util.js +19 -0
  97. package/dist/util/field.util.d.ts +5 -0
  98. package/dist/util/field.util.js +19 -0
  99. package/dist/util/logger.d.ts +10 -1
  100. package/dist/util/logger.js +18 -0
  101. package/dist/util/object.util.d.ts +4 -0
  102. package/dist/util/object.util.js +8 -0
  103. package/dist/util/relationQuery.util.d.ts +15 -68
  104. package/dist/util/relationQuery.util.js +35 -83
  105. package/dist/util/rowKey.util.d.ts +1 -11
  106. package/dist/util/rowKey.util.js +1 -13
  107. package/package.json +1 -1
  108. package/dist/querier/relationCount.d.ts +0 -16
  109. package/dist/querier/relationCount.js +0 -121
@@ -37,7 +37,7 @@ export type QueryAggregateOp = (typeof QUERY_AGGREGATE_OPS)[number];
37
37
  export declare function isQueryAggregateOp(op: string): op is QueryAggregateOp;
38
38
  /**
39
39
  * DISTINCT-qualified aggregate ops, each mapped to the base op it applies to a field's distinct
40
- * values: `$countDistinct` → `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
40
+ * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
41
41
  * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
42
42
  * are omitted: DISTINCT is a no-op for them.
43
43
  */
@@ -91,11 +91,11 @@ type ExactlyOne<T> = {
91
91
  * compile error). Only `$count` accepts `'*'` (i.e. `COUNT(*)`); every other op requires a field.
92
92
  * DISTINCT variants are flat ops (`$countDistinct`/`$sumDistinct`/`$avgDistinct`) taking a field.
93
93
  *
94
- * @example { $count: '*' } → COUNT(*)
95
- * @example { $countDistinct: 'id' } → COUNT(DISTINCT "id")
96
- * @example { $sum: 'amount' } → SUM("amount")
97
- * @example { $sumDistinct: 'amount' } → SUM(DISTINCT "amount")
98
- * @example { $avg: 'age' } → AVG("age")
94
+ * @example { $count: '*' } -> COUNT(*)
95
+ * @example { $countDistinct: 'id' } -> COUNT(DISTINCT "id")
96
+ * @example { $sum: 'amount' } -> SUM("amount")
97
+ * @example { $sumDistinct: 'amount' } -> SUM(DISTINCT "amount")
98
+ * @example { $avg: 'age' } -> AVG("age")
99
99
  */
100
100
  export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E>>;
101
101
  /** A single-key `{ [op]: unknown }` shape for each op in `Ops`, matched to infer that op's result. */
@@ -113,7 +113,7 @@ type CountingOp = OpsOf<'$count' | '$countDistinct'>;
113
113
  *
114
114
  * @example
115
115
  * ```ts
116
- * { status: true } // → GROUP BY "status"
116
+ * { status: true } // -> GROUP BY "status"
117
117
  * ```
118
118
  */
119
119
  export type QueryGroupMap<E> = {
@@ -127,7 +127,7 @@ export type QueryGroupMap<E> = {
127
127
  * @example
128
128
  * ```ts
129
129
  * { count: { $count: '*' }, avgAge: { $avg: 'age' } }
130
- * // → COUNT(*) AS "count", AVG("age") AS "avgAge"
130
+ * // -> COUNT(*) AS "count", AVG("age") AS "avgAge"
131
131
  * ```
132
132
  */
133
133
  export type QueryAggMap<E> = {
@@ -172,11 +172,11 @@ export type QueryAggregateResult<E, G, A> = Simplify<{
172
172
  -readonly [K in keyof A]: QueryAggregateFnResult<E, A[K]>;
173
173
  }>;
174
174
  /**
175
- * Erased runtime shape of a HAVING clause (alias → comparison), consumed by the dialect builders.
175
+ * Erased runtime shape of a HAVING clause (alias -> comparison), consumed by the dialect builders.
176
176
  * Values are `unknown` because the SQL is built generically; the typed, per-column value checking
177
177
  * lives in {@link QueryAggregate.$having}.
178
178
  *
179
- * @example { count: { $gt: 5 } } → HAVING COUNT(*) > 5
179
+ * @example { count: { $gt: 5 } } -> HAVING COUNT(*) > 5
180
180
  */
181
181
  export type QueryHavingMap = {
182
182
  readonly [alias: string]: QueryWhereFieldValue<unknown> | undefined;
@@ -8,7 +8,7 @@ export function isQueryAggregateOp(op) {
8
8
  }
9
9
  /**
10
10
  * DISTINCT-qualified aggregate ops, each mapped to the base op it applies to a field's distinct
11
- * values: `$countDistinct` → `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
11
+ * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
12
12
  * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
13
13
  * are omitted: DISTINCT is a no-op for them.
14
14
  */
@@ -1,5 +1,5 @@
1
1
  import type { EntityData, EntityId, FieldKey, RelationKey, UpdatePayload, WrittenId } from './entity.js';
2
- import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpsertOneResult, QueryUpsertManyResult } from './query.js';
2
+ import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
5
5
  import type { QuerierCountedResult, QuerierResult, QuerierTransport } from './wire.js';
@@ -97,14 +97,14 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
97
97
  */
98
98
  export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions> {
99
99
  /**
100
- * streams the records matching the given search parameters as an async iterable.
101
- * Does not fill relations or fire lifecycle hooks - designed for high-performance
100
+ * streams the records matching the given search parameters as an async iterable, each with the
101
+ * relations and counts `findMany` reads. Fires no lifecycle hooks, and holds one row at a time, for
102
102
  * bulk reads (ETL, exports, migrations).
103
103
  * @param entity the target entity
104
104
  * @param q the criteria options
105
105
  * @return an async iterable of records
106
106
  */
107
- findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
107
+ findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
108
108
  /**
109
109
  * Insert a single record and return its ID (provided, `onInsert`-generated, or
110
110
  * database-generated - see {@link UniversalQuerier.insertMany} for the exact semantics).
@@ -1,4 +1,4 @@
1
- import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
3
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
4
  /** Appends `record`'s not-yet-`seen` insertable keys (real, caller-written, defined value) to `keys`. */
@@ -128,7 +128,7 @@ export type ParsedGroupEntry = {
128
128
  readonly alias: string;
129
129
  readonly op: QueryAggregateOp;
130
130
  readonly fieldRef: string;
131
- /** `true` for a flat distinct op (`$countDistinct`, ...) → `COUNT(DISTINCT field)`. */
131
+ /** `true` for a flat distinct op (`$countDistinct`, ...) -> `COUNT(DISTINCT field)`. */
132
132
  readonly distinct: boolean;
133
133
  };
134
134
  /**
@@ -171,3 +171,9 @@ export declare function assertNonNegativeInteger(value: number, clause: string):
171
171
  export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
172
172
  /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
173
173
  export declare function assertAggregateColumns(clauseMap: object, emitted: ReadonlySet<string>, clause: string): void;
174
+ /**
175
+ * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
176
+ * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
177
+ * where neither says, rather than guessed: every engine answers a guess with an error of its own.
178
+ */
179
+ export declare function textSearchFields<E>(meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): readonly string[];
@@ -424,3 +424,22 @@ export function assertAggregateColumns(clauseMap, emitted, clause) {
424
424
  }
425
425
  }
426
426
  }
427
+ /**
428
+ * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
429
+ * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
430
+ * where neither says, rather than guessed: every engine answers a guess with an error of its own.
431
+ */
432
+ export function textSearchFields(meta, search) {
433
+ if (search.$fields?.length) {
434
+ return search.$fields;
435
+ }
436
+ const fulltext = (meta.indexes ?? []).filter((index) => index.type === 'fulltext');
437
+ if (fulltext.length === 1) {
438
+ return fulltext[0].columns.map((entry) => entry.column);
439
+ }
440
+ const name = entityName(meta);
441
+ const declared = fulltext.length
442
+ ? `${fulltext.length} fulltext indexes to choose from`
443
+ : 'no fulltext index to search';
444
+ throw new TypeError(`$text on '${name}' names no $fields, and '${name}' declares ${declared}. Name them with $fields.`);
445
+ }
@@ -22,6 +22,11 @@ export declare const COLUMN_TYPES_BY_FAMILY: {
22
22
  };
23
23
  /** The family of a logical field type, or `undefined` where it names none. */
24
24
  export declare function columnFamily(type: unknown): ColumnFamily | undefined;
25
+ /**
26
+ * Whether a field's column holds whole numbers: a declared integer type, or a `Number` or `BigInt`
27
+ * with no scale, which every engine here stores as BIGINT.
28
+ */
29
+ export declare function isIntegerColumn(field: Pick<FieldOptions, 'type' | 'columnType' | 'precision' | 'scale'>): boolean;
25
30
  /**
26
31
  * Whether the field's expression is spliced into each statement that reads it, rather than stored.
27
32
  * Every read site asks this - the DDL skip, the projection, the `$where` and `ORDER BY` operands -
@@ -46,6 +46,25 @@ for (const family of getKeys(COLUMN_TYPES_BY_FAMILY)) {
46
46
  export function columnFamily(type) {
47
47
  return FAMILY_OF.get(typeof type === 'string' ? type.toLowerCase() : type);
48
48
  }
49
+ /** The numeric column types that hold whole numbers. */
50
+ const INTEGER_COLUMN_TYPES = new Set([
51
+ 'int',
52
+ 'integer',
53
+ 'tinyint',
54
+ 'smallint',
55
+ 'bigint',
56
+ ]);
57
+ /**
58
+ * Whether a field's column holds whole numbers: a declared integer type, or a `Number` or `BigInt`
59
+ * with no scale, which every engine here stores as BIGINT.
60
+ */
61
+ export function isIntegerColumn(field) {
62
+ const type = field.columnType ?? field.type;
63
+ if (typeof type === 'string') {
64
+ return INTEGER_COLUMN_TYPES.has(type.toLowerCase());
65
+ }
66
+ return type === BigInt || (type === Number && !field.precision && !field.scale);
67
+ }
49
68
  /**
50
69
  * Whether the field's expression is spliced into each statement that reads it, rather than stored.
51
70
  * Every read site asks this - the DDL skip, the projection, the `$where` and `ORDER BY` operands -
@@ -34,7 +34,7 @@ export declare class LoggerWrapper implements Logger {
34
34
  private readonly loggerFunction?;
35
35
  private readonly logValues;
36
36
  private readonly slowQuery?;
37
- constructor(options: LoggingOptions, config?: LoggerWrapperConfig);
37
+ constructor(options?: LoggingOptions, config?: LoggerWrapperConfig);
38
38
  /** Whether `logQuery` would ever actually surface bound values, given the configured levels/slowQuery/logValues. */
39
39
  willLogValues(): boolean;
40
40
  logQuery(query: string, values?: unknown[], duration?: number): void;
@@ -46,6 +46,15 @@ export declare class LoggerWrapper implements Logger {
46
46
  logSkippedMigration(message: string): void;
47
47
  private log;
48
48
  }
49
+ /**
50
+ * The wrapper a querier logs through, one per options object and read once: a pool builds a querier
51
+ * for every statement, and all of them log by the same options.
52
+ */
53
+ export declare function queryLoggerFor(extra?: {
54
+ readonly logger?: LoggingOptions;
55
+ readonly logValues?: boolean;
56
+ readonly slowQuery?: number;
57
+ }): LoggerWrapper;
49
58
  /**
50
59
  * Structural type for any EventEmitter-like connection pool that emits an
51
60
  * `'error'` event on a dropped connection (node-postgres, `mariadb`, etc.).
@@ -136,6 +136,24 @@ export class LoggerWrapper {
136
136
  }
137
137
  }
138
138
  }
139
+ const wrappersByOptions = new WeakMap();
140
+ let unconfigured;
141
+ /**
142
+ * The wrapper a querier logs through, one per options object and read once: a pool builds a querier
143
+ * for every statement, and all of them log by the same options.
144
+ */
145
+ export function queryLoggerFor(extra) {
146
+ if (!extra) {
147
+ unconfigured ??= new LoggerWrapper();
148
+ return unconfigured;
149
+ }
150
+ let wrapper = wrappersByOptions.get(extra);
151
+ if (!wrapper) {
152
+ wrapper = new LoggerWrapper(extra.logger, { logValues: extra.logValues, slowQuery: extra.slowQuery });
153
+ wrappersByOptions.set(extra, wrapper);
154
+ }
155
+ return wrapper;
156
+ }
139
157
  /**
140
158
  * Attaches an error listener to a connection pool so a dropped connection is logged instead of left
141
159
  * unhandled - which crashes the process for drivers that don't guard against it themselves
@@ -19,7 +19,11 @@ export declare function someValue(obj: object, pred: (value: unknown) => boolean
19
19
  export declare function isOperatorObject(value: unknown): value is Record<string, unknown>;
20
20
  /** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
21
21
  export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
22
+ /** Whether `value` is an object that is not an array, whose keys can be read. */
23
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
22
24
  export declare function getKeys<T extends object>(obj: T): (keyof T & string)[];
25
+ /** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */
26
+ export declare function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][];
23
27
  /**
24
28
  * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
25
29
  * the fallback is what an entity that named no table is called - which is why the sites spelling this
@@ -49,9 +49,17 @@ export function isOperatorObject(value) {
49
49
  export function isOperatorOnlyObject(value) {
50
50
  return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));
51
51
  }
52
+ /** Whether `value` is an object that is not an array, whose keys can be read. */
53
+ export function isRecord(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
52
56
  export function getKeys(obj) {
53
57
  return obj ? Object.keys(obj) : [];
54
58
  }
59
+ /** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */
60
+ export function definedEntries(record) {
61
+ return Object.entries(record).filter((entry) => entry[1] !== undefined);
62
+ }
55
63
  /**
56
64
  * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
57
65
  * the fallback is what an entity that named no table is called - which is why the sites spelling this
@@ -1,8 +1,8 @@
1
- import type { EntityMeta, FieldMeta, Except, Query, QueryPopulate, RelationKey, RelationMeta } from '../type/index.js';
1
+ import type { EntityMeta, QueryCount, QueryPopulate, RelationKey, RelationMeta, RelationQuery, QueryWhere } from '../type/index.js';
2
2
  export type RelationRequestSummary<E> = {
3
- readonly requestedKeys: RelationKey<E>[];
4
- readonly joinableKeys: RelationKey<E>[];
5
- readonly toManyKeys: RelationKey<E>[];
3
+ readonly requestedKeys: readonly RelationKey<E>[];
4
+ readonly joinableKeys: readonly RelationKey<E>[];
5
+ readonly toManyKeys: readonly RelationKey<E>[];
6
6
  };
7
7
  /**
8
8
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
@@ -32,67 +32,10 @@ export declare function parentJoins(relOpts: Pick<RelationMeta, 'references' | '
32
32
  * which is the mistake this module exists to prevent.
33
33
  */
34
34
  export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references'>, parentKeyCount: number): string[];
35
- /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
36
- export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
37
- /**
38
- * One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
39
- * child or tally row carries that key in. Matching the two halves means agreeing on every column, so
40
- * each side is read through `joins` rather than through the parent's own key list - the same set only
41
- * for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
42
- * sides from being spelled apart.
43
- *
44
- * Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
45
- * each row itself, so a page of children costs one key each and nothing else.
46
- */
47
- export declare function keyColumns(joins: readonly ParentJoin[], side: keyof ParentJoin): string[];
48
- /**
49
- * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
50
- * children in one statement.
51
- *
52
- * A composite over-selects, because the lists are independent and a pairing no parent has can still
53
- * match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
54
- * cheaper than the row-value comparison no engine spells the same way.
55
- */
56
- export declare function parentsIn(joins: readonly ParentJoin[], parents: readonly unknown[]): Record<string, unknown[]>;
57
- /**
58
- * The parents a bounded to-many read fans out over: the rows themselves, the columns matching them to
59
- * their children.
60
- */
61
- export type ParentPartition = {
62
- readonly joins: readonly ParentJoin[];
63
- readonly parents: readonly unknown[];
64
- /** The parent's own fields: a `LATERAL` row source has to spell its key column's type. */
65
- readonly parentFields: Readonly<Record<string, FieldMeta | undefined>>;
66
- };
67
- /**
68
- * Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
69
- * Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
70
- * correct and cheaper.
71
- */
72
- export declare function isBoundedPerParent(query: Pick<RelationQuery, '$limit' | '$skip'>): boolean;
73
- /**
74
- * `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
75
- * for. Shared by the backends so how the parent's filter merges into the relation's own is decided
76
- * once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
77
- */
78
- export declare function queryChildrenOf<E>(query: Query<E>, joins: readonly ParentJoin[], parent: unknown): Query<E>;
79
- /**
80
- * `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
81
- * no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
82
- */
83
- export declare function queryChildrenOfAll<E>(query: Query<E>, joins: readonly ParentJoin[], parents: readonly unknown[]): Query<E>;
84
- /**
85
- * `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
86
- * the parents it is being read for, whether the filter names their keys as values or, for a correlated
87
- * shape, as a reference to a row source.
88
- */
89
- export declare function queryNarrowedTo<E>(query: Query<E>, filter: Record<string, unknown>): Query<E>;
90
35
  /**
91
36
  * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
92
- * column a single key contributes, an OR of key maps for several.
93
- *
94
- * Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
95
- * has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
37
+ * column a single key contributes, an OR of whole key maps for several - lists of each column apart
38
+ * would pair values no parent has, and a delete would take a child of a parent that survives.
96
39
  */
97
40
  export declare function childrenOf(joins: readonly ParentJoin[], parentIds: readonly unknown[]): Record<string, unknown>;
98
41
  /**
@@ -107,11 +50,15 @@ export type JoinedRelationRejectedKey = (typeof JOINED_RELATION_REJECTIONS)[numb
107
50
  export declare function getRelationRequestSummary<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): RelationRequestSummary<E>;
108
51
  /** True when `$populate` includes at least one relation key. */
109
52
  export declare function populatesRelations<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): boolean;
110
- export type RelationQuery<E extends object = object> = Except<Query<E>, StatementOnlyClause> & {
111
- $required?: boolean;
112
- };
113
- /** The clauses that describe the statement rather than what a query selects. */
114
- type StatementOnlyClause = '$lock' | '$candidates';
53
+ /**
54
+ * Each relation a `$count` tallies, and the filter narrowing what it counts: the target's, whose type
55
+ * only the metadata knows this far down, as for a relation filter reaching the same subquery.
56
+ */
57
+ export declare function countedRelations<E>(meta: EntityMeta<E>, counts: QueryCount<E> | undefined): {
58
+ readonly relKey: RelationKey<E>;
59
+ readonly relation: RelationMeta;
60
+ readonly where: QueryWhere<unknown>;
61
+ }[];
115
62
  export type ParsedRelationQuery<E extends object = object> = {
116
63
  query: RelationQuery<E>;
117
64
  required: boolean;
@@ -1,5 +1,11 @@
1
- import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
2
- import { getKeys, someKey } from './object.util.js';
1
+ import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_STATEMENT_CLAUSES, } from '../type/query.js';
2
+ import { getKeys, isRecord, someKey } from './object.util.js';
3
+ /** What a query populating nothing requests, shared: most reads populate nothing, and ask on every one. */
4
+ const NOTHING_REQUESTED = Object.freeze({
5
+ requestedKeys: Object.freeze([]),
6
+ joinableKeys: Object.freeze([]),
7
+ toManyKeys: Object.freeze([]),
8
+ });
3
9
  /**
4
10
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
5
11
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
@@ -34,86 +40,19 @@ export function parentJoins(relOpts, parentKeyCount) {
34
40
  export function targetKeyColumns(relOpts, parentKeyCount) {
35
41
  return relOpts.references.slice(parentKeyCount).map(({ local }) => local);
36
42
  }
37
- /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
38
- export function joinedColumns(joins) {
39
- return Object.fromEntries(keyColumns(joins, 'joined').map((column) => [column, true]));
40
- }
41
- /**
42
- * One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
43
- * child or tally row carries that key in. Matching the two halves means agreeing on every column, so
44
- * each side is read through `joins` rather than through the parent's own key list - the same set only
45
- * for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
46
- * sides from being spelled apart.
47
- *
48
- * Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
49
- * each row itself, so a page of children costs one key each and nothing else.
50
- */
51
- export function keyColumns(joins, side) {
52
- return joins.map((join) => join[side]);
53
- }
54
- /**
55
- * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
56
- * children in one statement.
57
- *
58
- * A composite over-selects, because the lists are independent and a pairing no parent has can still
59
- * match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
60
- * cheaper than the row-value comparison no engine spells the same way.
61
- */
62
- export function parentsIn(joins, parents) {
63
- return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => read(it, parent))]));
64
- }
65
- /**
66
- * Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
67
- * Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
68
- * correct and cheaper.
69
- */
70
- export function isBoundedPerParent(query) {
71
- return query.$limit !== undefined || query.$skip !== undefined;
72
- }
73
- /**
74
- * The `$where` naming exactly one parent's children: every joined column equal to that parent's value.
75
- * What a per-parent bounded read filters each of its branches by, and the composite half of
76
- * {@link childrenOf}.
77
- */
78
- function childOf(joins, parent) {
79
- return Object.fromEntries(joins.map(({ parent: key, joined }) => [joined, read(parent, key)]));
80
- }
81
- /**
82
- * `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
83
- * for. Shared by the backends so how the parent's filter merges into the relation's own is decided
84
- * once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
85
- */
86
- export function queryChildrenOf(query, joins, parent) {
87
- return queryNarrowedTo(query, childOf(joins, parent));
88
- }
89
- /**
90
- * `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
91
- * no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
92
- */
93
- export function queryChildrenOfAll(query, joins, parents) {
94
- return queryNarrowedTo(query, parentsIn(joins, parents));
95
- }
96
- /**
97
- * `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
98
- * the parents it is being read for, whether the filter names their keys as values or, for a correlated
99
- * shape, as a reference to a row source.
100
- */
101
- export function queryNarrowedTo(query, filter) {
102
- return { ...query, $where: { ...query.$where, ...filter } };
103
- }
104
43
  /**
105
44
  * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
106
- * column a single key contributes, an OR of key maps for several.
107
- *
108
- * Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
109
- * has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
45
+ * column a single key contributes, an OR of whole key maps for several - lists of each column apart
46
+ * would pair values no parent has, and a delete would take a child of a parent that survives.
110
47
  */
111
48
  export function childrenOf(joins, parentIds) {
112
49
  const [first] = joins;
113
50
  if (joins.length === 1) {
114
51
  return { [first.joined]: parentIds };
115
52
  }
116
- return { $or: parentIds.map((id) => childOf(joins, id)) };
53
+ return {
54
+ $or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, read(id, parent)]))),
55
+ };
117
56
  }
118
57
  function read(row, key) {
119
58
  return row[key];
@@ -142,11 +81,11 @@ function assertJoinableRelationQuery(relKey, value) {
142
81
  }
143
82
  }
144
83
  export function getRelationRequestSummary(meta, populate) {
84
+ if (!populate)
85
+ return NOTHING_REQUESTED;
145
86
  const requestedKeys = [];
146
87
  const joinableKeys = [];
147
88
  const toManyKeys = [];
148
- if (!populate)
149
- return { requestedKeys, joinableKeys, toManyKeys };
150
89
  for (const key of getKeys(populate)) {
151
90
  if (!populate[key])
152
91
  continue;
@@ -172,8 +111,24 @@ export function populatesRelations(meta, populate) {
172
111
  return false;
173
112
  return someKey(populate, (key) => !!populate[key] && key in meta.relations);
174
113
  }
175
- /** Their runtime half, so the check below cannot drift from the type above. */
176
- const STATEMENT_ONLY_CLAUSES = ['$lock', ...QUERY_ROOT_NUMBER_CLAUSES];
114
+ /**
115
+ * Each relation a `$count` tallies, and the filter narrowing what it counts: the target's, whose type
116
+ * only the metadata knows this far down, as for a relation filter reaching the same subquery.
117
+ */
118
+ export function countedRelations(meta, counts) {
119
+ if (!counts) {
120
+ return [];
121
+ }
122
+ return getKeys(counts).flatMap((relKey) => {
123
+ const count = counts[relKey];
124
+ const relation = meta.relations[relKey];
125
+ if (!count || !relation) {
126
+ return [];
127
+ }
128
+ const where = typeof count === 'object' ? count.$where : undefined;
129
+ return [{ relKey, relation, where: where ?? {} }];
130
+ });
131
+ }
177
132
  // Taken from the clause groups declared beside `Query` itself, so a renamed clause fails to compile
178
133
  // here instead of quietly narrowing what a relation query accepts. `$required` is the one key that
179
134
  // is not a `Query` clause at all - it says how the relation joins, not what it selects.
@@ -192,7 +147,7 @@ export function parseRelationQueryValue(value) {
192
147
  // Caught before the shape check so the message names the key, rather than reporting the whole
193
148
  // object as an unrecognized relation query value.
194
149
  if (isRecord(value)) {
195
- const statementOnly = STATEMENT_ONLY_CLAUSES.find((clause) => clause in value);
150
+ const statementOnly = QUERY_STATEMENT_CLAUSES.find((clause) => clause in value);
196
151
  if (statementOnly) {
197
152
  throw new TypeError(`'${statementOnly}' applies to the whole statement, not to a populated relation. Move it to the top level of the query.`);
198
153
  }
@@ -221,9 +176,6 @@ export function forEachRequestedRelation(meta, populate, fn) {
221
176
  fn(relKey, populate?.[relKey]);
222
177
  }
223
178
  }
224
- function isRecord(value) {
225
- return value !== null && typeof value === 'object' && !Array.isArray(value);
226
- }
227
179
  function isBooleanLikeValue(value) {
228
180
  return value === true || value === false || value === 0 || value === 1;
229
181
  }
@@ -237,7 +189,7 @@ function isValidRelationQueryShape(query) {
237
189
  if (RELATION_QUERY_BOOLEAN_KEYS.has(key) && !isBooleanLikeValue(value)) {
238
190
  return false;
239
191
  }
240
- if (RELATION_QUERY_OBJECT_KEYS.has(key) && !isRecord(value)) {
192
+ if (RELATION_QUERY_OBJECT_KEYS.has(key) && !isRecord(value) && !(key === '$select' && Array.isArray(value))) {
241
193
  return false;
242
194
  }
243
195
  if (RELATION_QUERY_NUMBER_KEYS.has(key) && (typeof value !== 'number' || !Number.isFinite(value))) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A row's key as a string, for matching rows to each other in a {@link dataKeyed} lookup.
2
+ * A row's key as a string, for matching rows to each other.
3
3
  *
4
4
  * Reads the columns off the row rather than taking their values, because every caller matches a
5
5
  * whole page of rows against one fixed column list: taking an array would make each of them build
@@ -11,13 +11,3 @@
11
11
  * key are not treated as one row.
12
12
  */
13
13
  export declare function rowKey(row: unknown, columns: readonly string[]): string;
14
- /**
15
- * A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
16
- * `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
17
- * pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
18
- * here, and faster than `{}`, which walks the prototype chain on every miss.
19
- *
20
- * It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
21
- * `hasOwnProperty` on one type-checks and throws.
22
- */
23
- export declare function dataKeyed<V>(): Record<string, V>;
@@ -1,7 +1,7 @@
1
1
  /** Separates the parts of a composite key: a unit separator, which no column value carries. */
2
2
  const KEY_SEPARATOR = '\u001f';
3
3
  /**
4
- * A row's key as a string, for matching rows to each other in a {@link dataKeyed} lookup.
4
+ * A row's key as a string, for matching rows to each other.
5
5
  *
6
6
  * Reads the columns off the row rather than taking their values, because every caller matches a
7
7
  * whole page of rows against one fixed column list: taking an array would make each of them build
@@ -23,18 +23,6 @@ export function rowKey(row, columns) {
23
23
  }
24
24
  return key;
25
25
  }
26
- /**
27
- * A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
28
- * `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
29
- * pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
30
- * here, and faster than `{}`, which walks the prototype chain on every miss.
31
- *
32
- * It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
33
- * `hasOwnProperty` on one type-checks and throws.
34
- */
35
- export function dataKeyed() {
36
- return Object.create(null);
37
- }
38
26
  function keyPart(value) {
39
27
  if (value instanceof Date) {
40
28
  return value.toISOString();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
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.55.0",
6
+ "version": "0.57.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -1,16 +0,0 @@
1
- import type { Querier, Query, QueryCount, Type } from '../type/index.js';
2
- /** What counting asks of a querier, rather than the whole interface: two reads, both batched. */
3
- type CountingQuerier = Pick<Querier, 'aggregate' | 'findMany'>;
4
- /**
5
- * A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
6
- * same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
7
- * `$exclude` loses it; the raw-array `$select` form has nothing to augment, so it is refused.
8
- */
9
- export declare function withIdForCounts<E extends object>(entity: Type<E>, q: Query<E>): Query<E>;
10
- /**
11
- * How many rows each counted relation holds, under `_count` on every parent. One grouped aggregate
12
- * per relation over every parent at once - the batching a populated to-many already gets - so the
13
- * cost stays flat in the number of rows the read returned rather than one statement per row.
14
- */
15
- export declare function fillRelationCounts<E>(querier: CountingQuerier, entity: Type<E>, payload: E[], count?: QueryCount<E>): Promise<void>;
16
- export {};