@lunora/sql-store 1.0.0-alpha.137 → 1.0.0-alpha.139

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.
@@ -147,8 +147,13 @@ interface SqlDialect {
147
147
  createCompanion: (companion: string, keyType: string) => SQL;
148
148
  /** DDL for the indexes that make the match fast. */
149
149
  createIndexes: (companion: string) => SQL[];
150
- /** Replace one document's row, given its already-analyzed token stream. */
151
- indexDocument: (companion: string, id: string, analyzed: string) => SQL;
150
+ /**
151
+ * Replace one document's row, given its already-analyzed token stream,
152
+ * only where `guard` holds. One idempotent statement: two writers of one
153
+ * id converge on one row, never raise a key conflict, and a writer whose
154
+ * `guard` fails writes nothing.
155
+ */
156
+ indexDocument: (companion: string, id: string, analyzed: string, guard: SQL) => SQL;
152
157
  /** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
153
158
  matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
154
159
  /** The `ORDER BY` expression, best first. */
package/dist/dialect.d.ts CHANGED
@@ -147,8 +147,13 @@ interface SqlDialect {
147
147
  createCompanion: (companion: string, keyType: string) => SQL;
148
148
  /** DDL for the indexes that make the match fast. */
149
149
  createIndexes: (companion: string) => SQL[];
150
- /** Replace one document's row, given its already-analyzed token stream. */
151
- indexDocument: (companion: string, id: string, analyzed: string) => SQL;
150
+ /**
151
+ * Replace one document's row, given its already-analyzed token stream,
152
+ * only where `guard` holds. One idempotent statement: two writers of one
153
+ * id converge on one row, never raise a key conflict, and a writer whose
154
+ * `guard` fails writes nothing.
155
+ */
156
+ indexDocument: (companion: string, id: string, analyzed: string, guard: SQL) => SQL;
152
157
  /** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
153
158
  matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
154
159
  /** The `ORDER BY` expression, best first. */
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createSqlCtxDb as a,readSqlCdcChangedTables as o,readSqlCdcChanges as t,readSqlCdcFloor as l,runSqlCdcMigration as s,sweepSqlCdcRetention as n}from"./packem_shared/createSqlCtxDb-B8367Cr7.mjs";import{runSqlAggregateMigrations as d,runSqlGlobalTableMigrations as S,runSqlRankMigrations as c}from"./packem_shared/runSqlAggregateMigrations-CmsPHIJb.mjs";import{d as g,r as f}from"./packem_shared/ctx-db-search-CvZzVlOl.mjs";import{S as m,m as x}from"./packem_shared/ctx-db-search-state-DGbsWTp6.mjs";import{f as p,e as u,b as h,s as M,t as T}from"./packem_shared/value-codec-DC_dHxUY.mjs";import{decodeGlobalRow as E}from"./packem_shared/decodeGlobalRow-VOl_TIbk.mjs";export{m as SEARCH_STATE_TABLE,g as backfillSqlSearchIndexes,a as createSqlCtxDb,p as decodeBigint,E as decodeGlobalRow,u as effectiveColumnKind,x as migrateSearchState,o as readSqlCdcChangedTables,t as readSqlCdcChanges,l as readSqlCdcFloor,d as runSqlAggregateMigrations,s as runSqlCdcMigration,S as runSqlGlobalTableMigrations,c as runSqlRankMigrations,f as runSqlSearchMigrations,h as sqliteDecode,M as sqliteEncode,n as sweepSqlCdcRetention,T as tryJsonParse};
1
+ import{createSqlCtxDb as a,readSqlCdcChangedTables as o,readSqlCdcChanges as t,readSqlCdcFloor as l,runSqlCdcMigration as s,sweepSqlCdcRetention as n}from"./packem_shared/createSqlCtxDb-D-mXDmWV.mjs";import{runSqlAggregateMigrations as d,runSqlGlobalTableMigrations as S,runSqlRankMigrations as c}from"./packem_shared/runSqlAggregateMigrations-CmsPHIJb.mjs";import{d as g,r as f}from"./packem_shared/ctx-db-search-Dxuv99A0.mjs";import{S as m,m as x}from"./packem_shared/ctx-db-search-state-DGbsWTp6.mjs";import{f as p,e as u,b as h,s as M,t as T}from"./packem_shared/value-codec-DC_dHxUY.mjs";import{decodeGlobalRow as E}from"./packem_shared/decodeGlobalRow-VOl_TIbk.mjs";export{m as SEARCH_STATE_TABLE,g as backfillSqlSearchIndexes,a as createSqlCtxDb,p as decodeBigint,E as decodeGlobalRow,u as effectiveColumnKind,x as migrateSearchState,o as readSqlCdcChangedTables,t as readSqlCdcChanges,l as readSqlCdcFloor,d as runSqlAggregateMigrations,s as runSqlCdcMigration,S as runSqlGlobalTableMigrations,c as runSqlRankMigrations,f as runSqlSearchMigrations,h as sqliteDecode,M as sqliteEncode,n as sweepSqlCdcRetention,T as tryJsonParse};
@@ -1 +1 @@
1
- import"./ctx-db-search-state-DGbsWTp6.mjs";import{d as S,c as s,a as i,r as l}from"./ctx-db-search-CvZzVlOl.mjs";import"./decodeGlobalRow-VOl_TIbk.mjs";export{S as backfillSqlSearchIndexes,s as createSearchSync,i as runSqlSearch,l as runSqlSearchMigrations};
1
+ import"./ctx-db-search-state-DGbsWTp6.mjs";import{d as S,c as s,a as i,r as l}from"./ctx-db-search-Dxuv99A0.mjs";import"./decodeGlobalRow-VOl_TIbk.mjs";export{S as backfillSqlSearchIndexes,s as createSearchSync,i as runSqlSearch,l as runSqlSearchMigrations};
@@ -1,4 +1,4 @@
1
- import{LunoraError as v}from"@lunora/errors";import{m as xt,D as Pt}from"./ctx-db-search-state-DGbsWTp6.mjs";import{c as Ht,r as Kt,A as jt,C as Gt,R as Vt,a as Yt,T as Xt,x as zt,b as Qt}from"./ctx-db-search-CvZzVlOl.mjs";import{d as gi}from"./ctx-db-search-CvZzVlOl.mjs";import{literalInList as Jt,throwingScheduler as Zt,CDC_LOG_TABLE as P,cdcForkedError as en,cursorBelowRetainedFloor as tn,cdcTrimmedError as nn,assertNoExplicitUndefined as nt,hasTrigger as on,runRowValidators as De,assertFlatPredicate as it,mergeWhere as B,rankTableName as we,sortColumnName as ye,resolveRankPartition as ot,encodePartitionKey as Ae,resolveRankSeekTuple as rn,RANK_TIEBREAK as pe,CountRlsUnsupportedError as rt,stripReservedPatchFields as sn,normalizeIdStructurally as an,assertValidClientId as cn,aggregateSqlFunction as $e,softDeleteScope as _e,compileWhereSql as he,normalizeOrderKeys as dn,uniqueIndexFields as ln,equalityPinnedFields as un,buildSeekWhere as fn,decodeCursor as wn,resolveRelationPredicates as st,resolveWith as at,relationHooks as ct,applySelect as dt,encodeCursor as $n,NotFoundError as _n,applyOnDelete as hn,ConflictError as Le,normalizeCountArgument as En,selectIndexForCount as yn,encodeAggregateKey as Y,aggregateTableName as Q,selectIndexForAggregate as pn,readAggregateValue as Be,runTriggers as Rn,matchesRankStaticWhere as lt,rankPivotConditionSql as St,CURSOR_PREFIX as Sn,selectIndexForGroupBy as gn,tiebreakDirectionFor as vn,fanOutScalarCounts as Tn,matchesStaticWhere as Fe,foldAggregateTally as ut,coerceAggregateNumber as Oe,CDC_LOG_TABLE_SEQ_INDEX as mn,NotUniqueError as bn}from"@lunora/shard-engine";import{sql as e}from"drizzle-orm";import{d as gt,b as In,e as vt,n as Cn,c as An}from"./value-codec-DC_dHxUY.mjs";import{runSqlGlobalTableMigrations as Ln,runSqlAggregateMigrations as kn,runSqlRankMigrations as Mn}from"./runSqlAggregateMigrations-CmsPHIJb.mjs";import{queryAll as m,queryRun as M,createIndexIfNotExists as Nn,decodeRow as ee,serializeDocumentColumn as J,OCC_VERSION_COLUMN as Ee,columnRefSql as F,decodeRows as ft,tableColumns as Tt,nextRowVersion as wt,serializeColumnValue as We,forEachRowPaged as $t,BACKFILL_BATCH_SIZE as Ue,nullSafeEqualsSql as Ke,queryBatch as _t}from"./decodeGlobalRow-VOl_TIbk.mjs";import{decodeGlobalRow as Ti}from"./decodeGlobalRow-VOl_TIbk.mjs";const qn=(a,c)=>{if(a.size<c)return;const E=a.keys().next().value;E!==void 0&&a.delete(E)},ht=50,Dn=(a,c={})=>{const E=c.maxBatch??ht,r=Number.isInteger(E)&&E>0?E:ht,p=new Map;let w=!1;const d=async(g,T)=>{const b=[...T.keys()];for(let C=0;C<b.length;C+=r){const N=b.slice(C,C+r);try{const W=await a(g,N);for(const G of N){const V=W.get(G);for(const ie of T.get(G)??[])ie.resolve(V)}}catch(W){for(const G of N)for(const V of T.get(G)??[])V.reject(W)}}},L=()=>{w=!1;const g=[...p.entries()];p.clear();for(const[T,b]of g)d(T,b).catch(()=>{})};return{async load(g,T){return new Promise((b,C)=>{let N=p.get(g);N||(N=new Map,p.set(g,N));const W=N.get(T);W?W.push({reject:C,resolve:b}):N.set(T,[{reject:C,resolve:b}]),w||(w=!0,queueMicrotask(L))})}}},Bn=new Set(["_id","id"]),mt=(a,c)=>a.name!=="postgres"||c.nullable!==!0?"":c.direction==="desc"?" NULLS LAST":" NULLS FIRST",Fn=(a,c)=>{const E=a.map(r=>e`${F(r.field)} ${e.raw(`${r.direction==="desc"?"DESC":"ASC"}${mt(c,r)}`)}`);if(!a.some(r=>Bn.has(r.field))){const r=vn(a);E.push(e`${F("id")} ${e.raw(r==="desc"?"DESC":"ASC")}`)}return e.join(E,e`, `)},Et=50,On=(a,c,E)=>{const r={...c};for(const[p,w]of Tt(a)){if(w.serverDefault){r[p]=w.serverDefault({auth:E});continue}r[p]===void 0&&(w.defaultFn?r[p]=w.defaultFn():"defaultValue"in w&&(r[p]=w.defaultValue))}return r},yt=(a,c,E,r)=>{for(const[p,w]of Tt(a)){if(w.serverDefault){p in c&&(E[p]=w.serverDefault({auth:r}));continue}w.onUpdateFn&&!(p in c)&&(E[p]=w.onUpdateFn())}},Wn=128,Un=()=>{const a=new Map;return{delete:c=>{a.delete(c)},get:c=>{const E=a.get(c);if(E!==void 0)return a.delete(c),a.set(c,E),E},set:(c,E)=>{a.has(c)?a.delete(c):qn(a,Wn),a.set(c,E)}}},xn=async(a,c,E,r,p)=>{const w=p.get(r);if(w!==void 0)return w;const d=Object.entries(E.tables).filter(([,g])=>g.shardMode===void 0||g.shardMode.kind==="global").map(([g])=>g),L=await Promise.all(d.map(async g=>({found:(await m(a,c,e`SELECT 1 FROM ${e.identifier(g)} WHERE ${e.identifier("id")} = ${r} LIMIT 1`)).length>0,tableName:g})));for(const g of L)if(g.found)return p.set(r,g.tableName),g.tableName},te=a=>a==null?null:Number(a),Pn=(a,c,E)=>E.map(r=>{const p={};for(const w of c){const d=a.shape[w];p[w]=In(r[w]??null,d&&vt(d))}return{key:p,value:te(r.value)}}),Hn=(a,c,E)=>[...a.partitionBy??[],...a.sortBy.map(p=>p.field),...a.where?Object.keys(a.where):[]].every(p=>c[p]===E[p]),bt=a=>a.length===1?a[0]:e`(${e.join(a,e` AND `)})`,Z=a=>e.join(a.map(c=>e`${e.identifier(c)}`),e`, `),ne=a=>e.join(a.map(c=>e`${c}`),e`, `),Kn=(a,c,E,r,p)=>{const w=[];for(let d=0;d<E.length+1;d+=1){const L=E[d],g=c.sortBy[d],T=d<E.length&&L!==void 0&&g!==void 0,b=g?.direction==="desc"?"desc":"asc",C=T?St(L,r[L],b,!1):e`${e.identifier(pe)} < ${p}`;if(C===void 0)continue;const N=E.slice(0,d).map(W=>Ke(a,e`${e.identifier(W)}`,r[W]));N.push(C),w.push(bt(N))}return w.length>0?e.join(w,e` OR `):e`1 = 0`},jn=(a,c,E)=>{if(E.length!==c.length)return;const r=[];for(const[p,w]of c.entries()){const d=St(w.column,E[p],w.direction,!0);if(d===void 0)continue;const L=[];for(let g=0;g<p;g+=1){const T=c[g];T!==void 0&&L.push(Ke(a,e`${e.identifier(T.column)}`,E[g]))}L.push(d),r.push(bt(L))}return r.length===0?e`(1 = 0)`:e`(${e.join(r,e` OR `)})`},Gn=(a,c)=>{if(a.sortBy.length===0)throw new v("INTERNAL",`rankIndex "${a.name}" requires at least one "sortBy" column for stable pagination`);const E=[{column:"__partition__",direction:"asc",nullable:!1}];for(const[r,p]of a.sortBy.entries())E.push({column:c[r]??ye(r),direction:p.direction,nullable:!0});return E.push({column:pe,direction:"asc",nullable:!1}),E},Vn=async(a,c,E,r,p)=>{const d=[];for(let b=0;b<p.length;b+=50)d.push(p.slice(b,b+50));const L=await Promise.all(d.map(async b=>m(a,c,e`SELECT * FROM ${e.identifier(r)} WHERE ${e.identifier("id")} IN (${ne(b)})`))),g=new Map;for(const b of L)for(const C of b)g.set(C.id,C);const T=[];for(const b of p){const C=ee(E,g.get(b));C&&T.push(C)}return T},Yn=a=>{const c=JSON.stringify(a),E=new TextEncoder().encode(c);let r="";for(const p of E)r+=String.fromCodePoint(p);return Sn+btoa(r)},It=a=>{if(a===void 0)return!1;const c=vt(a);return c==="any"||c==="bigint"||c==="from"||c==="union"},Pe=(a,c,E)=>{if(It(a.shape[c]))throw new v("BAD_REQUEST",`${E}: "${c}" may hold an order-preserving key rather than a value SQL can reduce or group — declare an aggregateIndex covering this (by, field, op) so the maintained companion answers it instead (its running total is a REAL, so it stays exact only while the total is inside 2^53)`)},Xn=(a,c,E,r)=>{for(const p of E)Pe(a,p,`groupBy(${c}, { by: [..."${p}"] })`);r.field!==void 0&&Pe(a,r.field,`groupBy(${c}, { agg: { op: "${r.op}", field: "${r.field}" } })`)},He="__cdc_sweep",Ct="global",zn=6e4,Qn=1e4,Jn=3e4;let pt=0;const Zn=async(a,c)=>{const{autoincrementPrimaryKey:E,key:r,real:p,text:w}=c.companionTypes;await M(a,c,e`CREATE TABLE IF NOT EXISTS ${e.identifier(P)} (${e.identifier("seq")} ${e.raw(E)}, ${e.identifier("ts")} ${e.raw(p)} NOT NULL, ${e.identifier("table")} ${e.raw(r)} NOT NULL, ${e.identifier("id")} ${e.raw(r)} NOT NULL, ${e.identifier("op")} ${e.raw(r)} NOT NULL, ${e.identifier("doc")} ${e.raw(w)})`);const d=c.indexKeyPrefix?.("string");await Nn(a,c,{columns:e`${d===void 0?e`${e.identifier("table")}`:e`${e.identifier("table")}(${e.raw(String(d))})`}, ${e.identifier("seq")}`,name:mn,table:P,unique:!1}),await M(a,c,e`CREATE TABLE IF NOT EXISTS ${e.identifier(He)} (${e.identifier("id")} ${e.raw(r)} NOT NULL PRIMARY KEY, ${e.identifier("lease_until")} ${e.raw(p)} NOT NULL)`),await M(a,c,e`INSERT INTO ${e.identifier(He)} (${e.identifier("id")}, ${e.identifier("lease_until")}) VALUES (${Ct}, ${0}) ${c.name==="mysql"?e`ON DUPLICATE KEY UPDATE ${e.identifier("id")} = ${e.identifier("id")}`:e`ON CONFLICT(${e.identifier("id")}) DO NOTHING`}`)},ei=async(a,c,E)=>{const r=e`UPDATE ${e.identifier(He)} SET ${e.identifier("lease_until")} = ${E+zn} WHERE ${e.identifier("id")} = ${Ct} AND ${e.identifier("lease_until")} <= ${E}`;if(c.supportsReturning)return(await m(a,c,e`${r} RETURNING ${e.identifier("id")}`)).length>0;const p=await M(a,c,r);return(c.affectedRows?c.affectedRows(p??{rowsAffected:0}):0)>0},ti=async(a,c,E,r)=>{if(!await ei(a,c,r))return;const p=r-E;await M(a,c,e`
1
+ import{LunoraError as v}from"@lunora/errors";import{m as xt,D as Pt}from"./ctx-db-search-state-DGbsWTp6.mjs";import{c as Ht,r as Kt,A as jt,C as Gt,R as Vt,a as Yt,T as Xt,x as zt,b as Qt}from"./ctx-db-search-Dxuv99A0.mjs";import{d as gi}from"./ctx-db-search-Dxuv99A0.mjs";import{literalInList as Jt,throwingScheduler as Zt,CDC_LOG_TABLE as P,cdcForkedError as en,cursorBelowRetainedFloor as tn,cdcTrimmedError as nn,assertNoExplicitUndefined as nt,hasTrigger as on,runRowValidators as De,assertFlatPredicate as it,mergeWhere as B,rankTableName as we,sortColumnName as ye,resolveRankPartition as ot,encodePartitionKey as Ae,resolveRankSeekTuple as rn,RANK_TIEBREAK as pe,CountRlsUnsupportedError as rt,stripReservedPatchFields as sn,normalizeIdStructurally as an,assertValidClientId as cn,aggregateSqlFunction as $e,softDeleteScope as _e,compileWhereSql as he,normalizeOrderKeys as dn,uniqueIndexFields as ln,equalityPinnedFields as un,buildSeekWhere as fn,decodeCursor as wn,resolveRelationPredicates as st,resolveWith as at,relationHooks as ct,applySelect as dt,encodeCursor as $n,NotFoundError as _n,applyOnDelete as hn,ConflictError as Le,normalizeCountArgument as En,selectIndexForCount as yn,encodeAggregateKey as Y,aggregateTableName as Q,selectIndexForAggregate as pn,readAggregateValue as Be,runTriggers as Rn,matchesRankStaticWhere as lt,rankPivotConditionSql as St,CURSOR_PREFIX as Sn,selectIndexForGroupBy as gn,tiebreakDirectionFor as vn,fanOutScalarCounts as Tn,matchesStaticWhere as Fe,foldAggregateTally as ut,coerceAggregateNumber as Oe,CDC_LOG_TABLE_SEQ_INDEX as mn,NotUniqueError as bn}from"@lunora/shard-engine";import{sql as e}from"drizzle-orm";import{d as gt,b as In,e as vt,n as Cn,c as An}from"./value-codec-DC_dHxUY.mjs";import{runSqlGlobalTableMigrations as Ln,runSqlAggregateMigrations as kn,runSqlRankMigrations as Mn}from"./runSqlAggregateMigrations-CmsPHIJb.mjs";import{queryAll as m,queryRun as M,createIndexIfNotExists as Nn,decodeRow as ee,serializeDocumentColumn as J,OCC_VERSION_COLUMN as Ee,columnRefSql as F,decodeRows as ft,tableColumns as Tt,nextRowVersion as wt,serializeColumnValue as We,forEachRowPaged as $t,BACKFILL_BATCH_SIZE as Ue,nullSafeEqualsSql as Ke,queryBatch as _t}from"./decodeGlobalRow-VOl_TIbk.mjs";import{decodeGlobalRow as Ti}from"./decodeGlobalRow-VOl_TIbk.mjs";const qn=(a,c)=>{if(a.size<c)return;const E=a.keys().next().value;E!==void 0&&a.delete(E)},ht=50,Dn=(a,c={})=>{const E=c.maxBatch??ht,r=Number.isInteger(E)&&E>0?E:ht,p=new Map;let w=!1;const d=async(g,T)=>{const b=[...T.keys()];for(let C=0;C<b.length;C+=r){const N=b.slice(C,C+r);try{const W=await a(g,N);for(const G of N){const V=W.get(G);for(const ie of T.get(G)??[])ie.resolve(V)}}catch(W){for(const G of N)for(const V of T.get(G)??[])V.reject(W)}}},L=()=>{w=!1;const g=[...p.entries()];p.clear();for(const[T,b]of g)d(T,b).catch(()=>{})};return{async load(g,T){return new Promise((b,C)=>{let N=p.get(g);N||(N=new Map,p.set(g,N));const W=N.get(T);W?W.push({reject:C,resolve:b}):N.set(T,[{reject:C,resolve:b}]),w||(w=!0,queueMicrotask(L))})}}},Bn=new Set(["_id","id"]),mt=(a,c)=>a.name!=="postgres"||c.nullable!==!0?"":c.direction==="desc"?" NULLS LAST":" NULLS FIRST",Fn=(a,c)=>{const E=a.map(r=>e`${F(r.field)} ${e.raw(`${r.direction==="desc"?"DESC":"ASC"}${mt(c,r)}`)}`);if(!a.some(r=>Bn.has(r.field))){const r=vn(a);E.push(e`${F("id")} ${e.raw(r==="desc"?"DESC":"ASC")}`)}return e.join(E,e`, `)},Et=50,On=(a,c,E)=>{const r={...c};for(const[p,w]of Tt(a)){if(w.serverDefault){r[p]=w.serverDefault({auth:E});continue}r[p]===void 0&&(w.defaultFn?r[p]=w.defaultFn():"defaultValue"in w&&(r[p]=w.defaultValue))}return r},yt=(a,c,E,r)=>{for(const[p,w]of Tt(a)){if(w.serverDefault){p in c&&(E[p]=w.serverDefault({auth:r}));continue}w.onUpdateFn&&!(p in c)&&(E[p]=w.onUpdateFn())}},Wn=128,Un=()=>{const a=new Map;return{delete:c=>{a.delete(c)},get:c=>{const E=a.get(c);if(E!==void 0)return a.delete(c),a.set(c,E),E},set:(c,E)=>{a.has(c)?a.delete(c):qn(a,Wn),a.set(c,E)}}},xn=async(a,c,E,r,p)=>{const w=p.get(r);if(w!==void 0)return w;const d=Object.entries(E.tables).filter(([,g])=>g.shardMode===void 0||g.shardMode.kind==="global").map(([g])=>g),L=await Promise.all(d.map(async g=>({found:(await m(a,c,e`SELECT 1 FROM ${e.identifier(g)} WHERE ${e.identifier("id")} = ${r} LIMIT 1`)).length>0,tableName:g})));for(const g of L)if(g.found)return p.set(r,g.tableName),g.tableName},te=a=>a==null?null:Number(a),Pn=(a,c,E)=>E.map(r=>{const p={};for(const w of c){const d=a.shape[w];p[w]=In(r[w]??null,d&&vt(d))}return{key:p,value:te(r.value)}}),Hn=(a,c,E)=>[...a.partitionBy??[],...a.sortBy.map(p=>p.field),...a.where?Object.keys(a.where):[]].every(p=>c[p]===E[p]),bt=a=>a.length===1?a[0]:e`(${e.join(a,e` AND `)})`,Z=a=>e.join(a.map(c=>e`${e.identifier(c)}`),e`, `),ne=a=>e.join(a.map(c=>e`${c}`),e`, `),Kn=(a,c,E,r,p)=>{const w=[];for(let d=0;d<E.length+1;d+=1){const L=E[d],g=c.sortBy[d],T=d<E.length&&L!==void 0&&g!==void 0,b=g?.direction==="desc"?"desc":"asc",C=T?St(L,r[L],b,!1):e`${e.identifier(pe)} < ${p}`;if(C===void 0)continue;const N=E.slice(0,d).map(W=>Ke(a,e`${e.identifier(W)}`,r[W]));N.push(C),w.push(bt(N))}return w.length>0?e.join(w,e` OR `):e`1 = 0`},jn=(a,c,E)=>{if(E.length!==c.length)return;const r=[];for(const[p,w]of c.entries()){const d=St(w.column,E[p],w.direction,!0);if(d===void 0)continue;const L=[];for(let g=0;g<p;g+=1){const T=c[g];T!==void 0&&L.push(Ke(a,e`${e.identifier(T.column)}`,E[g]))}L.push(d),r.push(bt(L))}return r.length===0?e`(1 = 0)`:e`(${e.join(r,e` OR `)})`},Gn=(a,c)=>{if(a.sortBy.length===0)throw new v("INTERNAL",`rankIndex "${a.name}" requires at least one "sortBy" column for stable pagination`);const E=[{column:"__partition__",direction:"asc",nullable:!1}];for(const[r,p]of a.sortBy.entries())E.push({column:c[r]??ye(r),direction:p.direction,nullable:!0});return E.push({column:pe,direction:"asc",nullable:!1}),E},Vn=async(a,c,E,r,p)=>{const d=[];for(let b=0;b<p.length;b+=50)d.push(p.slice(b,b+50));const L=await Promise.all(d.map(async b=>m(a,c,e`SELECT * FROM ${e.identifier(r)} WHERE ${e.identifier("id")} IN (${ne(b)})`))),g=new Map;for(const b of L)for(const C of b)g.set(C.id,C);const T=[];for(const b of p){const C=ee(E,g.get(b));C&&T.push(C)}return T},Yn=a=>{const c=JSON.stringify(a),E=new TextEncoder().encode(c);let r="";for(const p of E)r+=String.fromCodePoint(p);return Sn+btoa(r)},It=a=>{if(a===void 0)return!1;const c=vt(a);return c==="any"||c==="bigint"||c==="from"||c==="union"},Pe=(a,c,E)=>{if(It(a.shape[c]))throw new v("BAD_REQUEST",`${E}: "${c}" may hold an order-preserving key rather than a value SQL can reduce or group — declare an aggregateIndex covering this (by, field, op) so the maintained companion answers it instead (its running total is a REAL, so it stays exact only while the total is inside 2^53)`)},Xn=(a,c,E,r)=>{for(const p of E)Pe(a,p,`groupBy(${c}, { by: [..."${p}"] })`);r.field!==void 0&&Pe(a,r.field,`groupBy(${c}, { agg: { op: "${r.op}", field: "${r.field}" } })`)},He="__cdc_sweep",Ct="global",zn=6e4,Qn=1e4,Jn=3e4;let pt=0;const Zn=async(a,c)=>{const{autoincrementPrimaryKey:E,key:r,real:p,text:w}=c.companionTypes;await M(a,c,e`CREATE TABLE IF NOT EXISTS ${e.identifier(P)} (${e.identifier("seq")} ${e.raw(E)}, ${e.identifier("ts")} ${e.raw(p)} NOT NULL, ${e.identifier("table")} ${e.raw(r)} NOT NULL, ${e.identifier("id")} ${e.raw(r)} NOT NULL, ${e.identifier("op")} ${e.raw(r)} NOT NULL, ${e.identifier("doc")} ${e.raw(w)})`);const d=c.indexKeyPrefix?.("string");await Nn(a,c,{columns:e`${d===void 0?e`${e.identifier("table")}`:e`${e.identifier("table")}(${e.raw(String(d))})`}, ${e.identifier("seq")}`,name:mn,table:P,unique:!1}),await M(a,c,e`CREATE TABLE IF NOT EXISTS ${e.identifier(He)} (${e.identifier("id")} ${e.raw(r)} NOT NULL PRIMARY KEY, ${e.identifier("lease_until")} ${e.raw(p)} NOT NULL)`),await M(a,c,e`INSERT INTO ${e.identifier(He)} (${e.identifier("id")}, ${e.identifier("lease_until")}) VALUES (${Ct}, ${0}) ${c.name==="mysql"?e`ON DUPLICATE KEY UPDATE ${e.identifier("id")} = ${e.identifier("id")}`:e`ON CONFLICT(${e.identifier("id")}) DO NOTHING`}`)},ei=async(a,c,E)=>{const r=e`UPDATE ${e.identifier(He)} SET ${e.identifier("lease_until")} = ${E+zn} WHERE ${e.identifier("id")} = ${Ct} AND ${e.identifier("lease_until")} <= ${E}`;if(c.supportsReturning)return(await m(a,c,e`${r} RETURNING ${e.identifier("id")}`)).length>0;const p=await M(a,c,r);return(c.affectedRows?c.affectedRows(p??{rowsAffected:0}):0)>0},ti=async(a,c,E,r)=>{if(!await ei(a,c,r))return;const p=r-E;await M(a,c,e`
2
2
  DELETE FROM ${e.identifier(P)} WHERE ${e.identifier("seq")} IN (
3
3
  SELECT ${e.identifier("seq")} FROM (
4
4
  SELECT ${e.identifier("seq")} FROM ${e.identifier(P)} WHERE ${e.identifier("ts")} <= ${p} ORDER BY ${e.identifier("seq")} ASC LIMIT ${e.raw(String(Qn))}
@@ -0,0 +1 @@
1
+ import{LunoraError as p}from"@lunora/errors";import{D as I,c as vt,m as Dt,r as U,a as qt,l as lt,w as F,b as Ft}from"./ctx-db-search-state-DGbsWTp6.mjs";import{ftsUnmappedPage as Ut,groupUnmappedRows as bt,ftsWriteDocument as mt,ftsPurgeDocument as ht,ftsCompanionDdl as kt,unionAll as Ht,ftsRowidMapName as Pt}from"@lunora/shard-engine";import{sql as i}from"drizzle-orm";import{queryAll as h,OCC_VERSION_COLUMN as Bt,nullSafeEqualsSql as Wt,decodeRow as V,runInOrder as O,indexState as L,createIndexIfNotExists as X,dropIndexIfExists as M,queryRun as T,decodeRows as pt,columnRefSql as rt,serializeColumnValue as jt,readRowsPage as xt,forEachRowPaged as Kt}from"./decodeGlobalRow-VOl_TIbk.mjs";const Gt=n=>{const t=atob(n),e=Uint8Array.from(t,r=>r.codePointAt(0)??0);return new TextDecoder().decode(e)},Yt=()=>new p("BAD_REQUEST","invalid cursor"),ot=16,st=8,y=1024,b=(n,t)=>t.query(n),Qt=(n,t)=>{if(!t)return{exact:!0,lower:n,upper:n};const e=[...n].at(-1)??"",r=(e.codePointAt(0)??0)+1;if(r>=55296&&r<=57343||r>1114111)return{exact:!0,lower:n,upper:n};const o=n.slice(0,n.length-e.length);return{exact:!1,lower:n,upper:o+String.fromCodePoint(r)}},Cn=(n,t,e)=>{const r={eq:(o,s)=>{if(!n.definition.filterFields?.includes(o))throw new p("INTERNAL",`field "${o}" is not a filter field of search index "${n.indexName}" on table "${t}"`);if(n.filters.length>=st)throw new p("BAD_REQUEST",`search index "${n.indexName}" on table "${t}": at most ${String(st)} .eq() filters are supported per search query`);return n.filters.push({field:o,value:s}),r},search:(o,s)=>{const a=n;if(o!==a.definition.field)throw new p("INTERNAL",`search index "${a.indexName}" on table "${t}" indexes "${a.definition.field}", not "${o}"`);const f=b(s,e).length;if(f>ot)throw new p("BAD_REQUEST",`search index "${a.indexName}" on table "${t}": at most ${String(ot)} search terms are supported (got ${String(f)})`);return a.field=o,a.query=s,a.hasQuery=!0,r}};return r},Ln=n=>{if(n.length>y)throw new p("BAD_REQUEST",`more than ${String(y)} documents match this search — narrow it with filters or read it a page at a time with .paginate()`)},Mn=n=>Math.min(n.offset+n.numItems+1,y),Vt=n=>btoa(`search:${String(n)}`),Xt=n=>{let t;try{t=Gt(n)}catch{return}if(!t.startsWith("search:"))return;const e=Number(t.slice(7));return Number.isInteger(e)&&e>=0?e:void 0},vn=n=>{if(typeof n.endCursor=="string")throw new p("BAD_REQUEST","bounded (endCursor) pagination is not supported on search queries — relevance order has no stable range boundary");if(!Number.isFinite(n.numItems))throw new p("BAD_REQUEST",`search pagination needs a finite numItems, got ${String(n.numItems)}`);const t=Math.max(0,Math.floor(n.numItems)),e=n.cursor?Xt(n.cursor):0;if(e===void 0)throw Yt();if(e+t>=y)throw new p("BAD_REQUEST",`search pagination reaches the ${String(y)}-document limit (offset ${String(e)} + ${String(t)} requested) — a page must end below the cap so the probe row that answers \`hasMore\` still fits: retry with numItems ${String(Math.max(1,y-e-1))} or fewer, or narrow the query or the filters instead`);return{numItems:t,offset:e}},Dn=(n,t)=>{const e=t.offset+t.numItems,r=t.numItems>0&&n.length>e;return{continueCursor:r?Vt(e):null,isDone:!r,page:n.slice(t.offset,e)}},qn=n=>{if(n===void 0)return y+1;if(!Number.isFinite(n))return y;const t=Math.max(0,Math.floor(n));if(t>y)throw new p("BAD_REQUEST",`search returns at most ${String(y)} documents (asked for ${String(t)}) — narrow the query or paginate instead`);return t},Jt=(n,t)=>{if(!t.includes("."))return n[t];let e=n;for(const r of t.split(".")){if(e===null||typeof e!="object"||Array.isArray(e))return;e=e[r]}return e},k=(n,t)=>`${n}__fts_${t}`,S="__id__",g="__token__",A="__n__",zt=1e3,gt=(n,t)=>t.document(n),x=(n,t)=>Jt(n,t),Zt=n=>typeof n=="string"?n:n==null?"":typeof n=="number"||typeof n=="bigint"||typeof n=="boolean"?String(n):JSON.stringify(n)??"",tn=(n,t,e)=>n!==void 0&&t!==void 0&&x(n,e.field)===x(t,e.field),nn=(n,t)=>gt(Zt(x(n,t.field)),I(t.language)).slice(0,zt),q=(n,t)=>nn(n,t).join(" "),Tt=(n,t)=>{const e=new Map;for(const r of gt(n,t))e.set(r,(e.get(r)??0)+1);return e},yt=[Bt,"_creationTime"],at=50,$=(n,t)=>i`${i.identifier(n)}.${i.identifier(t)}`,v=(n,t,e)=>i`DELETE FROM ${i.identifier(n)} WHERE ${$(n,S)} = ${t}${e===void 0?i``:i` AND ${e}`}`,Rt=(n,{row:t,table:e})=>{const r=yt.map(o=>i` AND ${Wt(n.name,$(e,o),t[o]??null)}`);return i`SELECT 1 FROM ${i.identifier(e)} WHERE ${$(e,"id")} = ${t.id}${i.join(r)}${n.name==="postgres"?i` FOR SHARE`:i``}`},D=(n,t)=>i`EXISTS (${Rt(n,t)})`,H=(n,t)=>i`NOT EXISTS (SELECT 1 FROM ${i.identifier(n)} WHERE ${$(n,"id")} = ${t})`,_t=(n,t,e,r)=>r===void 0?H(t,e):D(n,{row:r,table:t}),J=async(n,t,e,r)=>{const o=new Map;for(let s=0;s<r.length;s+=at){const a=r.slice(s,s+at),f=await h(n,t,i`SELECT * FROM ${i.identifier(e)} WHERE ${$(e,"id")} IN (${i.join(a.map(u=>i`${u}`),i`, `)})`);for(const u of f)o.set(String(u.id),u)}return o},K=(n,t,e)=>n.name==="mysql"?i`${i.identifier(t)}(${i.raw(String(e))})`:n.textPatternOperatorClass===void 0?i`${i.identifier(t)}`:i`${i.identifier(t)} ${i.raw(n.textPatternOperatorClass)}`,z=512,Z=(n,t)=>({columns:i`${K(n,g,256)}, ${K(n,S,z)}`,concurrently:!0,name:`${t}__unique`,table:t,unique:!0}),W=n=>i`${n} COLLATE utf8mb4_0900_bin`,j=(n,t)=>{const e=JSON.stringify(t.map(([r,o])=>({o,t:r})));switch(n.name){case"mysql":return i`JSON_TABLE(${e}, '$[*]' COLUMNS (${i.identifier("t")} VARCHAR(256) PATH '$.t', ${i.identifier("o")} INT PATH '$.o')) AS ${i.identifier("j")}`;case"postgres":return i`json_to_recordset(${e}::json) AS ${i.identifier("j")}(${i.identifier("t")} text, ${i.identifier("o")} integer)`;default:return i`(SELECT json_extract(${$("e","value")}, '$.t') AS ${i.identifier("t")}, json_extract(${$("e","value")}, '$.o') AS ${i.identifier("o")} FROM json_each(${e}) AS ${i.identifier("e")}) AS ${i.identifier("j")}`}},Nt=(n,t,e,r,o)=>{const s=i.join([g,S,A].map(f=>i.identifier(f)),i`, `),a=f=>i`INSERT INTO ${i.identifier(t)} (${s}) SELECT ${$("j","t")}, ${e}, ${$("j","o")} FROM ${j(n,r)} WHERE ${f}`;switch(n.name){case"mysql":{const f=D(n,o),u=$(t,g),c=i.identifier("held");return[i`${a(i`${f} AND NOT EXISTS (SELECT 1 FROM ${i.identifier(t)} AS ${c} WHERE ${$("held",S)} = ${e} AND ${$("held",g)} = ${W($("j","t"))})`)} ON DUPLICATE KEY UPDATE ${$(t,A)} = ${$(t,A)}`,i`UPDATE ${i.identifier(t)} JOIN ${j(n,r)} ON ${u} = ${W($("j","t"))} SET ${$(t,A)} = ${$("j","o")} WHERE ${$(t,S)} = ${e} AND ${f}`,i`DELETE FROM ${i.identifier(t)} WHERE ${$(t,S)} = ${e} AND ${f} AND ${u} NOT IN (SELECT ${W($("j","t"))} FROM ${j(n,r)})`]}case"postgres":{const f=i.identifier("__source__"),u=i.identifier("__purged__"),c=i`EXISTS (SELECT 1 FROM ${f})`;return[i`WITH ${f} AS (${Rt(n,o)}), ${u} AS (${v(t,e,c)} RETURNING 1) ${a(i`${c} AND (SELECT COUNT(*) FROM ${u}) >= 0`)} ON CONFLICT DO NOTHING`]}default:{const f=D(n,o);return[v(t,e,f),i`${a(f)} ON CONFLICT DO NOTHING`]}}},At={uniqueKeyMissing:!1,unmappedSkipped:0},ft=new Set,ut=(n,t)=>{ft.has(n)||(ft.add(n),console.warn(`[@lunora/sql-store] ${t}`))},ct=100,dt=3,G=async(n,t,e,r=0)=>{const{companion:o,definition:s,index:a,tableName:f}=e,u={done:!0,last:r,left:0};if((await h(n,t,t.tableExists(f))).length===0)return u;const d=await h(n,t,Ut(o,ct,r));if(d.length===0)return u;const w=Math.max(...d.map(_=>Number(_.rowid))),E=bt(d),l=await J(n,t,f,[...E.keys()]),m=[];for(const[_,nt]of E){const B=l.get(_),et=B===void 0?void 0:V(s,B),it=_t(t,f,_,B);m.push(...et?mt(o,_,q(et,a),{guard:it,unmappedRowids:nt}):ht(o,_,{guard:it,unmappedRowids:nt}))}await O(n,t,m);const R=await h(n,t,i`SELECT COUNT(*) AS ${i.identifier("n")} FROM ${i.identifier(o)} WHERE ${$(o,"rowid")} > ${Math.max(0,r)} AND ${$(o,"rowid")} <= ${w}`);return{done:d.length<ct,last:w,left:Number(R[0]?.n??0)}},en=async(n,t,e)=>{let r=0,o=0;for(;;){let s=await G(n,t,e,r);for(let a=1;s.left>0&&a<dt;a+=1)s=await G(n,t,e,r);if(s.left>0&&(o+=s.left,console.warn(`[@lunora/sql-store] search migration of "${e.companion}": ${String(s.left)} row(s) lost every one of ${String(dt)} attempts to a concurrent write and were left for a later pass.`)),s.done)return o;r=s.last}},rn=async(n,t,e,r)=>r==="step"?(await G(n,t,e),At):{uniqueKeyMissing:!1,unmappedSkipped:await en(n,t,e)},Y=n=>`${n}__btree`,on=async(n,t,e)=>{const r=Z(t,e),o=await L(n,t,e,r.name);if(o!=="valid"){if(o==="absent"&&(await h(n,t,i`SELECT 1 AS ${i.identifier("present")} FROM ${i.identifier(e)} LIMIT 1`)).length===0)try{await X(n,t,{...r,concurrently:!1}),await M(n,t,e,Y(e));return}catch(a){if(await L(n,t,e,r.name).catch(()=>{})==="valid")return;ut(`${e}:empty`,`search companion "${e}": adding its unique key failed (${String(a)}); run backfillSqlSearchIndexes.`);return}ut(`${e}:missing`,`search companion "${e}" has no usable unique key, so two backfills racing over one row can index it twice. Run backfillSqlSearchIndexes, which adds it.`)}},sn=async(n,t,e)=>{const r=$(e,S),o=$(e,g);return(await h(n,t,i`SELECT ${r} AS ${i.identifier("id")}, ${o} AS ${i.identifier("token")}, COUNT(*) AS ${i.identifier("n")} FROM ${i.identifier(e)} GROUP BY ${r}, ${o} HAVING COUNT(*) > 1`)).map(a=>({count:Number(a.n),id:String(a.id),token:String(a.token)}))},an=(n,t,{count:e,id:r,token:o})=>{const s=i`${$(t,S)} = ${r} AND ${$(t,g)} = ${o}`;switch(n.name){case"mysql":return i`DELETE FROM ${i.identifier(t)} WHERE ${s} LIMIT ${i.raw(String(e-1))}`;case"postgres":return i`DELETE FROM ${i.identifier(t)} WHERE ctid = ANY (ARRAY(SELECT ctid FROM ${i.identifier(t)} WHERE ${s} OFFSET 1))`;default:return i`DELETE FROM ${i.identifier(t)} WHERE rowid IN (SELECT rowid FROM ${i.identifier(t)} WHERE ${s} LIMIT -1 OFFSET 1)`}},fn=async(n,t,e,r)=>{const{companion:o,definition:s,index:a,tableName:f}=e;for(const d of r)await T(n,t,an(t,o,d));const u=[...new Set(r.map(d=>d.id))],c=await J(n,t,f,u);for(const d of u){const w=c.get(d),E=w===void 0?void 0:V(s,w);if(w!==void 0&&E){const l=[...Tt(q(E,a),I(a.language))];await O(n,t,Nt(t,o,d,l,{row:w,table:f}));continue}await T(n,t,v(o,d,_t(t,f,d,w)))}},un=async(n,t,e)=>t.name!=="mysql"?"":(await h(n,t,i`SELECT COLUMN_NAME AS ${i.identifier("name")}, COLLATION_NAME AS ${i.identifier("collation")} FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ${e} AND COLUMN_NAME IN (${g}, ${S})`)).filter(o=>!String(o.collation).endsWith("_bin")).map(o=>` Its ${String(o.name)} column uses ${String(o.collation)}, which treats values differing only in case or accents as one; convert it with ALTER TABLE \`${e}\` MODIFY \`${String(o.name)}\` VARCHAR(768) COLLATE utf8mb4_0900_bin NOT NULL.`).join(""),cn=async(n,t,e)=>{if(t.name!=="mysql")return 0;const r=$(e,S),o=await h(n,t,i`SELECT COUNT(*) AS ${i.identifier("n")} FROM (SELECT 1 AS ${i.identifier("x")} FROM ${i.identifier(e)} GROUP BY LEFT(${r}, ${i.raw(String(z))}), ${$(e,g)} HAVING COUNT(DISTINCT ${r}) > 1) AS ${i.identifier("shared")}`);return Number(o[0]?.n??0)},dn=async(n,t,e)=>{const r=Z(t,e),o=await L(n,t,e,r.name);if(o==="valid")return"valid";o==="invalid"&&await M(n,t,e,r.name);try{return await X(n,t,r),"built"}catch(s){const a=await L(n,t,e,r.name);if(a==="valid")return"valid";if(a==="invalid"&&await M(n,t,e,r.name),!t.isUniqueViolation(s))throw s;return"refused"}},$t=3,$n=async(n,t,e)=>{const{companion:r}=e,o=await un(n,t,r);let s="";o!==""&&console.warn(`[@lunora/sql-store] search companion "${r}" folds case and accents in its unique key.${o}`);for(let a=0;a<$t;a+=1){const f=await dn(n,t,r);if(f==="valid")return await M(n,t,r,Y(r)),!0;if(f==="refused"){const u=await sn(n,t,r);if(u.length>0)await fn(n,t,e,u);else{const c=await cn(n,t,r);s=c>0?` ${String(c)} token(s) are held by document ids that agree on their first ${String(z)} characters, which the key cannot tell apart; shorten those ids.`:o}}}return await L(n,t,r,Z(t,r).name)==="valid"?(await M(n,t,r,Y(r)),!0):(console.warn(`[@lunora/sql-store] search companion "${r}" still has no unique key after ${String($t)} attempts: writes keep working, but two backfills racing over one row can index it twice.${s}`),!1)},wn=async(n,t,e,r)=>r==="step"?At:{uniqueKeyMissing:!await $n(n,t,e),unmappedSkipped:0},Sn=(n,t)=>t?i`${i.identifier(g)} LIKE ${`${n}%`}`:i`${i.identifier(g)} = ${n}`,Ot=(n,t)=>{const e=t.filters.map(r=>i`m.${rt(r.field)} = ${jt(r.value)}`);return n.softDeleteMode&&e.push(i`m.${rt(n.softDeleteMode.field)} IS NULL`),e},It=async(n,t,e,r,o,s,a)=>{const f=Ot(e,o);let u=i`SELECT m.* FROM (${a}) s JOIN ${i.identifier(r)} m ON m.${i.identifier("id")} = s.${i.identifier(S)}`;return f.length>0&&(u=i`${u} WHERE ${i.join(f,i` AND `)}`),u=i`${u} ORDER BY s.${i.identifier("__score__")} DESC, m.${i.identifier("_creationTime")} DESC, m.${i.identifier("id")} ASC LIMIT ${i.raw(String(s))}`,pt(e,await h(n,t,u))},En=async(n,t,e,r,o,s)=>{const a=b(o.query,I(o.definition.language));if(a.length===0)return[];const f=k(r,o.indexName),u=a.length-1,c=a.map((m,R)=>Sn(m,R===u)),d=i.join(c,i` OR `),w=c.map(m=>i`SUM(CASE WHEN ${m} THEN ${i.identifier(A)} ELSE 0 END)`),E=i.join(w.map(m=>i`${m} > 0`),i` AND `),l=i`SELECT ${i.identifier(S)}, ${i.join(w,i` + `)} AS ${i.identifier("__score__")} FROM ${i.identifier(f)} WHERE ${d} GROUP BY ${i.identifier(S)} HAVING ${E}`;return It(n,t,e,r,o,s,l)},ln=async(n,t,e,r,o,s)=>{const a=b(o.query,I(o.definition.language));if(a.length===0)return[];const f=k(r,o.indexName),u=`${f}__vocab`,c=a.length-1,d=a.map((l,m)=>{const R=Qt(l,m===c),_=R.exact?i`${i.identifier("term")} = ${R.lower}`:i`${i.identifier("term")} >= ${R.lower} AND ${i.identifier("term")} < ${R.upper}`;return i`SELECT ${i.identifier("doc")}, ${i.raw(String(m))} AS ${i.identifier("__term__")}, COUNT(*) AS ${i.identifier("__n__")} FROM ${i.identifier(u)} WHERE ${_} GROUP BY ${i.identifier("doc")}`}),w=a.map((l,m)=>i`SUM(CASE WHEN u.${i.identifier("__term__")} = ${i.raw(String(m))} THEN u.${i.identifier("__n__")} ELSE 0 END)`),E=i`SELECT f.${i.identifier(S)} AS ${i.identifier(S)}, ${i.join(w,i` + `)} AS ${i.identifier("__score__")} FROM (${Ht(d)}) u JOIN ${i.identifier(f)} f ON f.rowid = u.${i.identifier("doc")} GROUP BY f.${i.identifier(S)} HAVING ${i.join(w.map(l=>i`${l} > 0`),i` AND `)}`;return It(n,t,e,r,o,s,E)},mn=async(n,t,e,r,o,s)=>{const a=t.nativeTextSearch,f=b(o.query,I(o.definition.language));if(!a||f.length===0)return[];const u=k(r,o.indexName),c=[a.matches(u,f),...Ot(e,o)],d=i`SELECT m.* FROM ${i.identifier(u)} JOIN ${i.identifier(r)} m ON m.${i.identifier("id")} = ${i.identifier(u)}.${i.identifier(S)} WHERE ${i.join(c,i` AND `)} ORDER BY ${a.rank(u,f)} DESC, m.${i.identifier("_creationTime")} DESC, m.${i.identifier("id")} ASC LIMIT ${i.raw(String(s))}`;return pt(e,await h(n,t,d))},hn={ensureCompanion:async(n,t,e)=>{const{integer:r,key:o}=t.companionTypes;await T(n,t,i`CREATE TABLE IF NOT EXISTS ${i.identifier(e)} (${i.identifier(g)} ${i.raw(o)} NOT NULL, ${i.identifier(S)} ${i.raw(o)} NOT NULL, ${i.identifier(A)} ${i.raw(r)} NOT NULL)`),await X(n,t,{columns:K(t,S,191),name:`${e}__by_id`,table:e,unique:!1}),await on(n,t,e)},indexDocument:async(n,t,e,r,o,s)=>{const a=[...Tt(q(r,o),I(o.language))];await O(n,t,Nt(t,e,String(s.row.id),a,s))},migrate:wn,name:"inverted",purgeDocument:async(n,t,e,r,o)=>{await T(n,t,v(e,r,H(o,r)))},runSearch:En},pn={ensureCompanion:async(n,t,e)=>{await O(n,t,kt(e))},indexDocument:async(n,t,e,r,o,s)=>{await O(n,t,mt(e,String(s.row.id),q(r,o),{guard:D(t,s)}))},migrate:rn,name:"fts5",purgeDocument:async(n,t,e,r,o)=>{await O(n,t,ht(e,r,{guard:H(o,r)}))},runSearch:ln},gn={ensureCompanion:async(n,t,e)=>{const r=t.nativeTextSearch;if(r){await T(n,t,r.createCompanion(e,t.companionTypes.key));for(const o of r.createIndexes(e))await T(n,t,o)}},indexDocument:async(n,t,e,r,o,s)=>{const a=t.nativeTextSearch;a&&await T(n,t,a.indexDocument(e,String(s.row.id),q(r,o),D(t,s)))},name:"native",purgeDocument:async(n,t,e,r,o)=>{await T(n,t,v(e,r,H(o,r)))},runSearch:mn},C=(n,t)=>n.strategy==="native"&&t.nativeTextSearch!==void 0?gn:t.supportsFts5?pn:hn,P=(n,t)=>`${vt(n)}/${C(n,t).name}`,tt=function*(n){for(const[t,e]of Object.entries(n.tables)){const r=e.searchIndexes;if(!(e.shardMode!==void 0&&e.shardMode.kind!=="global"||!r))for(const o of r)yield[t,e,o]}},N=(n,t)=>k(n,t.name),Tn=async(n,t,e,r)=>{const o=N(e,r);return lt(await U(n,t,o),P(r,t)).finished?!0:Ft(n,t,o)},Fn=async(n,t,e,r,o,s)=>{if(!await Tn(n,t,r,o.definition))throw new p("SEARCH_INDEX_BUILDING",`search index "${o.indexName}" on table "${r}" is still backfilling and currently covers only part of the table — retry once it finishes${t.searchBackfillHint===void 0?"":`, or complete it now: ${t.searchBackfillHint}`}`);return C(o.definition,t).runSearch(n,t,e,r,o,s)},wt=n=>n.slice(n.lastIndexOf("/")+1),St=200,Et=5,yn=async(n,t,e,r)=>{const o=[...r.keys()],s=await h(n,t,i`SELECT * FROM ${i.identifier(e)} WHERE ${i.identifier("id")} >= ${o[0]} AND ${i.identifier("id")} <= ${o.at(-1)} ORDER BY ${i.identifier("id")} ASC`),a=new Map;for(const f of s){const u=f.id,c=r.get(u)?.row;c&&yt.some(d=>String(c[d])!==String(f[d]))&&a.set(u,{row:f})}return a},Q=async(n,t,e,r,o,s,a)=>{const f=C(s,t);let u=a;for(let c=0;u.size>0&&c<Et;c+=1){for(const{document:d,row:w}of u.values()){const E=d??V(e,w);E&&await f.indexDocument(n,t,o,E,s,{row:w,table:r})}u=await yn(n,t,r,u)}u.size>0&&console.warn(`[@lunora/sql-store] search index "${o}": ${String(u.size)} row(s) were rewritten on every one of ${String(Et)} re-check rounds and were left to their writers — each stays stale only if none of those writes changed its indexed text.`)},Ct=async(n,t,e,r,o)=>{const s=N(r,o),a=P(o,t),f=lt(await U(n,t,s),a);if(f.finished)return!0;if(f.wipe&&await F(n,t,s,void 0,!1,a),(await h(n,t,t.tableExists(r))).length===0)return await F(n,t,s,void 0,!0,a),!0;const c=await xt(n,t,r,f.cursor,St),d=c.findLast(l=>typeof l.id=="string")?.id,w=new Map;for(const l of c)typeof l.id=="string"&&w.set(l.id,{row:l});await Q(n,t,e,r,s,o,w);const E=c.length<St;return await F(n,t,s,d??f.cursor,E,a),E},Lt=async(n,t,e)=>{await Dt(n,e);for(const[r,,o]of tt(t)){const s=N(r,o),a=P(o,e),f=await U(n,e,s);f.profile!==void 0&&wt(f.profile)!==wt(a)&&(await T(n,e,i`DROP TABLE IF EXISTS ${i.identifier(s)}`),await T(n,e,i`DROP TABLE IF EXISTS ${i.identifier(Pt(s))}`),await qt(n,e,s)),await C(o,e).ensureCompanion(n,e,s)}},Rn=async(n,t,e,r,o)=>{const s=N(r,o);if((await U(n,t,s)).profile!==void 0)return;let f=0;(await h(n,t,t.tableExists(r))).length>0&&await Kt(n,t,e,r,()=>{f+=1},{limit:1}),await F(n,t,s,void 0,f===0,P(o,t))},Mt=async(n,t,e,r,o,s)=>await C(o,t).migrate?.(n,t,{companion:N(r,o),definition:e,index:o,tableName:r},s)??{uniqueKeyMissing:!1,unmappedSkipped:0},Un=async(n,t,e)=>{await Lt(n,t,e);for(const[r,o,s]of tt(t)){if(await Mt(n,e,o,r,s,"step"),s.staged){await Rn(n,e,o,r,s);continue}await Ct(n,e,o,r,s)}},bn=async(n,t,e)=>{const r={uniqueKeyMissing:[],unmappedSkipped:0};await Lt(n,t,e);for(const[o,s,a]of tt(t)){const f=await Mt(n,e,s,o,a,"drain");r.unmappedSkipped+=f.unmappedSkipped,f.uniqueKeyMissing&&r.uniqueKeyMissing.push(N(o,a));let u=!1;for(;!u;)u=await Ct(n,e,s,o,a)}return r},kn=n=>{const{dialect:t,exec:e,schema:r}=n;return async(o,s,a)=>{const f=r.tables[o],u=f?.searchIndexes;if(!(!f||!u||u.length===0))for(const c of u){if(tn(a?.previous,a?.document,c))continue;const d=N(o,c);if(a){await Q(e,t,f,o,d,c,new Map([[s,{document:a.document,row:{...a.written,id:s}}]]));continue}await C(c,t).purgeDocument(e,t,d,s,o);const E=(await J(e,t,o,[s])).get(s);E!==void 0&&await Q(e,t,f,o,d,c,new Map([[s,{row:E}]]))}}};export{Cn as A,vn as C,Dn as R,qn as T,Fn as a,Mn as b,kn as c,bn as d,Un as r,Ln as x};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/sql-store",
3
- "version": "1.0.0-alpha.137",
3
+ "version": "1.0.0-alpha.139",
4
4
  "description": "Internal dialect-parameterized SQL store core for Lunora .global() backends (D1, PlanetScale)",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@lunora/errors": "1.0.0-alpha.41",
54
- "@lunora/shard-engine": "1.0.0-alpha.84",
54
+ "@lunora/shard-engine": "1.0.0-alpha.85",
55
55
  "drizzle-orm": "^0.45.2"
56
56
  },
57
57
  "engines": {
@@ -1 +0,0 @@
1
- import{LunoraError as g}from"@lunora/errors";import{D as C,c as vt,m as Dt,r as b,a as qt,l as St,w as q,b as Ft}from"./ctx-db-search-state-DGbsWTp6.mjs";import{ftsUnmappedPage as bt,groupUnmappedRows as Ut,ftsWriteDocument as Et,ftsPurgeDocument as lt,ftsCompanionDdl as kt,unionAll as Ht,ftsRowidMapName as Pt}from"@lunora/shard-engine";import{sql as i}from"drizzle-orm";import{OCC_VERSION_COLUMN as Bt,nullSafeEqualsSql as Wt,queryAll as h,decodeRow as Y,runInOrder as O,indexState as M,createIndexIfNotExists as Q,dropIndexIfExists as v,queryRun as p,decodeRows as mt,columnRefSql as et,serializeColumnValue as jt,readRowsPage as xt,forEachRowPaged as Kt}from"./decodeGlobalRow-VOl_TIbk.mjs";const Gt=n=>{const t=atob(n),e=Uint8Array.from(t,r=>r.codePointAt(0)??0);return new TextDecoder().decode(e)},Yt=()=>new g("BAD_REQUEST","invalid cursor"),it=16,rt=8,y=1024,U=(n,t)=>t.query(n),Qt=(n,t)=>{if(!t)return{exact:!0,lower:n,upper:n};const e=[...n].at(-1)??"",r=(e.codePointAt(0)??0)+1;if(r>=55296&&r<=57343||r>1114111)return{exact:!0,lower:n,upper:n};const o=n.slice(0,n.length-e.length);return{exact:!1,lower:n,upper:o+String.fromCodePoint(r)}},Cn=(n,t,e)=>{const r={eq:(o,s)=>{if(!n.definition.filterFields?.includes(o))throw new g("INTERNAL",`field "${o}" is not a filter field of search index "${n.indexName}" on table "${t}"`);if(n.filters.length>=rt)throw new g("BAD_REQUEST",`search index "${n.indexName}" on table "${t}": at most ${String(rt)} .eq() filters are supported per search query`);return n.filters.push({field:o,value:s}),r},search:(o,s)=>{const a=n;if(o!==a.definition.field)throw new g("INTERNAL",`search index "${a.indexName}" on table "${t}" indexes "${a.definition.field}", not "${o}"`);const f=U(s,e).length;if(f>it)throw new g("BAD_REQUEST",`search index "${a.indexName}" on table "${t}": at most ${String(it)} search terms are supported (got ${String(f)})`);return a.field=o,a.query=s,a.hasQuery=!0,r}};return r},Ln=n=>{if(n.length>y)throw new g("BAD_REQUEST",`more than ${String(y)} documents match this search — narrow it with filters or read it a page at a time with .paginate()`)},Mn=n=>Math.min(n.offset+n.numItems+1,y),Vt=n=>btoa(`search:${String(n)}`),Xt=n=>{let t;try{t=Gt(n)}catch{return}if(!t.startsWith("search:"))return;const e=Number(t.slice(7));return Number.isInteger(e)&&e>=0?e:void 0},vn=n=>{if(typeof n.endCursor=="string")throw new g("BAD_REQUEST","bounded (endCursor) pagination is not supported on search queries — relevance order has no stable range boundary");if(!Number.isFinite(n.numItems))throw new g("BAD_REQUEST",`search pagination needs a finite numItems, got ${String(n.numItems)}`);const t=Math.max(0,Math.floor(n.numItems)),e=n.cursor?Xt(n.cursor):0;if(e===void 0)throw Yt();if(e+t>=y)throw new g("BAD_REQUEST",`search pagination reaches the ${String(y)}-document limit (offset ${String(e)} + ${String(t)} requested) — a page must end below the cap so the probe row that answers \`hasMore\` still fits: retry with numItems ${String(Math.max(1,y-e-1))} or fewer, or narrow the query or the filters instead`);return{numItems:t,offset:e}},Dn=(n,t)=>{const e=t.offset+t.numItems,r=t.numItems>0&&n.length>e;return{continueCursor:r?Vt(e):null,isDone:!r,page:n.slice(t.offset,e)}},qn=n=>{if(n===void 0)return y+1;if(!Number.isFinite(n))return y;const t=Math.max(0,Math.floor(n));if(t>y)throw new g("BAD_REQUEST",`search returns at most ${String(y)} documents (asked for ${String(t)}) — narrow the query or paginate instead`);return t},Jt=(n,t)=>{if(!t.includes("."))return n[t];let e=n;for(const r of t.split(".")){if(e===null||typeof e!="object"||Array.isArray(e))return;e=e[r]}return e},k=(n,t)=>`${n}__fts_${t}`,S="__id__",T="__token__",A="__n__",zt=1e3,ht=(n,t)=>t.document(n),j=(n,t)=>Jt(n,t),Zt=n=>typeof n=="string"?n:n==null?"":typeof n=="number"||typeof n=="bigint"||typeof n=="boolean"?String(n):JSON.stringify(n)??"",tn=(n,t,e)=>n!==void 0&&t!==void 0&&j(n,e.field)===j(t,e.field),nn=(n,t)=>ht(Zt(j(n,t.field)),C(t.language)).slice(0,zt),D=(n,t)=>nn(n,t).join(" "),pt=(n,t)=>{const e=new Map;for(const r of ht(n,t))e.set(r,(e.get(r)??0)+1);return e},gt=[Bt,"_creationTime"],ot=50,$=(n,t)=>i`${i.identifier(n)}.${i.identifier(t)}`,I=(n,t,e)=>i`DELETE FROM ${i.identifier(n)} WHERE ${$(n,S)} = ${t}${e===void 0?i``:i` AND ${e}`}`,Tt=(n,{row:t,table:e})=>{const r=gt.map(o=>i` AND ${Wt(n.name,$(e,o),t[o]??null)}`);return i`SELECT 1 FROM ${i.identifier(e)} WHERE ${$(e,"id")} = ${t.id}${i.join(r)}${n.name==="postgres"?i` FOR SHARE`:i``}`},F=(n,t)=>i`EXISTS (${Tt(n,t)})`,V=(n,t)=>i`NOT EXISTS (SELECT 1 FROM ${i.identifier(n)} WHERE ${$(n,"id")} = ${t})`,yt=(n,t,e,r)=>r===void 0?V(t,e):F(n,{row:r,table:t}),Rt=async(n,t,e,r)=>{const o=new Map;for(let s=0;s<r.length;s+=ot){const a=r.slice(s,s+ot),f=await h(n,t,i`SELECT * FROM ${i.identifier(e)} WHERE ${$(e,"id")} IN (${i.join(a.map(u=>i`${u}`),i`, `)})`);for(const u of f)o.set(String(u.id),u)}return o},x=(n,t,e)=>n.name==="mysql"?i`${i.identifier(t)}(${i.raw(String(e))})`:n.textPatternOperatorClass===void 0?i`${i.identifier(t)}`:i`${i.identifier(t)} ${i.raw(n.textPatternOperatorClass)}`,X=512,J=(n,t)=>({columns:i`${x(n,T,256)}, ${x(n,S,X)}`,concurrently:!0,name:`${t}__unique`,table:t,unique:!0}),B=n=>i`${n} COLLATE utf8mb4_0900_bin`,W=(n,t)=>{const e=JSON.stringify(t.map(([r,o])=>({o,t:r})));switch(n.name){case"mysql":return i`JSON_TABLE(${e}, '$[*]' COLUMNS (${i.identifier("t")} VARCHAR(256) PATH '$.t', ${i.identifier("o")} INT PATH '$.o')) AS ${i.identifier("j")}`;case"postgres":return i`json_to_recordset(${e}::json) AS ${i.identifier("j")}(${i.identifier("t")} text, ${i.identifier("o")} integer)`;default:return i`(SELECT json_extract(${$("e","value")}, '$.t') AS ${i.identifier("t")}, json_extract(${$("e","value")}, '$.o') AS ${i.identifier("o")} FROM json_each(${e}) AS ${i.identifier("e")}) AS ${i.identifier("j")}`}},_t=(n,t,e,r,o)=>{const s=i.join([T,S,A].map(f=>i.identifier(f)),i`, `),a=f=>i`INSERT INTO ${i.identifier(t)} (${s}) SELECT ${$("j","t")}, ${e}, ${$("j","o")} FROM ${W(n,r)} WHERE ${f}`;switch(n.name){case"mysql":{const f=F(n,o),u=$(t,T),c=i.identifier("held");return[i`${a(i`${f} AND NOT EXISTS (SELECT 1 FROM ${i.identifier(t)} AS ${c} WHERE ${$("held",S)} = ${e} AND ${$("held",T)} = ${B($("j","t"))})`)} ON DUPLICATE KEY UPDATE ${$(t,A)} = ${$(t,A)}`,i`UPDATE ${i.identifier(t)} JOIN ${W(n,r)} ON ${u} = ${B($("j","t"))} SET ${$(t,A)} = ${$("j","o")} WHERE ${$(t,S)} = ${e} AND ${f}`,i`DELETE FROM ${i.identifier(t)} WHERE ${$(t,S)} = ${e} AND ${f} AND ${u} NOT IN (SELECT ${B($("j","t"))} FROM ${W(n,r)})`]}case"postgres":{const f=i.identifier("__source__"),u=i.identifier("__purged__"),c=i`EXISTS (SELECT 1 FROM ${f})`;return[i`WITH ${f} AS (${Tt(n,o)}), ${u} AS (${I(t,e,c)} RETURNING 1) ${a(i`${c} AND (SELECT COUNT(*) FROM ${u}) >= 0`)} ON CONFLICT DO NOTHING`]}default:{const f=F(n,o);return[I(t,e,f),i`${a(f)} ON CONFLICT DO NOTHING`]}}},Nt={uniqueKeyMissing:!1,unmappedSkipped:0},st=new Set,at=(n,t)=>{st.has(n)||(st.add(n),console.warn(`[@lunora/sql-store] ${t}`))},ft=100,ut=3,K=async(n,t,e,r=0)=>{const{companion:o,definition:s,index:a,tableName:f}=e,u={done:!0,last:r,left:0};if((await h(n,t,t.tableExists(f))).length===0)return u;const d=await h(n,t,bt(o,ft,r));if(d.length===0)return u;const w=Math.max(...d.map(_=>Number(_.rowid))),l=Ut(d),E=await Rt(n,t,f,[...l.keys()]),m=[];for(const[_,Z]of l){const P=E.get(_),tt=P===void 0?void 0:Y(s,P),nt=yt(t,f,_,P);m.push(...tt?Et(o,_,D(tt,a),{guard:nt,unmappedRowids:Z}):lt(o,_,{guard:nt,unmappedRowids:Z}))}await O(n,t,m);const R=await h(n,t,i`SELECT COUNT(*) AS ${i.identifier("n")} FROM ${i.identifier(o)} WHERE ${$(o,"rowid")} > ${Math.max(0,r)} AND ${$(o,"rowid")} <= ${w}`);return{done:d.length<ft,last:w,left:Number(R[0]?.n??0)}},en=async(n,t,e)=>{let r=0,o=0;for(;;){let s=await K(n,t,e,r);for(let a=1;s.left>0&&a<ut;a+=1)s=await K(n,t,e,r);if(s.left>0&&(o+=s.left,console.warn(`[@lunora/sql-store] search migration of "${e.companion}": ${String(s.left)} row(s) lost every one of ${String(ut)} attempts to a concurrent write and were left for a later pass.`)),s.done)return o;r=s.last}},rn=async(n,t,e,r)=>r==="step"?(await K(n,t,e),Nt):{uniqueKeyMissing:!1,unmappedSkipped:await en(n,t,e)},G=n=>`${n}__btree`,on=async(n,t,e)=>{const r=J(t,e),o=await M(n,t,e,r.name);if(o!=="valid"){if(o==="absent"&&(await h(n,t,i`SELECT 1 AS ${i.identifier("present")} FROM ${i.identifier(e)} LIMIT 1`)).length===0)try{await Q(n,t,{...r,concurrently:!1}),await v(n,t,e,G(e));return}catch(a){if(await M(n,t,e,r.name).catch(()=>{})==="valid")return;at(`${e}:empty`,`search companion "${e}": adding its unique key failed (${String(a)}); run backfillSqlSearchIndexes.`);return}at(`${e}:missing`,`search companion "${e}" has no usable unique key, so two backfills racing over one row can index it twice. Run backfillSqlSearchIndexes, which adds it.`)}},sn=async(n,t,e)=>{const r=$(e,S),o=$(e,T);return(await h(n,t,i`SELECT ${r} AS ${i.identifier("id")}, ${o} AS ${i.identifier("token")}, COUNT(*) AS ${i.identifier("n")} FROM ${i.identifier(e)} GROUP BY ${r}, ${o} HAVING COUNT(*) > 1`)).map(a=>({count:Number(a.n),id:String(a.id),token:String(a.token)}))},an=(n,t,{count:e,id:r,token:o})=>{const s=i`${$(t,S)} = ${r} AND ${$(t,T)} = ${o}`;switch(n.name){case"mysql":return i`DELETE FROM ${i.identifier(t)} WHERE ${s} LIMIT ${i.raw(String(e-1))}`;case"postgres":return i`DELETE FROM ${i.identifier(t)} WHERE ctid = ANY (ARRAY(SELECT ctid FROM ${i.identifier(t)} WHERE ${s} OFFSET 1))`;default:return i`DELETE FROM ${i.identifier(t)} WHERE rowid IN (SELECT rowid FROM ${i.identifier(t)} WHERE ${s} LIMIT -1 OFFSET 1)`}},fn=async(n,t,e,r)=>{const{companion:o,definition:s,index:a,tableName:f}=e;for(const d of r)await p(n,t,an(t,o,d));const u=[...new Set(r.map(d=>d.id))],c=await Rt(n,t,f,u);for(const d of u){const w=c.get(d),l=w===void 0?void 0:Y(s,w);if(w!==void 0&&l){const E=[...pt(D(l,a),C(a.language))];await O(n,t,_t(t,o,d,E,{row:w,table:f}));continue}await p(n,t,I(o,d,yt(t,f,d,w)))}},un=async(n,t,e)=>t.name!=="mysql"?"":(await h(n,t,i`SELECT COLUMN_NAME AS ${i.identifier("name")}, COLLATION_NAME AS ${i.identifier("collation")} FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ${e} AND COLUMN_NAME IN (${T}, ${S})`)).filter(o=>!String(o.collation).endsWith("_bin")).map(o=>` Its ${String(o.name)} column uses ${String(o.collation)}, which treats values differing only in case or accents as one; convert it with ALTER TABLE \`${e}\` MODIFY \`${String(o.name)}\` VARCHAR(768) COLLATE utf8mb4_0900_bin NOT NULL.`).join(""),cn=async(n,t,e)=>{if(t.name!=="mysql")return 0;const r=$(e,S),o=await h(n,t,i`SELECT COUNT(*) AS ${i.identifier("n")} FROM (SELECT 1 AS ${i.identifier("x")} FROM ${i.identifier(e)} GROUP BY LEFT(${r}, ${i.raw(String(X))}), ${$(e,T)} HAVING COUNT(DISTINCT ${r}) > 1) AS ${i.identifier("shared")}`);return Number(o[0]?.n??0)},dn=async(n,t,e)=>{const r=J(t,e),o=await M(n,t,e,r.name);if(o==="valid")return"valid";o==="invalid"&&await v(n,t,e,r.name);try{return await Q(n,t,r),"built"}catch(s){const a=await M(n,t,e,r.name);if(a==="valid")return"valid";if(a==="invalid"&&await v(n,t,e,r.name),!t.isUniqueViolation(s))throw s;return"refused"}},ct=3,$n=async(n,t,e)=>{const{companion:r}=e,o=await un(n,t,r);let s="";o!==""&&console.warn(`[@lunora/sql-store] search companion "${r}" folds case and accents in its unique key.${o}`);for(let a=0;a<ct;a+=1){const f=await dn(n,t,r);if(f==="valid")return await v(n,t,r,G(r)),!0;if(f==="refused"){const u=await sn(n,t,r);if(u.length>0)await fn(n,t,e,u);else{const c=await cn(n,t,r);s=c>0?` ${String(c)} token(s) are held by document ids that agree on their first ${String(X)} characters, which the key cannot tell apart; shorten those ids.`:o}}}return await M(n,t,r,J(t,r).name)==="valid"?(await v(n,t,r,G(r)),!0):(console.warn(`[@lunora/sql-store] search companion "${r}" still has no unique key after ${String(ct)} attempts: writes keep working, but two backfills racing over one row can index it twice.${s}`),!1)},wn=async(n,t,e,r)=>r==="step"?Nt:{uniqueKeyMissing:!await $n(n,t,e),unmappedSkipped:0},Sn=(n,t)=>t?i`${i.identifier(T)} LIKE ${`${n}%`}`:i`${i.identifier(T)} = ${n}`,At=(n,t)=>{const e=t.filters.map(r=>i`m.${et(r.field)} = ${jt(r.value)}`);return n.softDeleteMode&&e.push(i`m.${et(n.softDeleteMode.field)} IS NULL`),e},Ot=async(n,t,e,r,o,s,a)=>{const f=At(e,o);let u=i`SELECT m.* FROM (${a}) s JOIN ${i.identifier(r)} m ON m.${i.identifier("id")} = s.${i.identifier(S)}`;return f.length>0&&(u=i`${u} WHERE ${i.join(f,i` AND `)}`),u=i`${u} ORDER BY s.${i.identifier("__score__")} DESC, m.${i.identifier("_creationTime")} DESC, m.${i.identifier("id")} ASC LIMIT ${i.raw(String(s))}`,mt(e,await h(n,t,u))},En=async(n,t,e,r,o,s)=>{const a=U(o.query,C(o.definition.language));if(a.length===0)return[];const f=k(r,o.indexName),u=a.length-1,c=a.map((m,R)=>Sn(m,R===u)),d=i.join(c,i` OR `),w=c.map(m=>i`SUM(CASE WHEN ${m} THEN ${i.identifier(A)} ELSE 0 END)`),l=i.join(w.map(m=>i`${m} > 0`),i` AND `),E=i`SELECT ${i.identifier(S)}, ${i.join(w,i` + `)} AS ${i.identifier("__score__")} FROM ${i.identifier(f)} WHERE ${d} GROUP BY ${i.identifier(S)} HAVING ${l}`;return Ot(n,t,e,r,o,s,E)},ln=async(n,t,e,r,o,s)=>{const a=U(o.query,C(o.definition.language));if(a.length===0)return[];const f=k(r,o.indexName),u=`${f}__vocab`,c=a.length-1,d=a.map((E,m)=>{const R=Qt(E,m===c),_=R.exact?i`${i.identifier("term")} = ${R.lower}`:i`${i.identifier("term")} >= ${R.lower} AND ${i.identifier("term")} < ${R.upper}`;return i`SELECT ${i.identifier("doc")}, ${i.raw(String(m))} AS ${i.identifier("__term__")}, COUNT(*) AS ${i.identifier("__n__")} FROM ${i.identifier(u)} WHERE ${_} GROUP BY ${i.identifier("doc")}`}),w=a.map((E,m)=>i`SUM(CASE WHEN u.${i.identifier("__term__")} = ${i.raw(String(m))} THEN u.${i.identifier("__n__")} ELSE 0 END)`),l=i`SELECT f.${i.identifier(S)} AS ${i.identifier(S)}, ${i.join(w,i` + `)} AS ${i.identifier("__score__")} FROM (${Ht(d)}) u JOIN ${i.identifier(f)} f ON f.rowid = u.${i.identifier("doc")} GROUP BY f.${i.identifier(S)} HAVING ${i.join(w.map(E=>i`${E} > 0`),i` AND `)}`;return Ot(n,t,e,r,o,s,l)},mn=async(n,t,e,r,o,s)=>{const a=t.nativeTextSearch,f=U(o.query,C(o.definition.language));if(!a||f.length===0)return[];const u=k(r,o.indexName),c=[a.matches(u,f),...At(e,o)],d=i`SELECT m.* FROM ${i.identifier(u)} JOIN ${i.identifier(r)} m ON m.${i.identifier("id")} = ${i.identifier(u)}.${i.identifier(S)} WHERE ${i.join(c,i` AND `)} ORDER BY ${a.rank(u,f)} DESC, m.${i.identifier("_creationTime")} DESC, m.${i.identifier("id")} ASC LIMIT ${i.raw(String(s))}`;return mt(e,await h(n,t,d))},hn={ensureCompanion:async(n,t,e)=>{const{integer:r,key:o}=t.companionTypes;await p(n,t,i`CREATE TABLE IF NOT EXISTS ${i.identifier(e)} (${i.identifier(T)} ${i.raw(o)} NOT NULL, ${i.identifier(S)} ${i.raw(o)} NOT NULL, ${i.identifier(A)} ${i.raw(r)} NOT NULL)`),await Q(n,t,{columns:x(t,S,191),name:`${e}__by_id`,table:e,unique:!1}),await on(n,t,e)},indexDocument:async(n,t,e,r,o,s)=>{const a=[...pt(D(r,o),C(o.language))];await O(n,t,_t(t,e,String(s.row.id),a,s))},migrate:wn,name:"inverted",purgeDocument:async(n,t,e,r,o)=>{await p(n,t,I(e,r,V(o,r)))},runSearch:En},pn={ensureCompanion:async(n,t,e)=>{await O(n,t,kt(e))},indexDocument:async(n,t,e,r,o,s)=>{await O(n,t,Et(e,String(s.row.id),D(r,o),{guard:F(t,s)}))},migrate:rn,name:"fts5",purgeDocument:async(n,t,e,r,o)=>{await O(n,t,lt(e,r,{guard:V(o,r)}))},runSearch:ln},gn={ensureCompanion:async(n,t,e)=>{const r=t.nativeTextSearch;if(r){await p(n,t,r.createCompanion(e,t.companionTypes.key));for(const o of r.createIndexes(e))await p(n,t,o)}},indexDocument:async(n,t,e,r,o,s)=>{const a=t.nativeTextSearch;if(!a)return;const f=String(s.row.id);await p(n,t,I(e,f)),await p(n,t,a.indexDocument(e,f,D(r,o)))},name:"native",purgeDocument:async(n,t,e,r)=>{await p(n,t,I(e,r))},runSearch:mn},L=(n,t)=>n.strategy==="native"&&t.nativeTextSearch!==void 0?gn:t.supportsFts5?pn:hn,H=(n,t)=>`${vt(n)}/${L(n,t).name}`,z=function*(n){for(const[t,e]of Object.entries(n.tables)){const r=e.searchIndexes;if(!(e.shardMode!==void 0&&e.shardMode.kind!=="global"||!r))for(const o of r)yield[t,e,o]}},N=(n,t)=>k(n,t.name),Tn=async(n,t,e,r)=>{const o=N(e,r);return St(await b(n,t,o),H(r,t)).finished?!0:Ft(n,t,o)},Fn=async(n,t,e,r,o,s)=>{if(!await Tn(n,t,r,o.definition))throw new g("SEARCH_INDEX_BUILDING",`search index "${o.indexName}" on table "${r}" is still backfilling and currently covers only part of the table — retry once it finishes${t.searchBackfillHint===void 0?"":`, or complete it now: ${t.searchBackfillHint}`}`);return L(o.definition,t).runSearch(n,t,e,r,o,s)},dt=n=>n.slice(n.lastIndexOf("/")+1),$t=200,wt=5,yn=async(n,t,e,r)=>{const o=[...r.keys()],s=await h(n,t,i`SELECT * FROM ${i.identifier(e)} WHERE ${i.identifier("id")} >= ${o[0]} AND ${i.identifier("id")} <= ${o.at(-1)} ORDER BY ${i.identifier("id")} ASC`),a=new Map;for(const f of s){const u=f.id,c=r.get(u)?.row;c&&gt.some(d=>String(c[d])!==String(f[d]))&&a.set(u,{row:f})}return a},It=async(n,t,e,r,o,s,a)=>{const f=L(s,t);let u=a;for(let c=0;u.size>0&&c<wt;c+=1){for(const{document:d,row:w}of u.values()){const l=d??Y(e,w);l&&await f.indexDocument(n,t,o,l,s,{row:w,table:r})}u=await yn(n,t,r,u)}u.size>0&&console.warn(`[@lunora/sql-store] search index "${o}": ${String(u.size)} row(s) were rewritten on every one of ${String(wt)} re-check rounds and were left to their writers — each stays stale only if none of those writes changed its indexed text.`)},Ct=async(n,t,e,r,o)=>{const s=N(r,o),a=H(o,t),f=St(await b(n,t,s),a);if(f.finished)return!0;if(f.wipe&&await q(n,t,s,void 0,!1,a),(await h(n,t,t.tableExists(r))).length===0)return await q(n,t,s,void 0,!0,a),!0;const c=await xt(n,t,r,f.cursor,$t),d=c.findLast(E=>typeof E.id=="string")?.id,w=new Map;for(const E of c)typeof E.id=="string"&&w.set(E.id,{row:E});await It(n,t,e,r,s,o,w);const l=c.length<$t;return await q(n,t,s,d??f.cursor,l,a),l},Lt=async(n,t,e)=>{await Dt(n,e);for(const[r,,o]of z(t)){const s=N(r,o),a=H(o,e),f=await b(n,e,s);f.profile!==void 0&&dt(f.profile)!==dt(a)&&(await p(n,e,i`DROP TABLE IF EXISTS ${i.identifier(s)}`),await p(n,e,i`DROP TABLE IF EXISTS ${i.identifier(Pt(s))}`),await qt(n,e,s)),await L(o,e).ensureCompanion(n,e,s)}},Rn=async(n,t,e,r,o)=>{const s=N(r,o);if((await b(n,t,s)).profile!==void 0)return;let f=0;(await h(n,t,t.tableExists(r))).length>0&&await Kt(n,t,e,r,()=>{f+=1},{limit:1}),await q(n,t,s,void 0,f===0,H(o,t))},Mt=async(n,t,e,r,o,s)=>await L(o,t).migrate?.(n,t,{companion:N(r,o),definition:e,index:o,tableName:r},s)??{uniqueKeyMissing:!1,unmappedSkipped:0},bn=async(n,t,e)=>{await Lt(n,t,e);for(const[r,o,s]of z(t)){if(await Mt(n,e,o,r,s,"step"),s.staged){await Rn(n,e,o,r,s);continue}await Ct(n,e,o,r,s)}},Un=async(n,t,e)=>{const r={uniqueKeyMissing:[],unmappedSkipped:0};await Lt(n,t,e);for(const[o,s,a]of z(t)){const f=await Mt(n,e,s,o,a,"drain");r.unmappedSkipped+=f.unmappedSkipped,f.uniqueKeyMissing&&r.uniqueKeyMissing.push(N(o,a));let u=!1;for(;!u;)u=await Ct(n,e,s,o,a)}return r},kn=n=>{const{dialect:t,exec:e,schema:r}=n;return async(o,s,a)=>{const f=r.tables[o],u=f?.searchIndexes;if(!(!f||!u||u.length===0))for(const c of u){if(tn(a?.previous,a?.document,c))continue;const d=N(o,c);if(a){await It(e,t,f,o,d,c,new Map([[s,{document:a.document,row:{...a.written,id:s}}]]));continue}await L(c,t).purgeDocument(e,t,d,s,o)}}};export{Cn as A,vn as C,Dn as R,qn as T,Fn as a,Mn as b,kn as c,Un as d,bn as r,Ln as x};