@lunora/d1 1.0.0-alpha.18 → 1.0.0-alpha.180
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/LICENSE.md +6 -0
- package/dist/dialect.d.mts +69 -30
- package/dist/dialect.d.ts +69 -30
- package/dist/dialect.mjs +1 -36
- package/dist/index.d.mts +356 -165
- package/dist/index.d.ts +356 -165
- package/dist/index.mjs +1 -7
- package/dist/packem_shared/D1Client-CzS0Qfa6.mjs +1 -0
- package/dist/packem_shared/D1TimeoutError-D4YUqZPk.mjs +1 -0
- package/dist/packem_shared/MigrationRunner-DADTo9Q8.mjs +5 -0
- package/dist/packem_shared/OCC_VERSION_COLUMN-ByC64Jgx.mjs +1 -0
- package/dist/packem_shared/backfillD1SearchIndexes-BHgn1SNb.mjs +1 -0
- package/dist/packem_shared/d1QueryTag-BcEGW4q1.mjs +1 -0
- package/dist/packem_shared/exportGlobalRows-gh5TUaVm.mjs +1 -0
- package/dist/packem_shared/facetGlobalColumn-DcNoIRME.mjs +1 -0
- package/dist/packem_shared/quoteIdentifier-CObIFRhb.mjs +1 -0
- package/dist/packem_shared/sqliteDialect-B_3JNtdq.mjs +1 -0
- package/dist/packem_shared/wire-codec-9PgXH_2y.mjs +1 -0
- package/package.json +6 -4
- package/dist/packem_shared/D1Client-DA3flo1o.mjs +0 -143
- package/dist/packem_shared/MigrationRunner-BkEwQ-Ya.mjs +0 -149
- package/dist/packem_shared/createD1CtxDb-BMR8J0dT.mjs +0 -14
- package/dist/packem_shared/exportGlobalRows-DR6UiAZ8.mjs +0 -122
- package/dist/packem_shared/facetGlobalColumn-C6u_WMIY.mjs +0 -142
- package/dist/packem_shared/quoteIdentifier-B-ZeSe1V.mjs +0 -3
- package/dist/packem_shared/sqliteDialect-DqYnHPuu.mjs +0 -27
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
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-gh5TUaVm.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-DcNoIRME.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{n as E,e as m,d as g}from"./wire-codec-9PgXH_2y.mjs";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,l=!0;for(;l;){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 d=c.at(-1);d!==void 0&&(a=String(d.id)),l=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],l=n.kind==="optional";if(a===void 0&&l)continue;const c=n.parse;if(typeof c=="function")try{c(a)}catch(d){const f=d instanceof Error?d.message:String(d);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(d){return{error:{code:"BAD_ROW",line:i,message:d instanceof Error?d.message:String(d),table:n},kind:"error"}}a=I(e.tables[n],a);const l=S(e,n,a);if(l!==void 0)return{error:{code:"VALIDATION_ERROR",line:i,message:l,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(d){const{body:f}=p(d,{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 l=a.line??n,c=await A(t,e,o,a,l);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{n as _,e as A}from"./wire-codec-9PgXH_2y.mjs";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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const m=r=>{let t="";for(let o=0;o<r.length;o+=32768)t+=String.fromCharCode(...r.subarray(o,o+32768));return btoa(t)},E=r=>{const t=atob(r),s=new Uint8Array(t.length);for(let o=0;o<t.length;o+=1)s[o]=t.codePointAt(o)??0;return s},i="$lunora.wire$";const g="__proto__",w={BigInt64Array,BigUint64Array,Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array},A={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},l=r=>{if(r===null||typeof r!="object")return!1;const t=Object.getPrototypeOf(r);return t===null||t===Object.prototype},p=(r,t=0)=>{if(t>64)return!0;if(r===null)return!1;const s=typeof r;if(s==="string"||s==="boolean")return!1;if(s==="number")return!Number.isFinite(r);if(s!=="object")return!0;if(Array.isArray(r))return r.length>0&&r[0]===i?!0:r.some(o=>o===void 0||p(o,t+1));if(!l(r)||Object.hasOwn(r,g))return!0;for(const o of Object.keys(r)){const c=r[o];if(c!==void 0&&p(c,t+1))return!0}return!1},u=(r,t=0)=>{if(t>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(r===void 0)return[i,"undefined"];if(r===null)return null;const s=typeof r;if(s==="bigint"){const e=r.toString();if(e.length>1024)throw new RangeError("wire-codec: over-long bigint (max 1024 digits)");return[i,"bigint",e]}if(s==="number"){const e=r;return Number.isNaN(e)?[i,"nan"]:e===1/0?[i,"inf"]:e===-1/0?[i,"-inf"]:e}if(s!=="object")return r;if(r instanceof Date)return[i,"date",u(r.getTime(),t+1)];if(r instanceof Error){const e=r,n=u(Object.fromEntries(Object.keys(e).map(a=>[a,e[a]])),t+1),f=[i,"error",String(e.name),String(e.message),n];return e.cause!==void 0&&f.push(u(e.cause,t+1)),f}if(r instanceof URL)return[i,"url",r.href];if(r instanceof Map)return[i,"map",[...r.entries()].map(([e,n])=>[u(e,t+1),u(n,t+1)])];if(r instanceof Set)return[i,"set",[...r].map(e=>u(e,t+1))];if(r instanceof ArrayBuffer)return[i,"bytes",m(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const e=r,n=e.constructor.name,f=new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return n==="Uint8Array"?[i,"bytes",m(f)]:[i,"bytes",m(f),n]}if(Array.isArray(r)){const e=r.map(n=>u(n,t+1));return e.length>0&&e[0]===i?[i,"arr",e]:e}if(!l(r)){const e=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${e} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const o=r,c={};for(const e of Object.keys(o)){const n=o[e];if(n===void 0)continue;const f=u(n,t+1);e===g?Object.defineProperty(c,e,{configurable:!0,enumerable:!0,value:f,writable:!0}):c[e]=f}return c},y=(r,t=0)=>{if(t>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(r===null||typeof r!="object")return r;if(Array.isArray(r)){if(r[0]===i)switch(r[1]){case"-inf":return-1/0;case"arr":return r[2].map(e=>y(e,t+1));case"bigint":{const e=r[2];if(typeof e!="string"||e.length>1024||!/^-?\d+$/.test(e))throw new RangeError("wire-codec: invalid or over-long bigint (max 1024 digits)");return BigInt(e)}case"date":{const e=y(r[2],t+1);if(typeof e!="number")throw new TypeError("wire-codec: malformed date — epoch must be a number");return new Date(e)}case"map":{const e=r[2];return new Map(e.map(n=>{if(!Array.isArray(n)||n.length!==2)throw new TypeError("wire-codec: malformed map entry — expected a [key, value] pair");return[y(n[0],t+1),y(n[1],t+1)]}))}case"set":return new Set(r[2].map(e=>y(e,t+1)));case"url":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed url — href must be a string");return new URL(e)}case"error":{const e=r[2],n=r[3];if(typeof e!="string"||typeof n!="string")throw new TypeError("wire-codec: malformed error — name and message must be strings");const f=(Object.hasOwn(A,e)?A[e]:void 0)??Error,a=new f(n);a.name!==e&&Object.defineProperty(a,"name",{configurable:!0,value:e,writable:!0});const b=y(r[4],t+1);if(b===null||typeof b!="object"||Array.isArray(b))throw new TypeError("wire-codec: malformed error — props must be an object");for(const d of Object.keys(b))d===g?Object.defineProperty(a,d,{configurable:!0,enumerable:!0,value:b[d],writable:!0}):a[d]=b[d];return r.length>5&&Object.defineProperty(a,"cause",{configurable:!0,value:y(r[5],t+1),writable:!0}),a}case"bytes":{const e=r[2];if(typeof e!="string")throw new TypeError("wire-codec: malformed bytes — payload must be a base64 string");const n=E(e);if(m(n)!==e)throw new TypeError("wire-codec: malformed bytes — payload must be canonical padded base64");const f=r[3]??"Uint8Array";if(f==="ArrayBuffer")return n.buffer.byteLength===n.byteLength?n.buffer:n.slice().buffer;const a=Object.hasOwn(w,f)?w[f]:void 0;return a?new a(n.slice().buffer):n}case"inf":return 1/0;case"nan":return Number.NaN;case"undefined":return;default:return r.map(e=>y(e,t+1))}return r.map(c=>y(c,t+1))}const s=r,o={};for(const c of Object.keys(s)){const e=y(s[c],t+1);c===g?Object.defineProperty(o,c,{configurable:!0,enumerable:!0,value:e,writable:!0}):o[c]=e}return o};export{y as d,u as e,p as n};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/d1",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.180",
|
|
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,11 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/
|
|
54
|
-
"@lunora/
|
|
55
|
-
"
|
|
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.129",
|
|
56
|
+
"@lunora/sql-store": "1.0.0-alpha.181",
|
|
57
|
+
"drizzle-orm": "^0.45.3"
|
|
56
58
|
},
|
|
57
59
|
"engines": {
|
|
58
60
|
"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 };
|