uql-orm 0.74.0 → 0.75.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.
@@ -1,4 +1,4 @@
1
- var p=[];function y(e){for(let r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,1)}}var S=["$select","$populate","$exclude","$where","$sort"],E=["$count"],T=["$skip","$limit"],f=["$candidates"],O=["$distinct"],v=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...S,...E,...T,...f,...O,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function k(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let P=o,l={message:P?.error?.message??n.statusText,code:P?.error?.code??n.status};throw y({phase:"error",error:l,opts:t}),new K(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},V=c(s),re=new Map(V.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return k(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function F(){return q}function pe(){return F().getQuerier()}export{m as HttpQuerier,K as RequestError,R as get,pe as getQuerier,F as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,k as query,g as remove,de as setQuerierPool};
1
+ var p=[];function y(e){for(let r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,1)}}var P=["$select","$populate","$exclude","$where","$sort"],E=["$count"],T=["$skip","$limit"],f=["$candidates"],O=["$distinct"],v=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...P,...E,...T,...f,...O,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function k(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let S=o,l={message:S?.error?.message??n.statusText,code:S?.error?.code??n.status};throw y({phase:"error",error:l,opts:t}),new K(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},F=c(s),re=new Map(F.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return k(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function V(){return q}function pe(){return V().getQuerier()}export{m as HttpQuerier,K as RequestError,R as get,pe as getQuerier,V as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,k as query,g as remove,de as setQuerierPool};
2
2
 
3
- //# debugId=5397AE2C0FC782D564756E2164756E21
3
+ //# debugId=3410F618088BC65664756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -3,7 +3,7 @@
3
3
  "sources": ["../../src/browser/http/bus.ts", "../../src/type/query.ts", "../../src/type/queryRaw.ts", "../../src/util/object.util.ts", "../../src/http/query.ts", "../../src/browser/http/http.ts", "../../src/util/string.util.ts", "../../src/http/contract.ts", "../../src/browser/querier/httpQuerier.ts", "../../src/browser/options.ts"],
4
4
  "sourcesContent": [
5
5
  "import type { RequestCallback, RequestNotification } from '../type/index.js';\n\nconst subscriptors: RequestCallback[] = [];\n\nexport function notify(notification: RequestNotification): void {\n for (const subscriptor of subscriptors) {\n subscriptor(notification);\n }\n}\n\nexport function on(cb: RequestCallback): () => void {\n subscriptors.push(cb);\n const index = subscriptors.length - 1;\n return (): void => {\n subscriptors.splice(index, 1);\n };\n}\n",
6
- "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorQuery, QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterWhere<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\n);\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/** The fields of `E` a vector search can rank by. */\ntype VectorFieldKey<E> = { [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never }[FieldKey<E>];\n\n/**\n * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the\n * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never\n * where the target has no vector, since an empty map would admit any value at all.\n */\nexport type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never]\n ? never\n : { [P in VectorFieldKey<E>]?: QueryVectorQuery };\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector\n * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`\n * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once\n * per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its size or nearest row.\n IsMany<E[P]> extends true\n ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>>\n : QuerySortMap<RelationTarget<E[P]>, false>\n : NonNullable<E[P]> extends readonly number[]\n ? Root extends true\n ? QuerySortValue\n : QuerySortDirection | QueryVectorQuery\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Root extends true ? QuerySortByText : unknown);\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
6
+ "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorQuery, QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterWhere<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\n);\n\n/**\n * direction for the sort, and where nulls land in it.\n *\n * Unqualified, each engine has its own answer - Postgres and CockroachDB sort nulls last on `asc`, the\n * rest sort them first - so a placement is the only portable one. Engines with no `NULLS FIRST/LAST`\n * emulate it with a leading term, which no index can serve, which is why it is asked for and never\n * applied by default.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc' | QuerySortNullsDirection;\n\n/** A {@link QuerySortDirection} stating where nulls land. */\nexport type QuerySortNullsDirection = 'ascNullsFirst' | 'ascNullsLast' | 'descNullsFirst' | 'descNullsLast';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/** The fields of `E` a vector search can rank by. */\ntype VectorFieldKey<E> = { [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never }[FieldKey<E>];\n\n/**\n * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the\n * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never\n * where the target has no vector, since an empty map would admit any value at all.\n */\nexport type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never]\n ? never\n : { [P in VectorFieldKey<E>]?: QueryVectorQuery };\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector\n * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`\n * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once\n * per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its size or nearest row.\n IsMany<E[P]> extends true\n ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>>\n : QuerySortMap<RelationTarget<E[P]>, false>\n : NonNullable<E[P]> extends readonly number[]\n ? Root extends true\n ? QuerySortValue\n : QuerySortDirection | QueryVectorQuery\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Root extends true ? QuerySortByText : unknown);\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
7
7
  "import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';\nimport type { Type } from './utility.js';\n\n/** What a `raw` callback receives. See {@link QueryRawFn}. */\nexport type QueryRawRenderOptions = {\n /** The dialect rendering the SQL. */\n dialect: SqlQueryDialect;\n /** The alias of the table in scope, unescaped; empty where there is none. */\n prefix: string;\n /** {@link prefix} escaped, with its trailing dot. */\n escapedPrefix: string;\n /** The query context the SQL is written into. */\n ctx: QueryContext;\n /**\n * The entity being rendered, which a ref read off a definition's map resolves its column against: a\n * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.\n */\n entity?: Type<unknown>;\n};\n\n/** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */\nexport type QueryRawFnOptions = Partial<QueryRawRenderOptions>;\n\n/**\n * A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else\n * it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the\n * latter rejected `({ ctx }) => ctx.append(...)`, the form every computed field is written in, because\n * TypeScript's \"returning a value where void is expected\" allowance does not apply to a union.\n */\nexport type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;\n\nexport const RAW_VALUE: unique symbol = Symbol('rawValue');\nexport const RAW_ALIAS: unique symbol = Symbol('rawAlias');\n\nexport class QueryRaw {\n readonly [RAW_VALUE]: QueryRawFn;\n readonly [RAW_ALIAS]?: string;\n\n constructor(value: QueryRawFn, alias?: string) {\n this[RAW_VALUE] = value;\n this[RAW_ALIAS] = alias;\n }\n\n /** The same expression under an alias, for a `$select` projection. */\n as(alias: string): QueryRaw {\n return new QueryRaw(this[RAW_VALUE], alias);\n }\n\n /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */\n render(opts: QueryRawRenderOptions): void {\n const emitted = this[RAW_VALUE](opts);\n if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {\n opts.ctx.append(String(emitted));\n }\n }\n}\n\n/**\n * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it\n * renders as the field's column. Its `key` is how an index tells a column from an expression.\n */\nexport class ColumnRef<K extends string = string> extends QueryRaw {\n constructor(\n readonly key: K,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n\n/**\n * A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.\n * It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and\n * sorted like any other.\n *\n * `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom\n * fields so the aggregate a field declares decides the property's type and refuses `stored: true` on\n * one no delta can maintain.\n */\nexport class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {\n declare private readonly __value: V;\n declare private readonly __storable: Storable;\n\n constructor(\n /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */\n readonly spec: RelationAggregateSpec,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n",
8
8
  "import type { EntityMeta } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether `key` names an operator (`$eq`, `$push`...) rather than a field. */\nexport function isOperatorKey(key: string): boolean {\n return key.startsWith('$');\n}\n\n/** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether `value` is an object that is not an array, whose keys can be read. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nexport function getKeys<T extends object>(obj: T | null | undefined): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\n/** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */\nexport function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][] {\n return (Object.entries(record) as [K, V | undefined][]).filter((entry): entry is [K, V] => entry[1] !== undefined);\n}\n\n/**\n * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so\n * the fallback is what an entity that named no table is called - which is why the sites spelling this\n * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\n/**\n * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the\n * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which\n * is what a `$where` map and a composite key's id object both are; an array is a list of either.\n */\nexport function isScalarId(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) {\n return true;\n }\n if (Array.isArray(value)) {\n return false;\n }\n // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser\n // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column\n // with a map of several.\n const proto = Object.getPrototypeOf(value);\n return proto !== Object.prototype && proto !== null;\n}\n\n/** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */\nexport function isWhereMap(value: unknown): value is Record<string, unknown> {\n return !Array.isArray(value) && !isScalarId(value);\n}\n",
9
9
  "import type { QueryOptions, WireQuery } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.js';\n// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`\n// with it, in the browser bundle, which is on a size budget\nimport { RAW_VALUE } from '../type/queryRaw.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys, isWhereMap } from '../util/object.util.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof WireQuery<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof WireQuery<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): WireQuery<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n if (!isWhereMap(query['$where'])) {\n throw Object.assign(new TypeError(\"'$where' must be a JSON object\"), { status: 400 });\n }\n\n // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\n }\n\n return query as WireQuery<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? wireJson(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server\n * build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not\n * have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it\n * serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,\n * hits where the client's types already refuse a fragment.\n */\nexport function wireJson(value: unknown): string {\n return JSON.stringify(value, (_key: string, held: unknown) => {\n if (typeof held !== 'object' || held === null) {\n return held;\n }\n if (RAW_VALUE in held) {\n throw new TypeError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');\n }\n // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.\n if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {\n throw new TypeError('binary cannot travel over HTTP: what leaves the browser is JSON');\n }\n return held;\n });\n}\n",
@@ -13,7 +13,7 @@
13
13
  "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityWrite,\n EntityId,\n FieldKey,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdateWrite,\n WireQuery,\n WrittenId,\n} from '../../type/index.js';\nimport { isScalarId } from '../../util/object.util.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n /**\n * The URL segment an entity is addressed by, defaulting to its kebab-cased class name - the same\n * option the server handler takes, so one map serves both. State it where the default cannot: a\n * build that minifies class names renames every route.\n */\n readonly entityPath?: (entity: Type<unknown>) => string;\n};\n\n/** The id as one path segment, refusing a composite key, which has no spelling in `/:id` yet. Callers are `async`. */\nfunction idSegment<E>(entity: Type<E>, id: EntityId<E>): string {\n if (!isScalarId(id)) {\n throw new TypeError(`'${entity.name}' was addressed by an id object, which the HTTP route cannot carry.`);\n }\n return String(id);\n}\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n async findOneById<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n id: EntityId<E>,\n q?: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${basePath}/${idSegment(entity, id)}${qs}`,\n this.buildOptions(opts),\n );\n }\n\n findOne<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n return this.read<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`,\n q,\n opts,\n );\n }\n\n findMany<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: WireQuery<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<QueryFindResult<E, S, V, X, P, C>[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QueryPage<E, never>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n /** The `count` route capped at one row, so existence needs no endpoint of its own. */\n async exists<E extends object>(entity: Type<E>, q?: QueryFilter<E, never>, opts?: RequestOptions) {\n const res = await this.count(entity, { ...q, $limit: 1 }, opts);\n return { ...res, data: res.data > 0 };\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.insertMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async updateOneById<E extends object>(\n entity: Type<E>,\n id: EntityId<E>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${idSegment(entity, id)}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(\n entity: Type<E>,\n q: QuerySearch<E, never>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.saveMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${idSegment(entity, id)}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E, never>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${(this.defaults.entityPath ?? entityPath)(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
14
14
  "import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
15
15
  ],
16
- "mappings": "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",
17
- "debugId": "5397AE2C0FC782D564756E2164756E21",
16
+ "mappings": "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",
17
+ "debugId": "3410F618088BC65664756E2164756E21",
18
18
  "names": []
19
19
  }
@@ -16,10 +16,14 @@ type InsertShape<E> = {
16
16
  };
17
17
  /** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
18
18
  type HydratableField = readonly [string, HydrateKind];
19
+ /** A direction as a statement writes it: the suffix, and where the caller asked nulls to land. */
20
+ type SortOrder = {
21
+ readonly direction?: string;
22
+ readonly nulls?: 'first' | 'last';
23
+ };
19
24
  /** A sort term of a relation's rows as their aggregate orders by it: the column carrying it out. */
20
- export type SortRef = {
25
+ export type SortRef = SortOrder & {
21
26
  readonly ref: string;
22
- readonly direction: string;
23
27
  };
24
28
  /**
25
29
  * One column of a read's projection: the key its row answers under, none for a raw expression written
@@ -443,6 +447,12 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
443
447
  protected having(ctx: QueryContext, having: QueryHavingMap, emittedColumns: Record<string, string>): void;
444
448
  private static readonly SORT_DIRECTION_MAP;
445
449
  private resolveSortDirection;
450
+ /**
451
+ * One `ORDER BY` term. A placement the engine has no `NULLS FIRST/LAST` for becomes a term of its
452
+ * own in front of it, which is why one is only ever emitted where the caller asked for it: no index
453
+ * serves an expression. SQL Server needs a `CASE`, having no orderable boolean.
454
+ */
455
+ protected orderByTerm(expr: string, { direction, nulls }: SortOrder): string;
446
456
  /** Every operator of one `HAVING` condition, `AND`-joined. */
447
457
  protected havingCondition(ctx: QueryContext, expr: string, condition: QueryHavingMap[string]): void;
448
458
  /**
@@ -164,7 +164,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
164
164
  if (!order.length || (q.$limit === undefined && q.$skip === undefined)) {
165
165
  return false;
166
166
  }
167
- ctx.append(` ORDER BY ${order.map(({ ref, direction }) => ref + direction).join(', ')}`);
167
+ ctx.append(` ORDER BY ${order.map(({ ref, ...term }) => this.orderByTerm(ref, term)).join(', ')}`);
168
168
  return true;
169
169
  }
170
170
  /**
@@ -323,13 +323,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
323
323
  */
324
324
  carrySort(ctx, meta, q, opts) {
325
325
  const columns = [];
326
- const order = this.sortTerms(ctx, meta, q, opts).map(({ key, expr, direction, output }) => {
326
+ const order = this.sortTerms(ctx, meta, q, opts).map(({ key, expr, output, ...term }) => {
327
327
  if (output) {
328
- return { ref: expr, direction };
328
+ return { ref: expr, ...term };
329
329
  }
330
330
  const column = relationSortColumn(key);
331
331
  columns.push({ sql: expr, key: column });
332
- return { ref: this.escapeId(column, true), direction };
332
+ return { ref: this.escapeId(column, true), ...term };
333
333
  });
334
334
  return { columns, order };
335
335
  }
@@ -800,7 +800,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
800
800
  sort(ctx, entity, q, opts = {}) {
801
801
  const terms = this.sortTerms(ctx, getMeta(entity), q, opts);
802
802
  if (terms.length) {
803
- ctx.append(` ORDER BY ${terms.map(({ expr, direction }) => expr + direction).join(', ')}`);
803
+ ctx.append(` ORDER BY ${terms.map(({ expr, ...term }) => this.orderByTerm(expr, term)).join(', ')}`);
804
804
  }
805
805
  return terms.length > 0;
806
806
  }
@@ -837,7 +837,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
837
837
  // Where projected in the SELECT list, ordered by that alias rather than scored twice.
838
838
  const { order, project } = textSortOf(sort);
839
839
  const expr = project ? this.escapeId(project) : this.buildFragment(ctx, opts.rankText);
840
- columns.push({ key, expr, direction: this.resolveSortDirection(order), output: project !== undefined });
840
+ columns.push({ key, expr, ...this.resolveSortDirection(order), output: project !== undefined });
841
841
  continue;
842
842
  }
843
843
  if (relation) {
@@ -851,10 +851,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
851
851
  }
852
852
  const expr = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationAggregate(fragmentCtx, meta.entity, spec, prefix ?? ''));
853
853
  if (spec.search) {
854
- vectors.push({ key: name, expr, direction: '', output: false });
854
+ vectors.push({ key: name, expr, output: false });
855
855
  }
856
856
  else {
857
- columns.push({ key: keyPath, expr, direction: this.resolveSortDirection(direction), output: false });
857
+ columns.push({ key: keyPath, expr, ...this.resolveSortDirection(direction), output: false });
858
858
  }
859
859
  }
860
860
  if (rest === undefined) {
@@ -872,19 +872,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
872
872
  if (isVectorSearch(value)) {
873
873
  // Already projected in the SELECT list: order by that alias rather than recomputing it.
874
874
  vectors.push(value.$project
875
- ? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
875
+ ? { key: keyPath, expr: this.escapeId(value.$project), output: true }
876
876
  : {
877
877
  key: keyPath,
878
878
  expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value, prefix)),
879
- direction: '',
880
879
  output: false,
881
880
  });
882
881
  continue;
883
882
  }
884
- const direction = this.resolveSortDirection(value);
883
+ const order = this.resolveSortDirection(value);
885
884
  // A JSON path can sort by more than one reading, each carried under a name of its own.
886
885
  this.sortColumns(ctx, meta, key, prefix).forEach((column, index) => {
887
- columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, direction });
886
+ columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, ...order });
888
887
  });
