@lunora/d1 1.0.0-alpha.18 → 1.0.0-alpha.181

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.
package/dist/index.mjs CHANGED
@@ -1,7 +1 @@
1
- export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-DR6UiAZ8.mjs';
2
- export { D1Client, D1Session } from './packem_shared/D1Client-DA3flo1o.mjs';
3
- export { createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges } from './packem_shared/createD1CtxDb-BMR8J0dT.mjs';
4
- export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-C6u_WMIY.mjs';
5
- export { MigrationRunner } from './packem_shared/MigrationRunner-BkEwQ-Ya.mjs';
6
- export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
7
- export { createSqlCtxDb } from '@lunora/sql-store';
1
+ import{exportGlobalRows as o,importGlobalRows as t,selectGlobalTables as a}from"./packem_shared/exportGlobalRows-CrSRW2jw.mjs";import{D1Client as i,D1Session as n}from"./packem_shared/D1Client-CzS0Qfa6.mjs";import{backfillD1SearchIndexes as D,createD1CtxDb as p,readD1CdcChanges as x,runD1AggregateMigrations as b,runD1CdcMigration as g,runD1GlobalTableMigrations as m,runD1RankMigrations as c,runD1SearchMigrations as f,sweepD1CdcRetention as u}from"./packem_shared/backfillD1SearchIndexes-BHgn1SNb.mjs";import{facetGlobalColumn as d,listGlobalTables as G,readGlobalTablePage as T}from"./packem_shared/facetGlobalColumn-B9B-UuDR.mjs";import{MigrationRunner as M}from"./packem_shared/MigrationRunner-DADTo9Q8.mjs";import{d1QueryTag as h,emitD1QueryCost as w,readD1QueryCost as S}from"./packem_shared/d1QueryTag-BcEGW4q1.mjs";import{D1TimeoutError as Q,isTransientD1Error as k,retryingExec as q,withD1Retry as A}from"./packem_shared/D1TimeoutError-D4YUqZPk.mjs";import{default as P}from"./packem_shared/sqliteDialect-B_3JNtdq.mjs";import{applyCdcChanges as v}from"@lunora/shard-engine";import{createSqlCtxDb as B}from"@lunora/sql-store";export{i as D1Client,n as D1Session,Q as D1TimeoutError,M as MigrationRunner,v as applyCdcChanges,D as backfillD1SearchIndexes,p as createD1CtxDb,B as createSqlCtxDb,h as d1QueryTag,w as emitD1QueryCost,o as exportGlobalRows,d as facetGlobalColumn,t as importGlobalRows,k as isTransientD1Error,G as listGlobalTables,x as readD1CdcChanges,S as readD1QueryCost,T as readGlobalTablePage,q as retryingExec,b as runD1AggregateMigrations,g as runD1CdcMigration,m as runD1GlobalTableMigrations,c as runD1RankMigrations,f as runD1SearchMigrations,a as selectGlobalTables,P as sqliteDialect,u as sweepD1CdcRetention,A as withD1Retry};
@@ -0,0 +1 @@
1
+ import{drizzle as i}from"drizzle-orm/d1";import{isReadOnlyD1Sql as o,withD1Retry as a}from"./D1TimeoutError-D4YUqZPk.mjs";const c=(s,e)=>{if(s.size<e)return;const t=s.keys().next().value;t!==void 0&&s.delete(t)},l=256,h=(s,e,t)=>{const r=s.get(t);if(r)return s.delete(t),s.set(t,r),r;const n=e(t);return c(s,l),s.set(t,n),n},d="first-unconstrained";class u{session;stmtCache=new Map;constructor(e){this.session=e}prepare(e){return h(this.stmtCache,t=>this.session.prepare(t),e)}async run(e,...t){return this.prepare(e).bind(...t).run()}async all(e,...t){const r=async()=>this.prepare(e).bind(...t).all();return o(e)?a(r):r()}async first(e,...t){const r=async()=>this.prepare(e).bind(...t).first();return o(e)?a(r):r()}getBookmark(){return this.session.getBookmark()??void 0}}class b{db;stmtCache=new Map;drizzleHandle;constructor(e){this.db=e}withSession(e){const t=this.db.withSession(e??d);return new u(t)}prepare(e){return h(this.stmtCache,t=>this.db.prepare(t),e)}get drizzle(){return this.drizzleHandle?this.drizzleHandle:(this.drizzleHandle=i(this.db,{logger:!1}),this.drizzleHandle)}drizzleSession(e){const t=this.db.withSession(e??d);return i(t,{logger:!1})}async batch(e){return this.drizzle.batch(e)}get raw(){return this.db}}export{b as D1Client,u as D1Session};
@@ -0,0 +1 @@
1
+ import{LunoraError as h,unreachable as f}from"@lunora/errors";const y=["storage operation exceeded timeout","network connection lost","internal error while starting up d1 db storage","exceeded its memory limit","caused object to be reset"],M=3,S=50,b=1e3,D=2e3,p=/^[\s(]*(?:select|pragma|explain)\b/iu,_=t=>p.test(t),A=t=>t instanceof Error?`${t.name}: ${t.message}`.toLowerCase():typeof t=="string"?t.toLowerCase():"",I=t=>{const e=A(t);return e.length===0?!1:y.some(n=>e.includes(n))},N=t=>{const e=Math.min(b,S*2**(t-1));return Math.round(Math.random()*e)},L=async t=>new Promise(e=>{setTimeout(e,t)});class l extends Error{timeoutMs;constructor(e){super(`@lunora/d1: operation exceeded its ${String(e)}ms timeout and was abandoned (the underlying D1 call may still be running)`),this.name="D1TimeoutError",this.timeoutMs=e}}const R=async(t,e)=>{let n;try{return await Promise.race([t(),new Promise((o,r)=>{n=setTimeout(()=>{r(new l(e))},e)})])}finally{n!==void 0&&clearTimeout(n)}},x=async(t,e={})=>{const n=e.attempts??M;if(!Number.isInteger(n)||n<1)throw new h("BAD_REQUEST","@lunora/d1: `attempts` must be an integer >= 1");const o=e.sleep??L,{deadlineMs:r,timeoutMs:i}=e,d=Date.now(),w=r===void 0&&i===void 0,c=()=>r===void 0?Number.POSITIVE_INFINITY:r-(Date.now()-d);for(let s=1;s<=n;s+=1){const u=Math.min(i??Number.POSITIVE_INFINITY,Math.max(c(),0)),E=Date.now();try{return await(Number.isFinite(u)?R(t,u):t())}catch(a){const g=a instanceof l||I(a),T=w&&Date.now()-E>=D,m=c();if(s===n||!g||T||m<=0)throw a;await o(Math.min(N(s),m))}}return f("@lunora/d1: retry loop exited without returning or throwing")},P=(t,e)=>({...t,all:async(n,o)=>_(n)?x(async()=>t.all(n,o),e):t.all(n,o)});export{l as D1TimeoutError,_ as isReadOnlyD1Sql,I as isTransientD1Error,P as retryingExec,x as withD1Retry};
@@ -0,0 +1,5 @@
1
+ import{LunoraError as g}from"@lunora/errors";import{sql as w}from"drizzle-orm";import{D1Client as S}from"./D1Client-CzS0Qfa6.mjs";const d="__drizzle_migrations",A=`CREATE TABLE IF NOT EXISTS ${d} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC, version INTEGER)`,$=`CREATE UNIQUE INDEX IF NOT EXISTS ${d}_version ON ${d} (version)`,p=/\s/u,_=/[$\w]/u,T=String.raw`(?:\s|--[^\n]*\n|\/\*[\s\S]*?\*\/)`,y=/^(?:\s|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/u,L=new RegExp(String.raw`^CREATE${T}+(?:TEMP${T}+|TEMPORARY${T}+)?TRIGGER\b`,"iu"),z=/^[0-9a-f]{64}$/u,C=new RegExp(String.raw`UNIQUE constraint failed:\s*${d}\.(?:hash|version)`,"iu"),U=o=>{const e=o.sql,n=L.test(e.slice(y.exec(e)?.[0].length??0));let s=!1,a=!1,h=!1,f=!1,t=!1,c=!1,i,l="",E=!1,v=!1;for(let u=0;u<e.length;u+=1){const r=e[u],m=e[u+1];if(t){r===`
2
+ `&&(t=!1);continue}if(c){r==="*"&&m==="/"&&(c=!1,u+=1);continue}if(s){r==="'"&&(m==="'"?u+=1:s=!1);continue}if(a){r==='"'&&(m==='"'?u+=1:a=!1);continue}if(h){r==="]"&&(h=!1);continue}if(f){r==="`"&&(m==="`"?u+=1:f=!1);continue}if(r==="-"&&m==="-"){t=!0,u+=1;continue}if(r==="/"&&m==="*"){c=!0,u+=1;continue}if(i!==void 0){if(r!==void 0&&!p.test(r))throw new g("INTERNAL",`Migration "${o.name}" (v${String(o.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`);continue}if(n&&r!==void 0){if(_.test(r)){l+=r;continue}const R=l.toUpperCase();l="",R!==""?(E=v&&R==="END",v=!1):r!==";"&&!p.test(r)&&(E=!1,v=!1)}if(r==="'"){s=!0;continue}if(r==='"'){a=!0;continue}if(r==="["){h=!0;continue}if(r==="`"){f=!0;continue}if(r===";"){if(n&&!E){v=!0;continue}i=u}}return i},I=async o=>{const e=new TextEncoder().encode(o),n=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(n)].map(s=>s.toString(16).padStart(2,"0")).join("")},M=/^\uFEFF/u,D=o=>{let e=o.length;for(;e>0&&o[e-1]===`
3
+ `;)e-=1;return o.slice(0,e)},N=o=>D(o.replace(M,"").replaceAll(`\r
4
+ `,`
5
+ `)),O=async o=>({normalized:await I(N(o)),raw:await I(o)}),H=(o,e)=>o===e?.normalized||o===e?.raw;class q{client;migrations;constructor(e,n){this.client=e instanceof S?e:new S(e),this.migrations=[...n].toSorted((s,a)=>s.version-a.version),this.assertUniqueVersions(),this.assertUniqueSql()}async run(){await this.ensureTrackingTable();const e=await Promise.all(this.migrations.map(async i=>O(i.sql))),n=await this.readTrackingRows(e),s=new Map(n.filter(i=>i.version!==null).map(i=>[i.version,i.hash])),a=i=>String(e[i]?.normalized),h=this.migrations.filter((i,l)=>{const E=s.get(i.version);if(E!==void 0&&!H(E,e[l]))throw new g("MIGRATION_DRIFT",`Migration "${i.name}" (v${String(i.version)}) was already applied from different SQL (recorded sha256 ${E}, now ${a(l)}). An applied migration is never re-run: revert the edit and put the change in a new migration. If the edit is inert (a comment, whitespace, line endings), record the new text as applied: UPDATE ${d} SET hash = '${a(l)}' WHERE version = ${String(i.version)};`);return E===void 0}),f=n.filter(i=>i.version===null);if(h.length>0&&f.length>0)throw new g("MIGRATION_DRIFT",`${d} holds ${String(f.length)} row(s) recorded before versions were tracked whose SQL matches no current migration (id ${f.map(i=>String(i.id)).join(", ")}), and ${h.map(i=>`v${String(i.version)}`).join(", ")} ${h.length===1?"is":"are"} not recorded as applied — one of them may be an edited copy of what already ran. Refusing to guess: give each row the version it recorded (UPDATE ${d} SET version = <n> WHERE id = <id>; for a migration that no longer exists, any version no current migration uses), then run again.`);const t=[],c=[];for(const[i,l]of this.migrations.entries()){const E=a(i);if(!h.includes(l)){c.push({name:l.name,version:l.version});continue}await this.applyOne(l,E)?t.push({name:l.name,version:l.version}):c.push({name:l.name,version:l.version})}return{applied:t,skipped:c}}async hasVersionColumn(){return(await this.client.drizzle.all(w.raw(`SELECT name FROM pragma_table_info('${d}') WHERE name = 'version'`))).length>0}async ensureTrackingTable(){if(await this.client.drizzle.run(w.raw(A)),!await this.hasVersionColumn())try{await this.client.drizzle.run(w.raw(`ALTER TABLE ${d} ADD COLUMN version INTEGER`))}catch(e){if(!await this.hasVersionColumn())throw e}await this.client.drizzle.run(w.raw($))}async readTrackingRows(e){const n=w.raw(`SELECT id, hash, version FROM ${d} ORDER BY id`),s=await this.client.drizzle.all(n),a=new Map(this.migrations.flatMap((t,c)=>[[String(e[c]?.raw),t.version],[String(e[c]?.normalized),t.version]])),h=new Set(s.map(t=>t.version).filter(t=>t!==null));let f=!1;for(const t of s){const c=t.version===null?a.get(t.hash):void 0;c===void 0||h.has(c)||(await this.client.drizzle.run(w`UPDATE ${w.raw(d)} SET version = ${c} WHERE id = ${t.id} AND version IS NULL`),h.add(c),f=!0)}return f?await this.client.drizzle.all(n):s}async applyOne(e,n){const s=U(e),a=(s===void 0?e.sql:e.sql.slice(0,s)).trim();if(!z.test(n))throw new g("INTERNAL",`migration "${e.name}" produced a non-hex hash; refusing to inline into SQL`);const h=`INSERT INTO ${d} (hash, created_at, version) VALUES ('${n}', ${String(Date.now())}, ${String(e.version)})`,f=[this.client.drizzle.run(w.raw(a)),this.client.drizzle.run(w.raw(h))];try{await this.client.batch(f)}catch(t){if(C.test(t instanceof Error?t.message:String(t)))return!1;throw t}return!0}assertUniqueVersions(){const e=new Set;for(const n of this.migrations){if(!Number.isSafeInteger(n.version))throw new g("INTERNAL",`Migration "${n.name}" has version ${String(n.version)}; a version must be an integer`);if(e.has(n.version))throw new g("INTERNAL",`Duplicate migration version ${String(n.version)}`);e.add(n.version)}}assertUniqueSql(){const e=new Map;for(const n of this.migrations){const s=N(n.sql),a=e.get(s);if(a!==void 0)throw new g("INTERNAL",`Migrations ${String(a)} and ${String(n.version)} have identical SQL — bump the content, not just the version.`);e.set(s,n.version)}}}export{q as MigrationRunner,d as TRACKING_TABLE_NAME};
@@ -0,0 +1 @@
1
+ const o="_version";export{o as OCC_VERSION_COLUMN};
@@ -0,0 +1 @@
1
+ import{backfillSqlSearchIndexes as r,createSqlCtxDb as o,readSqlCdcChanges as i,runSqlAggregateMigrations as c,runSqlCdcMigration as s,runSqlGlobalTableMigrations as l,runSqlRankMigrations as g,runSqlSearchMigrations as D,sweepSqlCdcRetention as S}from"@lunora/sql-store";import{createSqlCtxDb as w,decodeGlobalRow as G}from"@lunora/sql-store";import n from"./sqliteDialect-B_3JNtdq.mjs";const b=e=>o({...e,dialect:n}),q=(e,t)=>l(e,t,n),u=(e,t)=>c(e,t,n),M=(e,t)=>g(e,t,n),x=(e,t)=>D(e,t,n),h=(e,t)=>r(e,t,n),p=e=>s(e,n),f=(e,t={})=>i(e,t,n),m=(e,t,a)=>S(e,n,t,a);export{h as backfillD1SearchIndexes,b as createD1CtxDb,w as createSqlCtxDb,G as decodeGlobalRow,f as readD1CdcChanges,u as runD1AggregateMigrations,p as runD1CdcMigration,q as runD1GlobalTableMigrations,M as runD1RankMigrations,x as runD1SearchMigrations,m as sweepD1CdcRetention};
@@ -0,0 +1 @@
1
+ const s="lunora",i="d1_query",c=new Set(["delete","insert","select","update"]),a=/\b(?:from|into|update)\s+["`[]?(?<table>[a-z_][\w$]*)/iu,u=/^\s*(?<verb>[a-z]+)/iu,d=o=>a.exec(o)?.groups?.table?.toLowerCase(),w=o=>{const t=u.exec(o)?.groups?.verb?.toLowerCase()??"",r=d(o);return c.has(t)?`${t}:${r??"unknown"}`:r===void 0?"other":`other:${r}`},n=(o,e)=>{const t=o?.[e];return typeof t=="number"&&Number.isFinite(t)?t:void 0},E=o=>{const e=n(o,"rows_read"),t=n(o,"rows_written");if(e===void 0&&t===void 0)return;const r=n(o,"duration");return{...r===void 0?{}:{durationMs:r},rowsRead:e??0,rowsWritten:t??0}},b=(o,e)=>{const t=E(e);if(t===void 0)return;const r=JSON.stringify({durationMs:t.durationMs,rowsRead:t.rowsRead,rowsWritten:t.rowsWritten,source:s,tag:w(o),type:i});console.log(r)};export{w as d1QueryTag,b as emitD1QueryCost,E as readD1QueryCost};
@@ -0,0 +1 @@
1
+ import{toErrorBody as p}from"@lunora/errors";import{needsWireEncoding as E,encodeWire as m,decodeWire as g}from"@lunora/wire";import{runD1GlobalTableMigrations as w}from"./backfillD1SearchIndexes-BHgn1SNb.mjs";import{quoteIdentifier as u}from"./quoteIdentifier-CObIFRhb.mjs";import{decodeGlobalRow as R}from"@lunora/sql-store";const O=200,k=(t,e)=>{const o=r=>t.tables[r]?.shardMode?.kind==="global";return e&&e.length>0?e.filter(r=>o(r)):Object.keys(t.tables).filter(r=>o(r))},h=(t,e,o)=>{const r=t.tables[e];return r?R(r,o):{_creationTime:o._creationTime,_id:o.id}},y=t=>E(t)?m(t):t,F=async function*(t,e,o){const r=k(e,o.tables),i=o.batchSize??O;await w(t,e);for(const s of r){const n=u(s);let a,d=!0;for(;d;){const c=a===void 0?await t.all(`SELECT * FROM ${n} ORDER BY "id" LIMIT ?`,[i]):await t.all(`SELECT * FROM ${n} WHERE "id" > ? ORDER BY "id" LIMIT ?`,[a,i]);for(const f of c)yield{doc:y(h(e,s,f)),table:s};const l=c.at(-1);l!==void 0&&(a=String(l.id)),d=c.length===i}}},b=t=>t?._meta?.column?.notNull===!1,I=(t,e)=>{const o=new Set;for(const[r,i]of Object.entries(t.shape)){if(e[r]!==null||i.kind!=="optional"||b(i))continue;const s=i._meta?.inner;b(s)||o.add(r)}return o.size===0?e:Object.fromEntries(Object.entries(e).filter(([r])=>!o.has(r)))},T=new Set(["_creationTime","_id"]),_=(t,e)=>Object.keys(e).find(o=>!T.has(o)&&!Object.hasOwn(t.shape,o)),S=(t,e,o)=>{const r=t.tables[e];if(!r)return`unknown table: ${e}`;const i=_(r,o);if(i!==void 0)return`unexpected field "${i}": not declared in table "${e}"`;for(const[s,n]of Object.entries(r.shape)){const a=o[s],d=n.kind==="optional";if(a===void 0&&d)continue;const c=n.parse;if(typeof c=="function")try{c(a)}catch(l){const f=l instanceof Error?l.message:String(l);return`field "${s}": ${f}`}}},v=async(t,e,o,r)=>{try{return e?(await e.all(`SELECT 1 AS hit FROM ${u(o)} WHERE "id" = ? LIMIT 1`,[r])).length>0:await t.get(r)!==null}catch{return!1}},A=async(t,e,o,r,i)=>{const{doc:s,table:n}=r;if(typeof n!="string"||n.length===0)return{error:{code:"BAD_ROW",line:i,message:"row is missing `table`",table:n},kind:"error"};if(e.tables[n]?.shardMode?.kind!=="global")return{kind:"skip"};if(!s||typeof s!="object"||Array.isArray(s))return{error:{code:"BAD_ROW",line:i,message:"row is missing or malformed `doc`",table:n},kind:"error"};let a;try{a=g(s)}catch(l){return{error:{code:"BAD_ROW",line:i,message:l instanceof Error?l.message:String(l),table:n},kind:"error"}}a=I(e.tables[n],a);const d=S(e,n,a);if(d!==void 0)return{error:{code:"VALIDATION_ERROR",line:i,message:d,table:n},kind:"error"};const c=typeof a._id=="string"?a._id:void 0;if(c!==void 0&&await v(t,o.exec,n,c))return{kind:"conflict"};try{return await t.insert(n,a,{allowExplicitId:!0}),{inserted:n,kind:"inserted"}}catch(l){const{body:f}=p(l,{fallbackCode:"INSERT_FAILED"});return{error:{code:f.code,line:i,message:f.message,table:n},kind:"error"}}},$=async(t,e,o)=>{const r=[],i={};let s=0,n=(o.startLine??1)-1;for(const a of o.rows){n+=1;const d=a.line??n,c=await A(t,e,o,a,d);switch(c.kind){case"conflict":{s+=1;break}case"error":{r.push(c.error);break}case"inserted":{i[c.inserted]=(i[c.inserted]??0)+1;break}}}return{conflicts:s,errors:r,inserted:i}};export{F as exportGlobalRows,$ as importGlobalRows,k as selectGlobalTables};
@@ -0,0 +1 @@
1
+ import{LunoraError as N}from"@lunora/errors";import{isInternalTableName as L}from"@lunora/shard-engine";import{sqliteDecode as G,effectiveColumnKind as D,decodeGlobalRow as F,sqliteEncode as M}from"@lunora/sql-store";import{needsWireEncoding as _,encodeWire as A}from"@lunora/wire";import{runD1GlobalTableMigrations as R}from"./backfillD1SearchIndexes-BHgn1SNb.mjs";import{TRACKING_TABLE_NAME as U}from"./MigrationRunner-DADTo9Q8.mjs";import{quoteIdentifier as l}from"./quoteIdentifier-CObIFRhb.mjs";const P=50,q=500,B=30,k=200,S=(t,o,n)=>Math.min(Math.max(t,o),n),K=t=>L(t)||t.startsWith("d1_")||t===U,v=/password|secret|token|hash|salt|credential/iu,u=(t,o)=>{const n=t.tables[o];return n?.shardMode?.kind==="global"?n:void 0},O=async t=>(await t.all("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",[])).map(n=>String(n.name)).filter(n=>!K(n)),$=async(t,o,n)=>{if(await R(t,o),!(await O(t)).includes(n))throw new N("UNKNOWN_TABLE",`unknown table: ${n}`,{status:404})},g=async(t,o,n="",e=[])=>{const r=await t.all(`SELECT COUNT(*) AS c FROM ${o}${n}`,e);return Number(r[0]?.c??0)},h=(t,o,n)=>u(t,o)!==void 0&&n==="_id"?"id":n,y=(t,o,n)=>{const e=u(t,o)?.shape[n];return e===void 0?void 0:D(e)},W=/^-?\d+$/u,Y=(t,o)=>{if(o!=="bigint")return M(t);const n=typeof t=="number"||typeof t=="string"?String(t).trim():void 0;return M(n!==void 0&&W.test(n)?BigInt(n):t,o)},C=(t,o,n,e)=>{const r=[],a=[];for(const s of e??[]){if(!n.includes(s.column))throw new N("UNKNOWN_COLUMN",`unknown column: ${s.column}`,{status:404});if(u(t,o)===void 0&&v.test(s.column))throw new N("FORBIDDEN",`cannot filter on a redacted column: ${s.column}`,{status:403});const i=l(h(t,o,s.column));s.value===null||s.value===void 0?r.push(`${i} IS NULL`):(r.push(`${i} = ?`),a.push(Y(s.value,y(t,o,s.column))))}return{params:a,where:r.length>0?` WHERE ${r.join(" AND ")}`:""}},j=(t,o,n)=>{const e=u(t,o);if(e)return F(e,n);const r={};for(const[a,s]of Object.entries(n))r[a]=s!=null&&v.test(a)?"•••":s;return r},I=async(t,o,n)=>{const e=u(o,n);return e?["_id","_creationTime",...Object.keys(e.shape)]:(await t.all(`PRAGMA table_info(${l(n)})`,[])).map(a=>String(a.name))},X=async(t,o,n)=>{if(u(o,n))return;const e=await t.all(`PRAGMA foreign_key_list(${l(n)})`,[]);if(e.length===0)return;const r={};for(const a of e){const s=String(a.from),i=String(a.table);r[s]??=i}return r},nt=async(t,o)=>{await R(t,o);const n=await O(t);return Promise.all(n.map(async e=>({name:e,rowCount:await g(t,l(e))})))},H=async(t,o,n,e)=>{if(u(o,n)!==void 0)return l("id");const a=(await t.all(`PRAGMA table_info(${l(n)})`,[])).filter(s=>Number(s.pk??0)>0);return a.length===1?l(String(a[0]?.name)):e.includes("id")?l("id"):"rowid"},ot=async(t,o,n)=>{const{table:e}=n;await $(t,o,e);const r=S(Math.trunc(n.limit??P),1,q),a=Math.max(0,Math.trunc(n.offset??0)),s=l(e),i=await I(t,o,e),{params:m,where:d}=C(o,e,i,n.filters),f=await g(t,s,d,m),w=await H(t,o,e,i),b=(await t.all(`SELECT * FROM ${s}${d} ORDER BY ${w} LIMIT ? OFFSET ?`,[...m,r,a])).map(E=>{const c=j(o,e,E);return _(c)?A(c):c}),T=await X(t,o,e);return T===void 0?{columns:i,rows:b,total:f}:{columns:i,refs:T,rows:b,total:f}},et=async(t,o,n)=>{const{column:e,table:r}=n;await $(t,o,r);const a=await I(t,o,r);if(!a.includes(e))throw new N("UNKNOWN_COLUMN",`unknown column: ${e}`,{status:404});const s=l(r),{params:i,where:m}=C(o,r,a,n.filters);if(u(o,r)===void 0&&v.test(e)){const c=await g(t,s,m,i);return{truncated:!1,values:c===0?[]:[{count:c,value:"•••"}]}}const d=S(Math.trunc(n.limit??B),1,k),f=l(h(o,r,e)),w=await t.all(`SELECT ${f} AS value, COUNT(*) AS count FROM ${s}${m} GROUP BY ${f} ORDER BY count DESC, ${f} ASC LIMIT ?`,[...i,d+1]),p=w.length>d,b=p?w.slice(0,d):w,T=y(o,r,e)==="bigint",E=b.map(c=>({count:Number(c.count),value:T?G(c.value,"bigint"):c.value}));return{truncated:p,values:_(E)?A(E):E}};export{et as facetGlobalColumn,nt as listGlobalTables,ot as readGlobalTablePage};
@@ -0,0 +1 @@
1
+ const t=e=>`"${e.replaceAll('"','""')}"`;export{t as quoteIdentifier};
@@ -0,0 +1 @@
1
+ import{renderSql as t}from"@lunora/shard-engine";import{sql as i}from"drizzle-orm";import{MAX_D1_TABLE_COLUMNS as a,sqlAffinityForKind as n}from"../dialect.mjs";const s=/unique constraint failed/iu,m={companionTypes:{autoincrementPrimaryKey:"INTEGER PRIMARY KEY AUTOINCREMENT",integer:"INTEGER",key:"TEXT",real:"REAL",text:"TEXT"},columnType:n,frameworkColumns:()=>[{name:"id",type:"TEXT PRIMARY KEY"},{name:"_creationTime",type:"REAL NOT NULL"}],isUniqueViolation:e=>e instanceof Error&&s.test(e.message),maxTableColumns:a,name:"sqlite",render:e=>t("sqlite",e),searchBackfillHint:"call backfillD1SearchIndexes with the store's exec from a host-side admin path",supportsFts5:!0,supportsReturning:!0,tableExists:e=>i`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${e}`};export{m as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/d1",
3
- "version": "1.0.0-alpha.18",
3
+ "version": "1.0.0-alpha.181",
4
4
  "description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,9 +50,12 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/do": "1.0.0-alpha.18",
54
- "@lunora/sql-store": "1.0.0-alpha.18",
55
- "drizzle-orm": "^0.45.2"
53
+ "@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
54
+ "@lunora/platform": "1.0.0-alpha.64",
55
+ "@lunora/shard-engine": "1.0.0-alpha.130",
56
+ "@lunora/sql-store": "1.0.0-alpha.182",
57
+ "@lunora/wire": "1.0.0-alpha.1",
58
+ "drizzle-orm": "^0.45.3"
56
59
  },
57
60
  "engines": {
58
61
  "node": "^22.15.0 || >=24.11.0"
@@ -1,143 +0,0 @@
1
- import { drizzle } from 'drizzle-orm/d1';
2
-
3
- const STMT_CACHE_CAPACITY = 256;
4
- const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
5
- class D1Session {
6
- session;
7
- /** See {@link D1Client.stmtCache}. Scoped per session. */
8
- stmtCache = /* @__PURE__ */ new Map();
9
- constructor(session) {
10
- this.session = session;
11
- }
12
- prepare(sql) {
13
- const cached = this.stmtCache.get(sql);
14
- if (cached) {
15
- this.stmtCache.delete(sql);
16
- this.stmtCache.set(sql, cached);
17
- return cached;
18
- }
19
- const stmt = this.session.prepare(sql);
20
- if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
21
- const oldest = this.stmtCache.keys().next().value;
22
- if (oldest !== void 0) {
23
- this.stmtCache.delete(oldest);
24
- }
25
- }
26
- this.stmtCache.set(sql, stmt);
27
- return stmt;
28
- }
29
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
30
- async run(sql, ...binds) {
31
- return this.prepare(sql).bind(...binds).run();
32
- }
33
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
34
- async all(sql, ...binds) {
35
- return this.prepare(sql).bind(...binds).all();
36
- }
37
- async first(sql, ...binds) {
38
- return this.prepare(sql).bind(...binds).first();
39
- }
40
- /**
41
- * Returns the most recent bookmark known to the session, or `undefined`
42
- * when D1 has not issued one yet.
43
- */
44
- getBookmark() {
45
- return this.session.getBookmark() ?? void 0;
46
- }
47
- }
48
- class D1Client {
49
- db;
50
- /**
51
- * SQL string -> prepared statement. Prepared statements are reusable in
52
- * D1; preparing the same SQL twice forces the worker to round-trip the
53
- * statement plan. Caching is per-instance so unit-test isolation holds.
54
- * Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
55
- */
56
- stmtCache = /* @__PURE__ */ new Map();
57
- /**
58
- * Lazily-built drizzle handle over the bare binding. Memoised so a single
59
- * `D1Client` reuses the same dialect/session machinery across calls.
60
- */
61
- drizzleHandle;
62
- constructor(database) {
63
- this.db = database;
64
- }
65
- /**
66
- * Open a Sessions-API scoped session. Pass the bookmark forwarded by
67
- * the client to opt into read-your-writes consistency.
68
- *
69
- * With no bookmark this is the first request of a session — there is no
70
- * prior write to read, so we open with the explicit `"first-unconstrained"`
71
- * constraint (Cloudflare's lowest-latency default: the first read may serve
72
- * from any replica). Read-your-writes for sequenced requests still flows
73
- * through the forwarded bookmark; a caller needing a strongly-consistent
74
- * very-first read should pass `"first-primary"` as the bookmark instead.
75
- */
76
- withSession(bookmark) {
77
- const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
78
- return new D1Session(session);
79
- }
80
- /**
81
- * Prepare a statement, reusing a cached one when the SQL text matches.
82
- * `bind()` on a prepared statement returns a new bound statement and
83
- * leaves the underlying prepared plan reusable, so cache hits are safe
84
- * even when the previous caller already called `.bind(...).run()`.
85
- */
86
- prepare(sql) {
87
- const cached = this.stmtCache.get(sql);
88
- if (cached) {
89
- this.stmtCache.delete(sql);
90
- this.stmtCache.set(sql, cached);
91
- return cached;
92
- }
93
- const stmt = this.db.prepare(sql);
94
- if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
95
- const oldest = this.stmtCache.keys().next().value;
96
- if (oldest !== void 0) {
97
- this.stmtCache.delete(oldest);
98
- }
99
- }
100
- this.stmtCache.set(sql, stmt);
101
- return stmt;
102
- }
103
- /**
104
- * Drizzle handle over the bare `env.DB` binding. Used for typed queries
105
- * against generated `sqliteTable` schemas; does **not** participate in the
106
- * D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
107
- * {@link drizzleSession} instead.
108
- */
109
- get drizzle() {
110
- if (this.drizzleHandle) {
111
- return this.drizzleHandle;
112
- }
113
- this.drizzleHandle = drizzle(this.db, { logger: false });
114
- return this.drizzleHandle;
115
- }
116
- /**
117
- * Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
118
- * supplied, opts into read-your-writes consistency for follow-up reads on
119
- * the same session.
120
- *
121
- * A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
122
- * drizzle calls into, so a single `unknown` cast lets us treat the session
123
- * as a `D1Database` for driver-construction purposes.
124
- */
125
- drizzleSession(bookmark) {
126
- const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
127
- return drizzle(session, { logger: false });
128
- }
129
- /**
130
- * Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
131
- * exactly; exposed on the client so callers don't need to hold a drizzle
132
- * handle just to run a typed batch.
133
- */
134
- async batch(items) {
135
- return this.drizzle.batch(items);
136
- }
137
- /** Direct access to the underlying binding (advanced use only). */
138
- get raw() {
139
- return this.db;
140
- }
141
- }
142
-
143
- export { D1Client, D1Session };
@@ -1,149 +0,0 @@
1
- import { sql } from 'drizzle-orm';
2
- import { D1Client } from './D1Client-DA3flo1o.mjs';
3
-
4
- const TRACKING_TABLE_NAME = "__drizzle_migrations";
5
- const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at NUMERIC)`;
6
- const WHITESPACE_RE = /\s/u;
7
- const TRAILING_SEMICOLON_RE = /;\s*$/u;
8
- const SHA256_HEX_RE = /^[0-9a-f]{64}$/u;
9
- const assertSingleStatement = (migration) => {
10
- const text = migration.sql;
11
- let inSingle = false;
12
- let inDouble = false;
13
- let inLineComment = false;
14
- let inBlockComment = false;
15
- let seenStatement = false;
16
- for (let index = 0; index < text.length; index += 1) {
17
- const character = text[index];
18
- const next = text[index + 1];
19
- if (inLineComment) {
20
- if (character === "\n") {
21
- inLineComment = false;
22
- }
23
- continue;
24
- }
25
- if (inBlockComment) {
26
- if (character === "*" && next === "/") {
27
- inBlockComment = false;
28
- index += 1;
29
- }
30
- continue;
31
- }
32
- if (inSingle) {
33
- if (character === "'") {
34
- if (next === "'") {
35
- index += 1;
36
- } else {
37
- inSingle = false;
38
- }
39
- }
40
- continue;
41
- }
42
- if (inDouble) {
43
- if (character === '"') {
44
- if (next === '"') {
45
- index += 1;
46
- } else {
47
- inDouble = false;
48
- }
49
- }
50
- continue;
51
- }
52
- if (character === "'") {
53
- inSingle = true;
54
- continue;
55
- }
56
- if (character === '"') {
57
- inDouble = true;
58
- continue;
59
- }
60
- if (character === "-" && next === "-") {
61
- inLineComment = true;
62
- index += 1;
63
- continue;
64
- }
65
- if (character === "/" && next === "*") {
66
- inBlockComment = true;
67
- index += 1;
68
- continue;
69
- }
70
- if (character === ";") {
71
- seenStatement = true;
72
- continue;
73
- }
74
- if (seenStatement && character !== void 0 && !WHITESPACE_RE.test(character)) {
75
- throw new Error(
76
- `Migration "${migration.name}" (v${String(migration.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`
77
- );
78
- }
79
- }
80
- };
81
- const hashMigration = async (text) => {
82
- const bytes = new TextEncoder().encode(text);
83
- const digest = await crypto.subtle.digest("SHA-256", bytes);
84
- return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
85
- };
86
- class MigrationRunner {
87
- client;
88
- migrations;
89
- /**
90
- * Accepts either a {@link D1Client} (preferred — gets typed batches +
91
- * drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
92
- * the caller's behalf so existing `@lunora/cli` callers keep working).
93
- */
94
- constructor(database, migrations) {
95
- this.client = database instanceof D1Client ? database : new D1Client(database);
96
- this.migrations = [...migrations].toSorted((a, b) => a.version - b.version);
97
- this.assertUniqueVersions();
98
- this.assertUniqueSql();
99
- }
100
- async run() {
101
- await this.client.drizzle.run(sql.raw(TRACKING_TABLE_DDL));
102
- const appliedRows = await this.client.drizzle.all(sql.raw(`SELECT hash FROM ${TRACKING_TABLE_NAME}`));
103
- const appliedHashes = new Set(appliedRows.map((row) => row.hash));
104
- const applied = [];
105
- const skipped = [];
106
- const hashes = await Promise.all(this.migrations.map(async (migration) => hashMigration(migration.sql)));
107
- for (const [index, migration] of this.migrations.entries()) {
108
- const hash = hashes[index];
109
- if (appliedHashes.has(hash)) {
110
- skipped.push({ name: migration.name, version: migration.version });
111
- continue;
112
- }
113
- await this.applyOne(migration, hash);
114
- applied.push({ name: migration.name, version: migration.version });
115
- }
116
- return { applied, skipped };
117
- }
118
- async applyOne(migration, hash) {
119
- assertSingleStatement(migration);
120
- const statementText = migration.sql.replace(TRAILING_SEMICOLON_RE, "").trim();
121
- if (!SHA256_HEX_RE.test(hash)) {
122
- throw new Error(`migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
123
- }
124
- const trackingInsertSql = `INSERT INTO ${TRACKING_TABLE_NAME} (hash, created_at) VALUES ('${hash}', ${String(Date.now())})`;
125
- const items = [this.client.drizzle.run(sql.raw(statementText)), this.client.drizzle.run(sql.raw(trackingInsertSql))];
126
- await this.client.batch(items);
127
- }
128
- assertUniqueVersions() {
129
- const seen = /* @__PURE__ */ new Set();
130
- for (const m of this.migrations) {
131
- if (seen.has(m.version)) {
132
- throw new Error(`Duplicate migration version ${String(m.version)}`);
133
- }
134
- seen.add(m.version);
135
- }
136
- }
137
- assertUniqueSql() {
138
- const seen = /* @__PURE__ */ new Map();
139
- for (const m of this.migrations) {
140
- const previousVersion = seen.get(m.sql);
141
- if (previousVersion !== void 0) {
142
- throw new Error(`Migrations ${String(previousVersion)} and ${String(m.version)} have identical SQL — bump the content, not just the version.`);
143
- }
144
- seen.set(m.sql, m.version);
145
- }
146
- }
147
- }
148
-
149
- export { MigrationRunner };
@@ -1,14 +0,0 @@
1
- import { createSqlCtxDb, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from '@lunora/sql-store';
2
- export { createSqlCtxDb, decodeGlobalRow } from '@lunora/sql-store';
3
- import sqliteDialect from './sqliteDialect-DqYnHPuu.mjs';
4
-
5
- const createD1ContextDatabase = (options) => createSqlCtxDb({ ...options, dialect: sqliteDialect });
6
- const runD1GlobalTableMigrations = (exec, schema) => runSqlGlobalTableMigrations(exec, schema, sqliteDialect);
7
- const runD1AggregateMigrations = (exec, schema) => runSqlAggregateMigrations(exec, schema, sqliteDialect);
8
- const runD1RankMigrations = (exec, schema) => runSqlRankMigrations(exec, schema, sqliteDialect);
9
- const runD1SearchMigrations = (exec, schema) => runSqlSearchMigrations(exec, schema, sqliteDialect);
10
- const runD1CdcMigration = (exec) => runSqlCdcMigration(exec, sqliteDialect);
11
- const readD1CdcChanges = (exec, options = {}) => readSqlCdcChanges(exec, options, sqliteDialect);
12
- const trimD1CdcChanges = (exec, throughSeq) => trimSqlCdcChanges(exec, throughSeq, sqliteDialect);
13
-
14
- export { createD1ContextDatabase as createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges };
@@ -1,122 +0,0 @@
1
- import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
2
- import { decodeGlobalRow } from '@lunora/sql-store';
3
-
4
- const DEFAULT_BATCH_SIZE = 200;
5
- const selectGlobalTables = (schema, requested) => {
6
- const isGlobal = (table) => schema.tables[table]?.shardMode?.kind === "global";
7
- if (requested && requested.length > 0) {
8
- return requested.filter((name) => isGlobal(name));
9
- }
10
- return Object.keys(schema.tables).filter((name) => isGlobal(name));
11
- };
12
- const decodeRow = (schema, table, row) => {
13
- const definition = schema.tables[table];
14
- if (!definition) {
15
- return { _creationTime: row["_creationTime"], _id: row["id"] };
16
- }
17
- return decodeGlobalRow(definition, row);
18
- };
19
- const exportGlobalRows = async function* (exec, schema, args) {
20
- const tables = selectGlobalTables(schema, args.tables);
21
- const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
22
- for (const table of tables) {
23
- let offset = 0;
24
- let hasMore = true;
25
- while (hasMore) {
26
- const rows = await exec.all(`SELECT * FROM ${quoteIdentifier(table)} LIMIT ? OFFSET ?`, [batchSize, offset]);
27
- for (const row of rows) {
28
- yield { doc: decodeRow(schema, table, row), table };
29
- }
30
- hasMore = rows.length === batchSize;
31
- offset += rows.length;
32
- }
33
- }
34
- };
35
- const validateRow = (schema, table, document) => {
36
- const definition = schema.tables[table];
37
- if (!definition) {
38
- return `unknown table: ${table}`;
39
- }
40
- for (const [field, validator] of Object.entries(definition.shape)) {
41
- const candidate = document[field];
42
- const optional = validator.kind === "optional";
43
- if (candidate === void 0 && optional) {
44
- continue;
45
- }
46
- const parser = validator.parse;
47
- if (typeof parser !== "function") {
48
- continue;
49
- }
50
- try {
51
- parser(candidate);
52
- } catch (error) {
53
- const message = error instanceof Error ? error.message : String(error);
54
- return `field "${field}": ${message}`;
55
- }
56
- }
57
- return void 0;
58
- };
59
- const explicitIdConflicts = async (writer, exec, table, explicitId) => {
60
- try {
61
- if (exec) {
62
- const probe = await exec.all(`SELECT 1 AS hit FROM ${quoteIdentifier(table)} WHERE "id" = ? LIMIT 1`, [explicitId]);
63
- return probe.length > 0;
64
- }
65
- const existing = await writer.get(explicitId);
66
- return existing !== null;
67
- } catch {
68
- return false;
69
- }
70
- };
71
- const importOneRow = async (writer, schema, args, row, line) => {
72
- const { doc, table } = row;
73
- if (schema.tables[table]?.shardMode?.kind !== "global") {
74
- return { kind: "skip" };
75
- }
76
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
77
- return { error: { code: "BAD_ROW", line, message: "row is missing or malformed `doc`", table }, kind: "error" };
78
- }
79
- const failure = validateRow(schema, table, doc);
80
- if (failure !== void 0) {
81
- return { error: { code: "VALIDATION_ERROR", line, message: failure, table }, kind: "error" };
82
- }
83
- const explicitId = typeof doc["_id"] === "string" ? doc["_id"] : void 0;
84
- if (explicitId !== void 0 && await explicitIdConflicts(writer, args.exec, table, explicitId)) {
85
- return { kind: "conflict" };
86
- }
87
- try {
88
- await writer.insert(table, doc, { allowExplicitId: true });
89
- return { inserted: table, kind: "inserted" };
90
- } catch (error) {
91
- const code = error.code ?? "INSERT_FAILED";
92
- const message = error instanceof Error ? error.message : String(error);
93
- return { error: { code, line, message, table }, kind: "error" };
94
- }
95
- };
96
- const importGlobalRows = async (writer, schema, args) => {
97
- const errors = [];
98
- const inserted = {};
99
- let conflicts = 0;
100
- let line = (args.startLine ?? 1) - 1;
101
- for (const row of args.rows) {
102
- line += 1;
103
- const outcome = await importOneRow(writer, schema, args, row, line);
104
- switch (outcome.kind) {
105
- case "conflict": {
106
- conflicts += 1;
107
- break;
108
- }
109
- case "error": {
110
- errors.push(outcome.error);
111
- break;
112
- }
113
- case "inserted": {
114
- inserted[outcome.inserted] = (inserted[outcome.inserted] ?? 0) + 1;
115
- break;
116
- }
117
- }
118
- }
119
- return { conflicts, errors, inserted };
120
- };
121
-
122
- export { exportGlobalRows, importGlobalRows, selectGlobalTables };