889
888
  }
890
889
  }
@@ -1097,7 +1096,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1097
1096
  Object.entries(sort).forEach(([key, dir], index) => {
1098
1097
  if (index > 0)
1099
1098
  ctx.append(', ');
1100
- ctx.append(this.aggregateRef(emittedColumns, key, '$sort') + this.resolveSortDirection(dir));
1099
+ ctx.append(this.orderByTerm(this.aggregateRef(emittedColumns, key, '$sort'), this.resolveSortDirection(dir)));
1101
1100
  });
1102
1101
  return true;
1103
1102
  }
@@ -1119,14 +1118,36 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1119
1118
  });
1120
1119
  }
1121
1120
  static SORT_DIRECTION_MAP = new Map([
1122
- [1, ''],
1123
- ['asc', ''],
1124
- ['desc', ' DESC'],
1125
- [-1, ' DESC'],
1121
+ [1, {}],
1122
+ ['asc', {}],
1123
+ ['desc', { direction: ' DESC' }],
1124
+ [-1, { direction: ' DESC' }],
1125
+ ['ascNullsFirst', { nulls: 'first' }],
1126
+ ['ascNullsLast', { nulls: 'last' }],
1127
+ ['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
1128
+ ['descNullsLast', { direction: ' DESC', nulls: 'last' }],
1126
1129
  ]);
1127
1130
  resolveSortDirection(sort) {
1128
- const direction = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1129
- return orRefuse(direction, `unknown sort direction: ${sort}`);
1131
+ const order = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1132
+ return orRefuse(order, `unknown sort direction: ${sort}`);
1133
+ }
1134
+ /**
1135
+ * One `ORDER BY` term. A placement the engine has no `NULLS FIRST/LAST` for becomes a term of its
1136
+ * own in front of it, which is why one is only ever emitted where the caller asked for it: no index
1137
+ * serves an expression. SQL Server needs a `CASE`, having no orderable boolean.
1138
+ */
1139
+ orderByTerm(expr, { direction = '', nulls }) {
1140
+ if (!nulls) {
1141
+ return expr + direction;
1142
+ }
1143
+ const first = nulls === 'first';
1144
+ if (this.features.nullsOrdering === 'clause') {
1145
+ return `${expr}${direction} NULLS ${first ? 'FIRST' : 'LAST'}`;
1146
+ }
1147
+ const lead = this.features.nullsOrdering === 'case'
1148
+ ? `CASE WHEN ${expr} IS NULL THEN ${first ? 0 : 1} ELSE ${first ? 1 : 0} END`
1149
+ : `${expr} IS ${first ? 'NOT NULL' : 'NULL'}`;
1150
+ return `${lead}, ${expr}${direction}`;
1130
1151
  }
1131
1152
  /** Every operator of one `HAVING` condition, `AND`-joined. */
1132
1153
  havingCondition(ctx, expr, condition) {
@@ -1839,7 +1860,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1839
1860
  const key = relationTermKey(term);
1840
1861
  return [key, `${alias}.${this.escapeId(key, true)}`];
1841
1862
  }),
1842
- order: order.map(({ ref, direction }) => `${alias}.${ref}${direction}`).join(', '),
1863
+ order: order.map(({ ref, ...term }) => this.orderByTerm(`${alias}.${ref}`, term)).join(', '),
1843
1864
  };
1844
1865
  }
1845
1866
  /**
@@ -24,6 +24,11 @@ export declare const JSON_ELEM_ALIAS = "_uql_elem";
24
24
  export declare const JSON_PULL_ALIAS = "_uql_pull";
25
25
  /** The field a MongoDB `$sort` by `$text` reads `textScore` from where it projects none, taken back out after. */
26
26
  export declare const TEXT_SCORE_ALIAS = "_uql_text_score";
27
+ /**
28
+ * The field a MongoDB `$sort` placing nulls orders by first: 1 where the value is null or missing, 0
29
+ * where it is not. MongoDB takes no `NULLS FIRST`, so the placement is this flag plus the sort on it.
30
+ */
31
+ export declare function nullsSortField(path: string): string;
27
32
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
28
33
  export declare const REL_TEMP_PREFIX = "_uql_rel_";
29
34
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -25,6 +25,13 @@ export const JSON_ELEM_ALIAS = '_uql_elem';
25
25
  export const JSON_PULL_ALIAS = '_uql_pull';
26
26
  /** The field a MongoDB `$sort` by `$text` reads `textScore` from where it projects none, taken back out after. */
27
27
  export const TEXT_SCORE_ALIAS = '_uql_text_score';
28
+ /**
29
+ * The field a MongoDB `$sort` placing nulls orders by first: 1 where the value is null or missing, 0
30
+ * where it is not. MongoDB takes no `NULLS FIRST`, so the placement is this flag plus the sort on it.
31
+ */
32
+ export function nullsSortField(path) {
33
+ return `_uql_nulls_${path.replace(/\./g, '_')}`;
34
+ }
28
35
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
29
36
  export const REL_TEMP_PREFIX = '_uql_rel_';
30
37
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -31,6 +31,7 @@ export const MYSQL_FEATURES = {
31
31
  correlatedWrites: true,
32
32
  rowLocks: true,
33
33
  rowLockWithWindow: true,
34
+ nullsOrdering: 'expression',
34
35
  rowLockOf: true,
35
36
  textScoreIndexes: true,
36
37
  orderedUpsertReturning: true,
@@ -35,6 +35,7 @@ export const PG_FEATURES = {
35
35
  correlatedWrites: true,
36
36
  rowLocks: true,
37
37
  rowLockWithWindow: false,
38
+ nullsOrdering: 'clause',
38
39
  rowLockOf: true,
39
40
  textScoreIndexes: false,
40
41
  orderedUpsertReturning: true,
@@ -199,7 +199,7 @@ function detectIndexDrifts(diff) {
199
199
  table: idxDiff.table,
200
200
  index: idxDiff.name,
201
201
  details: `Index "${idxDiff.name}" exists in database but not defined in entity`,
202
- suggestion: 'Add @Field({ index }) or create migration to drop',
202
+ suggestion: 'Declare it, or drop it via migration: generate:entities drops one uql named',
203
203
  });
204
204
  }
205
205
  else {
@@ -211,7 +211,7 @@ function detectIndexDrifts(diff) {
211
211
  table: idxDiff.table,
212
212
  index: idxDiff.name,
213
213
  details: `Index "${idxDiff.name}" differs from the entity (${idxDiff.description})`,
214
- suggestion: 'Drop and recreate the index via migration',
214
+ suggestion: 'Recreate it via migration, which generate:entities writes',
215
215
  });
216
216
  }
217
217
  }
@@ -33,6 +33,7 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
33
33
  generateDropTable(tableName: string): string;
34
34
  generateAlterTable(diff: SchemaDiff): string[];
35
35
  generateAlterTableDown(diff: SchemaDiff): string[];
36
+ private dropIndexCommand;
36
37
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
37
38
  generateCreateIndex(tableName: string, index: IndexSchema): string;
38
39
  /** An Atlas vector search index: its vector field first, then each field a `$vectorSearch` pre-filters on. */
@@ -1,6 +1,7 @@
1
1
  import { getMeta } from '../../entity/index.js';
2
2
  import { MongoDialect } from '../../mongo/mongoDialect.js';
3
3
  import { textLanguage } from '../../mongo/textLanguage.js';
4
+ import { indexChanges } from '../../schema/indexDifferences.js';
4
5
  import { QueryRaw, } from '../../type/index.js';
5
6
  import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
6
7
  import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
@@ -8,6 +9,7 @@ import { fulltextConfig, fulltextWeights } from '../../util/dialect.util.js';
8
9
  import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
9
10
  import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
10
11
  import { renderIndexDefinition } from './definitionToNode.js';
12
+ import { indexNodeToSchema } from './indexNodeToSchema.js';
11
13
  import { serializeMongoCommand } from './mongoCommand.js';
12
14
  /** The index types a key spec can say, a plain key or `'text'`, and Atlas's vector search index. */
13
15
  const MONGO_INDEX_TYPES = new Set(['btree', 'fulltext', 'vectorSearch']);
@@ -105,12 +107,21 @@ export class MongoSchemaGenerator extends MongoDialect {
105
107
  return serializeMongoCommand({ action: 'dropCollection', name: tableName });
106
108
  }
107
109
  generateAlterTable(diff) {
108
- return (diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index));
110
+ return [
111
+ ...(diff.indexesToDrop ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
112
+ ...(diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
113
+ ];
109
114
  }
110
115
  generateAlterTableDown(diff) {
111
- return (diff.indexesToAdd ?? []).map((index) => index.type === 'vectorSearch'
112
- ? serializeMongoCommand({ action: 'dropSearchIndex', collection: diff.tableName, name: index.name })
113
- : this.generateDropIndex(diff.tableName, index.name));
116
+ return [
117
+ ...(diff.indexesToAdd ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
118
+ ...(diff.indexesToDrop ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
119
+ ];
120
+ }
121
+ dropIndexCommand(tableName, index) {
122
+ return index.type === 'vectorSearch'
123
+ ? serializeMongoCommand({ action: 'dropSearchIndex', collection: tableName, name: index.name })
124
+ : this.generateDropIndex(tableName, index.name);
114
125
  }
115
126
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
116
127
  generateCreateIndex(tableName, index) {
@@ -200,15 +211,16 @@ export class MongoSchemaGenerator extends MongoDialect {
200
211
  if (!currentTable) {
201
212
  return { tableName: collectionName, type: 'create' };
202
213
  }
203
- const existingIndexes = new Set(currentTable.indexes.map((i) => i.name));
204
- const indexesToAdd = this.indexesOf(meta, collectionName).filter((index) => !existingIndexes.has(index.name));
205
- if (indexesToAdd.length === 0) {
214
+ // By name: MongoDB lists a text index's fields alphabetically, so a shape would not match its own.
215
+ const { toAdd, toDrop } = indexChanges(collectionName, this.indexesOf(meta, collectionName), currentTable.indexes, (index) => index.name);
216
+ if (!toAdd.length && !toDrop.length) {
206
217
  return undefined;
207
218
  }
208
219
  return {
209
220
  tableName: collectionName,
210
221
  type: 'alter',
211
- indexesToAdd,
222
+ indexesToAdd: toAdd.length ? toAdd : undefined,
223
+ indexesToDrop: toDrop.length ? toDrop.map(indexNodeToSchema) : undefined,
212
224
  };
213
225
  }
214
226
  }
@@ -189,15 +189,16 @@ export class Migrator {
189
189
  this.logger.logInfo('No schema changes detected.');
190
190
  return '';
191
191
  }
192
+ // Diff by diff in reverse, each rolled back in the order its generator wrote it.
192
193
  const down = [
193
- ...created.map((tableName) => generator.generateDropTable(tableName, { ifExists: true })),
194
- ...altered.flatMap((diff) => generator.generateAlterTableDown(diff)),
194
+ ...altered.toReversed().flatMap((diff) => generator.generateAlterTableDown(diff)),
195
+ ...created.toReversed().map((tableName) => generator.generateDropTable(tableName, { ifExists: true })),
195
196
  ];
196
197
  const { emit } = this.target.source;
197
198
  const filePath = await this.writeMigration(name, {
198
199
  docExtraLines: ['Generated from entity definitions'],
199
200
  upInner: emit(up),
200
- downInner: emit(down.reverse()),
201
+ downInner: emit(down),
201
202
  });
202
203
  this.logger.logInfo(`Created migration from entities: ${filePath}`);
203
204
  return filePath;
@@ -345,7 +346,13 @@ export class Migrator {
345
346
  this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.foreignKeysToAlter.length} foreign keys in table '${diff.tableName}': ${filteredDiff.foreignKeysToAlter.map((fk) => fk.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
346
347
  delete filteredDiff.foreignKeysToAlter;
347
348
  }
348
- delete filteredDiff.indexesToDrop;
349
+ if (filteredDiff.indexesToDrop?.length) {
350
+ // An index recreated under its old name is a drop and an add, held back together.
351
+ const dropped = new Set(filteredDiff.indexesToDrop.map((index) => index.name));
352
+ this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${dropped.size} indexes in table '${diff.tableName}': ${[...dropped].join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
353
+ filteredDiff.indexesToAdd = filteredDiff.indexesToAdd?.filter((index) => !dropped.has(index.name));
354
+ delete filteredDiff.indexesToDrop;
355
+ }
349
356
  delete filteredDiff.foreignKeysToDrop;
350
357
  }
351
358
  if (!options.drop && filteredDiff.columnsToDrop?.length) {
@@ -62,12 +62,16 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
62
62
  private addForeignKeyStatements;
63
63
  /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
64
64
  private dropForeignKeyStatements;
65
+ /** The `ALTER COLUMN` restating each of `columns`. */
66
+ private alterColumnStatements;
67
+ /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
68
+ private addIndexStatements;
69
+ /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
70
+ private dropIndexStatements;
65
71
  generateAlterTableDown(diff: SchemaDiff): string[];
66
72
  generateCreateIndex(tableName: string, index: IndexSchema, options?: {
67
73
  ifNotExists?: boolean;
68
74
  }): string;
69
- /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
70
- private addIndexStatements;
71
75
  /**
72
76
  * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
73
77
  * operand instead, which is already qualified.
@@ -106,13 +110,6 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
106
110
  * drift detection runs, with types normalized as the engine stores them.
107
111
  */
108
112
  diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
109
- /**
110
- * Indexes the entity declares that the table does not already have, in any shape.
111
- *
112
- * Additive only: an index the entity does not name may well have been created deliberately outside
113
- * the ORM, and dropping it is a decision for a reviewed migration.
114
- */
115
- private missingIndexes;
116
113
  protected diffOptions(): DiffOptions;
117
114
  /** Spread, not copied field by field, so a field the node gains cannot go missing here. */
118
115
  private columnNodeToSchema;
@@ -1,6 +1,6 @@
1
1
  import { getMeta } from '../entity/index.js';
2
2
  import { canonicalToSql, engineType, isVectorCategory } from '../schema/canonicalType.js';
3
- import { indexSignature } from '../schema/indexDifferences.js';
3
+ import { indexChanges } from '../schema/indexDifferences.js';
4
4
  import { buildSchemaAST, resolveColumnCanonicalType } from '../schema/schemaASTBuilder.js';
5
5
  import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
6
6
  import { isAutoIncrement, qualifyName } from '../util/index.js';
@@ -144,40 +144,19 @@ export class SqlSchemaGenerator {
144
144
  ...(diff.foreignKeysToDrop ?? []),
145
145
  ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
146
146
  ]));
147
- // Add new columns
148
- if (diff.columnsToAdd?.length) {
149
- for (const column of diff.columnsToAdd) {
150
- this.assertColumnAddable(diff.tableName, column);
151
- statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
152
- statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
153
- }
154
- }
155
- // Alter existing columns
156
- if (diff.columnsToAlter?.length) {
157
- for (const { to } of diff.columnsToAlter) {
158
- const colDef = this.generateColumnDefinitionFromSchema(to);
159
- const colStatements = this.generateAlterColumnStatements(diff.tableName, to, colDef);
160
- statements.push(...colStatements);
161
- }
162
- }
163
- // Drop columns
164
- if (diff.columnsToDrop?.length) {
165
- for (const columnName of diff.columnsToDrop) {
166
- statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
167
- }
168
- }
169
- // Add indexes
170
- if (diff.indexesToAdd?.length) {
171
- for (const index of diff.indexesToAdd) {
172
- statements.push(...this.addIndexStatements(diff.tableName, index));
173
- }
174
- }
175
- // Drop indexes
176
- if (diff.indexesToDrop?.length) {
177
- for (const indexName of diff.indexesToDrop) {
178
- statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
179
- }
180
- }
147
+ // Before the adds, which may reuse a dropped index's name, and before the columns: some engines
148
+ // drop an index along with its column, which would leave nothing here to name.
149
+ statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
150
+ for (const column of diff.columnsToAdd ?? []) {
151
+ this.assertColumnAddable(diff.tableName, column);
152
+ statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
153
+ statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
154
+ }
155
+ statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
156
+ for (const columnName of diff.columnsToDrop ?? []) {
157
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
158
+ }
159
+ statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
181
160
  // Last, so every column it names exists by now.
182
161
  if (diff.primaryKey?.to.length) {
183
162
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
@@ -198,6 +177,21 @@ export class SqlSchemaGenerator {
198
177
  dropForeignKeyStatements(tableName, constraintNames) {
199
178
  return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
200
179
  }
180
+ /** The `ALTER COLUMN` restating each of `columns`. */
181
+ alterColumnStatements(tableName, columns) {
182
+ return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
183
+ }
184
+ /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
185
+ addIndexStatements(tableName, indexes = []) {
186
+ return indexes.flatMap((index) => [
187
+ this.generateCreateIndex(tableName, index),
188
+ ...this.indexDdl.settleStatements(tableName, index),
189
+ ]);
190
+ }
191
+ /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
192
+ dropIndexStatements(tableName, indexes = [], schema) {
193
+ return indexes.map((index) => this.generateDropIndex(tableName, index.name, schema));
194
+ }
201
195
  generateAlterTableDown(diff) {
202
196
  const statements = [];
203
197
  // Constraints first, mirroring the up direction: the up added them last, so the down drops them
@@ -212,44 +206,26 @@ export class SqlSchemaGenerator {
212
206
  if (diff.primaryKey?.to.length) {
213
207
  statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
214
208
  }
215
- // Reverse column additions by dropping them
216
- if (diff.columnsToAdd?.length) {
217
- for (const column of diff.columnsToAdd) {
218
- statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
219
- }
220
- }
221
- // Reverse column alterations by restoring original schema
222
- if (diff.columnsToAlter?.length) {
223
- for (const { from } of diff.columnsToAlter) {
224
- const colDef = this.generateColumnDefinitionFromSchema(from);
225
- const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
226
- statements.push(...colStatements);
227
- }
228
- }
229
- // Reverse index additions by dropping them
230
- if (diff.indexesToAdd?.length) {
231
- for (const index of diff.indexesToAdd) {
232
- statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
233
- }
209
+ for (const column of diff.columnsToAdd ?? []) {
210
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
234
211
  }
212
+ statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
213
+ statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
214
+ statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
235
215
  if (diff.primaryKey?.from.length) {
236
216
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
237
217
  }
238
218
  // The constraint the up replaced, back under the name the database had for it. A foreign key the
239
219
  // up *dropped* is not restored: only its name survived the diff, never what it pointed at.
240
220
  statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
241
- if (diff.columnsToDrop?.length || diff.indexesToDrop?.length || diff.foreignKeysToDrop?.length) {
242
- statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes/foreign keys`);
221
+ if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
222
+ statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
243
223
  }
244
224
  return statements;
245
225
  }
246
226
  generateCreateIndex(tableName, index, options = {}) {
247
227
  return this.indexDdl.getCreateIndexStatement(tableName, index, options);
248
228
  }
249
- /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
250
- addIndexStatements(tableName, index) {
251
- return [this.generateCreateIndex(tableName, index), ...this.indexDdl.settleStatements(tableName, index)];
252
- }
253
229
  /**
254
230
  * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
255
231
  * operand instead, which is already qualified.
@@ -351,10 +327,12 @@ export class SqlSchemaGenerator {
351
327
  }
352
328
  // Indexes are matched here rather than by the differ, which pairs them by name so that a changed
353
329
  // one reads as one index that altered. A migration needs the opposite: an index already in the
354
- // table, under whatever name, must not be created again, and one whose shape differs is a
355
- // separate index rather than a change - no engine alters an index's columns or uniqueness.
330
+ // table, under whatever name, must not be created again, and one whose shape differs is dropped
331
+ // and created anew - no engine alters an index's columns or uniqueness.
356
332
  const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
357
- const indexesToAdd = this.missingIndexes(desired, currentTable);
333
+ const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
334
+ const indexesToAdd = indexes.toAdd.map(indexNodeToSchema);
335
+ const indexesToDrop = indexes.toDrop.map(indexNodeToSchema);
358
336
  const columnDiffs = tableDiff?.columnDiffs ?? [];
359
337
  const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
360
338
  const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
@@ -387,6 +365,7 @@ export class SqlSchemaGenerator {
387
365
  !columnsToAlter.length &&
388
366
  !columnsToDrop.length &&
389
367
  !indexesToAdd.length &&
368
+ !indexesToDrop.length &&
390
369
  !foreignKeysToAdd.length &&
391
370
  !foreignKeysToDrop.length &&
392
371
  !foreignKeysToAlter.length &&
@@ -402,21 +381,12 @@ export class SqlSchemaGenerator {
402
381
  columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
403
382
  columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
404
383
  indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
384
+ indexesToDrop: indexesToDrop.length ? indexesToDrop : undefined,
405
385
  foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
406
386
  foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
407
387
  foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
408
388
  };
409
389
  }
410
- /**
411
- * Indexes the entity declares that the table does not already have, in any shape.
412
- *
413
- * Additive only: an index the entity does not name may well have been created deliberately outside
414
- * the ORM, and dropping it is a decision for a reviewed migration.
415
- */
416
- missingIndexes(desired, currentTable) {
417
- const present = new Set(currentTable.indexes.map(indexSignature));
418
- return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
419
- }
420
390
  diffOptions() {
421
391
  return {
422
392
  normalizeType: engineType(this.dialect),
@@ -563,7 +533,9 @@ export class SqlSchemaGenerator {
563
533
  case 'alterColumn':
564
534
  return this.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
565
535
  case 'createIndex':
566
- return this.addIndexStatements(operation.tableName, renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)));
536
+ return this.addIndexStatements(operation.tableName, [
537
+ renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)),
538
+ ]);
567
539
  case 'dropIndex':
568
540
  return [this.generateDropIndex(operation.tableName, operation.indexName)];
569
541
  case 'addForeignKey':
@@ -2,10 +2,20 @@ import { type Document, type Filter, type Sort, type UpdateFilter } from 'mongod
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
3
  import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
4
4
  import { type CallbackKey } from '../util/index.js';
5
+ /**
6
+ * An ordering as the pipeline runs it: the `$sort`, the fields it needs on the document first - a
7
+ * placement's null flags - and the ones to take back off after it. One object, so a caller cannot
8
+ * order by a field it forgot to add, or leave one behind in the rows it answers with.
9
+ */
10
+ export type MongoSortPlan = {
11
+ readonly sort: Sort;
12
+ readonly stages: MongoAggregationPipelineEntry<Document>[];
13
+ readonly fields: string[];
14
+ };
5
15
  /** What a read pipeline contributes to {@link MongoDialect.readStages} beyond the query itself. */
6
16
  type MongoReadStages = {
7
17
  /** Ordering, which runs after the lookups when it reads one of their fields. */
8
- readonly sort?: Sort;
18
+ readonly sort?: MongoSortPlan;
9
19
  readonly pager?: MongoAggregationPipelineEntry<Document>[];
10
20
  /** A score the read answers as a field, a vector search's or a text search's; a temporary one leaves again. */
11
21
  readonly score?: {
@@ -126,7 +136,13 @@ export declare class MongoDialect extends AbstractDialect {
126
136
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
127
137
  * added for the sort alone would change what the caller gets back.
128
138
  */
129
- sort<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): Sort;
139
+ sort<E extends Document>(entity: Type<E>, q: Query<E>): Sort;
140
+ /**
141
+ * The ordering, and the paths whose nulls it places. MongoDB sorts null and missing lowest and takes
142
+ * no placement, so one is emulated: {@link nullsSortStages} flags each path and the flag is ordered
143
+ * by ahead of the value itself.
144
+ */
145
+ sortPlan<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): MongoSortPlan;
130
146
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
131
147
  private collectSort;
132
148
  /**
@@ -1,6 +1,6 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
3
+ import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
4
  import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
5
5
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { COUNT_RESULT_KEY } from '../type/query.js';
@@ -9,6 +9,7 @@ import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInte
9
9
  import { decodeBigIntsExcept } from '../util/wideNumber.js';
10
10
  import { textLanguage } from './textLanguage.js';
11
11
  import { vectorDistanceExpr } from './vectorDistance.js';
12
+ const EMPTY_SORT_PLAN = { sort: {}, stages: [], fields: [] };
12
13
  /** A scalar field's operator as the aggregation operator computing it. */
13
14
  const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
14
15
  /** Default {@link DialectFeatures} for MongoDB. */
@@ -460,10 +461,19 @@ export class MongoDialect extends AbstractDialect {
460
461
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
461
462
  * added for the sort alone would change what the caller gets back.
462
463
  */
463
- sort(entity, { $sort: sort, $populate: populate, $where: where }) {
464
+ sort(entity, q) {
465
+ return this.sortPlan(entity, q).sort;
466
+ }
467
+ /**
468
+ * The ordering, and the paths whose nulls it places. MongoDB sorts null and missing lowest and takes
469
+ * no placement, so one is emulated: {@link nullsSortStages} flags each path and the flag is ordered
470
+ * by ahead of the value itself.
471
+ */
472
+ sortPlan(entity, { $sort: sort, $populate: populate, $where: where }) {
464
473
  const meta = getMeta(entity);
465
474
  const nearest = {};
466
475
  const columns = {};
476
+ const placed = [];
467
477
  // Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
468
478
  if (sort?.$text) {
469
479
  rankedTextSearch(where);
@@ -472,12 +482,17 @@ export class MongoDialect extends AbstractDialect {
472
482
  // pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
473
483
  // exactly as it does on the SQL dialects.
474
484
  const joins = resolveQueryJoins(meta, { $populate: populate, $sort: sort });
475
- this.collectSort(meta, sort, joins, '', nearest, columns);
476
- // A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
477
- return { ...nearest, ...columns };
485
+ this.collectSort(meta, sort, joins, '', nearest, columns, placed);
486
+ const flags = Object.fromEntries(placed.map((path) => [nullsSortField(path), { $cond: [{ $eq: [{ $ifNull: [`$${path}`, null] }, null] }, 1, 0] }]));
487
+ return {
488
+ // A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
489
+ sort: { ...nearest, ...columns },
490
+ stages: placed.length ? [{ $addFields: flags }] : [],
491
+ fields: placed.map(nullsSortField),
492
+ };
478
493
  }
479
494
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
480
- collectSort(meta, sort, joins, path, nearest, out) {
495
+ collectSort(meta, sort, joins, path, nearest, out, placed) {
481
496
  for (const [key, value] of Object.entries(sort ?? {})) {
482
497
  const relation = meta.relations[key];
483
498
  if (key === '$text') {
@@ -495,7 +510,14 @@ export class MongoDialect extends AbstractDialect {
495
510
  if (isVectorSearch(value)) {
496
511
  throw new TypeError(`cannot $sort by a second vector '${key}' on MongoDB: $vectorSearch ranks by one`);
497
512
  }
498
- out[path + this.pathOf(meta, key)] = sortDirection(value);
513
+ const docPath = path + this.pathOf(meta, key);
514
+ const nulls = sortNulls(value);
515
+ if (nulls) {
516
+ placed.push(docPath);
517
+ // The flag holds 1 for a null, so ordering by it descending brings the null block to the front.
518
+ out[nullsSortField(docPath)] = nulls === 'first' ? -1 : 1;
519
+ }
520
+ out[docPath] = sortDirection(value);
499
521
  continue;
500
522
  }
501
523
  // A `$lookup` is what puts the relation's fields on the document, and only `$populate` asks for
@@ -521,7 +543,7 @@ export class MongoDialect extends AbstractDialect {
521
543
  continue;
522
544
  }
523
545
  const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, rest, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
524
- this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out);
546
+ this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out, placed);
525
547
  }
526
548
  }
527
549
  /**
@@ -699,7 +721,7 @@ export class MongoDialect extends AbstractDialect {
699
721
  return [
700
722
  ...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
701
723
  ...this.readStages(entity, q, {
702
- sort: this.sort(entity, q),
724
+ sort: this.sortPlan(entity, q),
703
725
  pager: this.pagerStages(q),
704
726
  score: text && { field: text.project ?? TEXT_SCORE_ALIAS, meta: 'textScore', temporary: !text.project },
705
727
  }),
@@ -722,10 +744,12 @@ export class MongoDialect extends AbstractDialect {
722
744
  // The value an ordering by a relation's aggregate reads, and the field it parks it on: both belong
723
745
  // with the lookups, since the `$sort` right after them is what they exist for.
724
746
  const aggregated = this.sortAggregateStages(entity, q.$sort);
747
+ const ordering = extra.sort ?? EMPTY_SORT_PLAN;
725
748
  const lookups = [...this.lookupStages(meta, joins), ...aggregated.stages];
726
749
  // Each to-many and each `$count`, which neither drop nor reorder a row, so they read the page alone.
727
750
  const related = this.relationReadStages(entity, q);
728
- const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
751
+ // The flags a placement orders by travel with their `$sort`, wherever the pipeline puts it.
752
+ const sort = hasKeys(ordering.sort) ? [...ordering.stages, { $sort: ordering.sort }] : [];
729
753
  const pager = extra.pager ?? [];
730
754
  // The score becomes a real field before anything reads it, so the lookups, the sort and the projection
731
755
  // that follow treat it like any other; merged into the query's own projection rather than standing in
@@ -743,7 +767,7 @@ export class MongoDialect extends AbstractDialect {
743
767
  // which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
744
768
  // it has run, so ordering by an unpopulated relation costs the same nothing it does there.
745
769
  const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
746
- const dropped = [...sortOnly, ...aggregated.fields];
770
+ const dropped = [...sortOnly, ...aggregated.fields, ...ordering.fields];
747
771
  const unset = dropped.length ? [{ $unset: dropped }] : [];
748
772
  // The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
749
773
  // that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
@@ -1328,9 +1352,16 @@ export class MongoDialect extends AbstractDialect {
1328
1352
  return { $vectorSearch: stage };
1329
1353
  }
1330
1354
  }
1331
- /** `-1` for the two descending spellings, `1` for everything else - MongoDB knows no other value. */
1355
+ /** `-1` for every descending spelling, `1` for everything else - MongoDB knows no other value. */
1332
1356
  function sortDirection(value) {
1333
- return value === 'desc' || value === -1 ? -1 : 1;
1357
+ return value === -1 || (typeof value === 'string' && value.startsWith('desc')) ? -1 : 1;
1358
+ }
1359
+ /** Where a direction asks nulls to land, where it asks at all. */
1360
+ function sortNulls(value) {
1361
+ if (typeof value !== 'string') {
1362
+ return undefined;
1363
+ }
1364
+ return value.endsWith('NullsFirst') ? 'first' : value.endsWith('NullsLast') ? 'last' : undefined;
1334
1365
  }
1335
1366
  /**
1336
1367
  * A `computed` field writing SQL is refused wherever a query names it, since no document engine
@@ -78,6 +78,8 @@ export class MongodbQuerier extends AbstractQuerier {
78
78
  this.dialect.constrainsRelations(entity, q.$where) ||
79
79
  this.dialect.sortsRelations(entity, q.$sort) ||
80
80
  this.dialect.readsAggregates(entity, q) ||
81
+ // A placement is a field the pipeline adds and orders by; a `find` cursor can add none.
82
+ hasKeys(this.dialect.sortPlan(entity, q).fields) ||
81
83
  textSortOf(q.$sort) !== undefined);
82
84
  }
83
85
  buildScalarProjection(entity, q) {
@@ -117,7 +119,7 @@ export class MongodbQuerier extends AbstractQuerier {
117
119
  this.dialect.buildVectorSearchStage(entity, vectorSort.vectorKey, vectorSort.vectorSearch, q.$where, q.$limit ?? 10, opts, vectorCandidates(q)),
118
120
  // `$vectorSearch` has already applied `$limit`, so the pager is its own.
119
121
  ...this.dialect.readStages(entity, q, {
120
- sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
122
+ sort: this.dialect.sortPlan(entity, { ...q, $sort: vectorSort.regularSort }),
121
123
  score: scoreAlias ? { field: scoreAlias, meta: 'vectorSearchScore' } : undefined,
122
124
  }),
123
125
  ];
@@ -33,6 +33,7 @@ const MSSQL_FEATURES = {
33
33
  correlatedWrites: true,
34
34
  rowLocks: true,
35
35
  rowLockWithWindow: true,
36
+ nullsOrdering: 'case',
36
37
  rowLockOf: true,
37
38
  textScoreIndexes: false,
38
39
  orderedUpsertReturning: false,
@@ -5,18 +5,28 @@ import type { IndexNode } from './types.js';
5
5
  * `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
6
6
  */
7
7
  export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
8
+ type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
8
9
  /**
9
10
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
10
11
  * over an expression, whose text the engine reprints, falls back to its name.
11
12
  */
12
- export declare function indexSignature(index: Pick<IndexNode, 'name' | 'entries' | 'unique'>): string;
13
+ export declare function indexSignature(index: ComparableIndex): string;
13
14
  /**
14
15
  * A constraint name without its kind marker, pairing an index with its older spelling
15
16
  * (`idx_User_email` with `User__email_idx`). Only one marker, the trailing one first.
16
17
  */
17
18
  export declare function indexNameStem(name: string): string;
19
+ /**
20
+ * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
21
+ * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
22
+ */
23
+ export declare function indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
24
+ toAdd: I[];
25
+ toDrop: IndexNode[];
26
+ };
18
27
  /**
19
28
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
20
29
  * path or a predicate is reprinted by the engine, so never compared.
21
30
  */
22
31
  export declare function describeIndexDifferences(source: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
32
+ export {};
@@ -1,11 +1,16 @@
1
1
  import { isVectorIndexType } from '../type/vector.js';
2
2
  import { fulltextConfig } from '../util/dialect.util.js';
3
+ import { derivedIndexName } from '../util/sql.util.js';
4
+ /** An entry the engine reprints in its own words, so never compared as written. */
5
+ function isReprinted(entry) {
6
+ return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
7
+ }
3
8
  /**
4
9
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
5
10
  * over an expression, whose text the engine reprints, falls back to its name.
6
11
  */
7
12
  export function indexSignature(index) {
8
- const comparable = !index.entries.some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
13
+ const comparable = !index.entries.some(isReprinted);
9
14
  const identity = comparable
10
15
  ? index.entries.map((entry) => entry.column).join(',')
11
16
  : `name:${indexNameStem(index.name)}`;
@@ -20,6 +25,33 @@ export function indexNameStem(name) {
20
25
  const bare = withoutSuffix === name ? name.replace(KIND_PREFIX, '') : withoutSuffix;
21
26
  return bare.replace(/__/g, '_');
22
27
  }
28
+ /**
29
+ * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
30
+ * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
31
+ */
32
+ export function indexChanges(table, declared, current, keyOf = indexSignature) {
33
+ const present = new Set(current.map(keyOf));
34
+ const wanted = new Set(declared.map(keyOf));
35
+ const claimed = new Set(declared.map((index) => index.name));
36
+ const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
37
+ return {
38
+ toAdd: declared.filter((index) => !present.has(keyOf(index))),
39
+ toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
40
+ };
41
+ }
42
+ /**
43
+ * Whether uql named the index itself, from its own columns: `Order__total_idx`, its unique `_uk`, or
44
+ * the `idx_Order_total` it wrote until 0.42.1.
45
+ */
46
+ function hasDerivedName(table, index) {
47
+ const parts = index.entries.map((entry, at) => (isReprinted(entry) ? `expr${at}` : entry.column));
48
+ const derived = [
49
+ derivedIndexName(table, parts),
50
+ derivedIndexName(table, parts, true),
51
+ `idx_${table}_${parts.join('_')}`,
52
+ ];
53
+ return derived.includes(index.name);
54
+ }
23
55
  /** What this version emits. */
24
56
  const KIND_SUFFIX = /_(?:idx|fk|ck|pk|uk|uq)$/i;
25
57
  /**
@@ -34,7 +66,7 @@ const KIND_PREFIX = /^(?:idx|fk|ck|pk|uk|uq)_/i;
34
66
  */
35
67
  export function describeIndexDifferences(source, target, facets) {
36
68
  const differences = [];
37
- const comparableEntries = ![...source.entries, ...target.entries].some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
69
+ const comparableEntries = ![...source.entries, ...target.entries].some(isReprinted);
38
70
  if (comparableEntries) {
39
71
  const [sourceColumns, targetColumns] = [source, target].map((index) => textFieldOrder(index, facets, index.entries.map((entry) => entrySignature(entry, facets))).join(', '));
40
72
  if (sourceColumns !== targetColumns) {
@@ -35,6 +35,7 @@ export const SQLITE_FEATURES = {
35
35
  correlatedWrites: true,
36
36
  rowLocks: false,
37
37
  rowLockWithWindow: true,
38
+ nullsOrdering: 'clause',
38
39
  rowLockOf: true,
39
40
  textScoreIndexes: false,
40
41
  orderedUpsertReturning: true,
@@ -144,6 +144,11 @@ export interface SqlDialectFeatures extends DialectFeatures {
144
144
  readonly rowLockWithWindow: boolean;
145
145
  /** Whether a lock can be narrowed to one table of a join, `FOR UPDATE OF`, which MariaDB lacks. */
146
146
  readonly rowLockOf: boolean;
147
+ /**
148
+ * How a `$sort` states where nulls land: the `NULLS FIRST/LAST` clause, a leading `IS NULL` term
149
+ * (MySQL, MariaDB), or a leading `CASE` (SQL Server, which has no orderable boolean).
150
+ */
151
+ readonly nullsOrdering: 'clause' | 'expression' | 'case';
147
152
  /**
148
153
  * Whether a fulltext index's heavier column needs a fulltext index of its own to be scored by, as
149
154
  * MySQL's `MATCH` does, which reads only an index over exactly its columns.
@@ -729,7 +729,10 @@ export type IndexOptions = Except<EntityIndexMeta, 'columns' | 'where'> & {
729
729
  */
730
730
  export type EntityIndexOptions<E> = Except<IndexOptions, 'include' | 'where'> & {
731
731
  readonly include?: (refs: RefMap<E>) => readonly ColumnRef<FieldKey<E>>[];
732
- /** Partial-index predicate. See {@link EntityWhere}. */
732
+ /**
733
+ * Partial-index predicate, written as the predicate the query passes: a planner matches the two by
734
+ * shape, so a hand-written `raw` that means the same thing leaves the index unused. See {@link EntityWhere}.
735
+ */
733
736
  readonly where?: EntityWhere<E>;
734
737
  };
735
738
  /**
@@ -203,7 +203,8 @@ export interface SchemaDiff {
203
203
  }[];
204
204
  readonly columnsToDrop?: string[];
205
205
  readonly indexesToAdd?: IndexSchema[];
206
- readonly indexesToDrop?: string[];
206
+ /** Whole rather than by name, so the rollback can create each again. */
207
+ readonly indexesToDrop?: IndexSchema[];
207
208
  readonly foreignKeysToAdd?: ForeignKeySchema[];
208
209
  /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
209
210
  readonly foreignKeysToDrop?: string[];
@@ -115,9 +115,16 @@ export type FilterOptions<E = unknown> = {
115
115
  readonly onMissing?: 'throw';
116
116
  });
117
117
  /**
118
- * direction for the sort.
119
- */
120
- export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
118
+ * direction for the sort, and where nulls land in it.
119
+ *
120
+ * Unqualified, each engine has its own answer - Postgres and CockroachDB sort nulls last on `asc`, the
121
+ * rest sort them first - so a placement is the only portable one. Engines with no `NULLS FIRST/LAST`
122
+ * emulate it with a leading term, which no index can serve, which is why it is asked for and never
123
+ * applied by default.
124
+ */
125
+ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc' | QuerySortNullsDirection;
126
+ /** A {@link QuerySortDirection} stating where nulls land. */
127
+ export type QuerySortNullsDirection = 'ascNullsFirst' | 'ascNullsLast' | 'descNullsFirst' | 'descNullsLast';
121
128
  /**
122
129
  * Accepted value for a field in `$sort` - either a direction or a vector similarity search.
123
130
  */
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.74.0",
6
+ "version": "0.75.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -74,12 +74,11 @@ export class Post {
74
74
  }
75
75
  ```
76
76
 
77
- - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as
78
- `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
79
- - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match:
80
- `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
77
+ - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
78
+ - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match: `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
81
79
  - Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
82
80
  - `@ManyToMany({ entity: () => Tag, through: () => PostTag })` names its junction entity.
81
+ - `@Index((post) => [post.authorId], { where: { archived: { $ne: true } } })` states a partial index's filter as the predicate the query passes, never as `raw`: a planner matches the two by shape, so `raw` that means the same thing leaves the index unused.
83
82
  - `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
84
83
 
85
84
  ## Queries
@@ -100,6 +99,10 @@ const users = await pool.findMany(User, {
100
99
 
101
100
  - The keys are `$select`, `$exclude`, `$where`, `$populate`, `$count`, `$distinct`, `$sort`, `$skip`, `$limit`;
102
101
  `$count: { posts: true }` tallies a to-many under `_count` without loading it.
102
+ - `$sort` takes `'asc'`/`1` or `'desc'`/`-1`, and `'ascNullsLast'`, `'ascNullsFirst'`, `'descNullsFirst'` or
103
+ `'descNullsLast'` to say where nulls land, which reads the same on every engine (emulated where there is no
104
+ `NULLS FIRST`). Unqualified, each engine keeps its own answer: Postgres and CockroachDB sort nulls last on `asc`,
105
+ the rest sort them first.
103
106
  - `$where` takes a value for equality or an operator map: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`,
104
107
  `$nin`, `$between`, `$like`, `$ilike`, `$regex`, `$startsWith`, `$endsWith`, `$includes`, `$isNull`,
105
108
  `$isNotNull`. `$and`, `$or`, `$not` and `$nor` combine clauses.