From 0dba1abf2aafc733d6a6871ba9aa209fcaa71394 Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 14 Sep 2026 21:03:06 +0800 Subject: [PATCH] =?UTF-8?q?test(P0):=20v0.8.0=20=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=9F=BA=E5=BA=A7=E4=B8=8E=E5=B7=A5=E7=A8=8B=E9=97=A8=E7=A6=81?= =?UTF-8?q?=E6=A0=B9=E6=B2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作流 C-1 / C-3 前半 + 测试代码类型检查。 【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts - TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入 (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃) - 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现 导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness) - 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码 与 entry.content.subarray 恒等分支) - FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close() (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相) - 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组) 【覆盖率口径】jest.config.cjs - 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体 排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由 - 新增 coverageThreshold 门禁(此前完全不存在) - 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43% - 修正 testMatch 使 tests/helpers 下的测试可被发现 【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests - 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏) - 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode 以断言收窄替代 as any - 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码) - parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例, 当前红灯,对应总账第 11 项,将在工作流 A 修复) 【版本契约】新增 tests/version-contract.test.ts - 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量 【CI 门禁】.gitea/workflows/ci.yml - lint 去掉 continue-on-error(此前永远不让 CI 变红) - 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验 - 版本 0.7.4 升至 0.8.0 --- .gitea/workflows/ci.yml | 39 +- dist/metona-sqlark.cjs | 2 +- dist/metona-sqlark.d.ts | 2 +- dist/metona-sqlark.esm.js | 2 +- dist/metona-sqlark.js | 2 +- dist/metona-sqlark.min.js | 2 +- jest.config.cjs | 93 +++-- package.json | 11 +- src/constants.ts | 2 +- src/engine/index.ts | 6 + src/index.ts | 12 +- src/query/index.ts | 7 + src/sql/index.ts | 4 +- tests/aria-cascade.test.ts | 4 +- tests/e2e/opfs.spec.ts | 9 +- tests/engine/aria-advanced.test.ts | 5 +- tests/engine/aria-batch.test.ts | 4 +- tests/engine/aria-buffer.test.ts | 4 +- tests/engine/aria-cache.test.ts | 10 +- tests/engine/aria-checksum.test.ts | 4 +- tests/engine/aria-edge-ext.test.ts | 6 +- tests/engine/aria-encryption.test.ts | 19 +- tests/engine/aria-extra.test.ts | 4 +- tests/engine/aria-final.test.ts | 4 +- tests/engine/aria-idx-flush-race.test.ts | 4 +- tests/engine/aria-index-lookup.test.ts | 4 +- tests/engine/aria-kv-backend.test.ts | 2 +- tests/engine/aria-locks.test.ts | 4 +- tests/engine/aria-maintenance.test.ts | 9 +- tests/engine/aria-matrix-audit.test.ts | 4 +- tests/engine/aria-mvcc-savepoint.test.ts | 4 +- tests/engine/aria-opfs-backend.test.ts | 102 ++--- tests/engine/aria-page-store.test.ts | 17 +- tests/engine/aria-prod-load.test.ts | 6 +- tests/engine/aria-repair-hardening.test.ts | 18 +- tests/engine/aria-wal-segment.test.ts | 4 +- tests/engine/aria.test.ts | 4 +- tests/engine/kvstore.test.ts | 2 +- tests/foreign-key-cascade.test.ts | 2 +- tests/groupby.test.ts | 17 +- tests/helpers/assertions.ts | 81 ++++ tests/helpers/faulty-backend.ts | 242 +++++++++++ tests/helpers/opfs-mock.ts | 88 ---- tests/helpers/storage-harness.test.ts | 225 ++++++++++ tests/helpers/storage-harness.ts | 452 +++++++++++++++++++++ tests/hooks-crud.test.ts | 16 +- tests/hybrid/index.test.ts | 2 +- tests/maintenance-sql.test.ts | 5 +- tests/query/query-system.test.ts | 1 - tests/sql-ext.test.ts | 10 +- tests/sql-ext2.test.ts | 4 +- tests/sql/parser.test.ts | 160 ++++++-- tests/table/schema.test.ts | 2 +- tests/v025-fixes.test.ts | 14 +- tests/v033-fixes.test.ts | 7 +- tests/v040-features.test.ts | 18 +- tests/v042-fixes.test.ts | 17 +- tests/v042-hardening.test.ts | 8 +- tests/v043-hardening.test.ts | 45 +- tests/v044-hardening.test.ts | 8 +- tests/v045-hardening.test.ts | 6 +- tests/v073-fixes.test.ts | 43 +- tests/v074-fixes.test.ts | 3 +- tests/version-contract.test.ts | 45 ++ tsconfig.test.json | 25 ++ 65 files changed, 1533 insertions(+), 454 deletions(-) create mode 100644 tests/helpers/assertions.ts create mode 100644 tests/helpers/faulty-backend.ts delete mode 100644 tests/helpers/opfs-mock.ts create mode 100644 tests/helpers/storage-harness.test.ts create mode 100644 tests/helpers/storage-harness.ts create mode 100644 tests/version-contract.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f3fc8cb..0a9e99f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -28,18 +28,30 @@ jobs: - name: Install dependencies run: npm ci - - name: Type check + # v0.8.0: 源码类型检查 + - name: Type check (src) run: npm run typecheck + # v0.8.0 新增:**测试代码**类型检查。 + # 此前 tests/ 既不在 tsconfig include 中,babel-jest 又只剥离类型不做检查, + # 于是测试里的类型错误、拼写错误、未 await 的断言全部不可见 + # (审计实例:v025-fixes 里标题写 indexeddb、实际传 'opfs' 的用例长期存活)。 + - name: Type check (tests) + run: npm run typecheck:tests + + # v0.8.0: lint 不再 continue-on-error —— 此前 lint 失败永远不让 CI 变红, + # 形同虚设。现在 src 与 tests 都纳入,0 error 才通过。 - name: Lint run: npm run lint - continue-on-error: true # v0.7.2: Run tests 拆两步 —— 常规套件并行(快),重型套件串行(runInBand)。 # 此前重型测试(10 万行 kv/opfs、生产矩阵)与常规套件在慢 runner 上并行 # 争抢 CPU 与 4GB 堆 → 单测超时(120~180s)与 OOM 类假失败。 - - name: Run tests (regular suites) - run: npx jest --forceExit --maxWorkers=1 --no-cache --testPathIgnorePatterns='/node_modules/|/tests/e2e/|/tests/helpers/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' + # + # v0.8.0: 常规套件**带覆盖率**运行(此前 CI 从不跑 --coverage,且 jest.config + # 没有任何 coverageThreshold → 覆盖率掉到 0% 也全绿)。阈值定义在 jest.config.cjs。 + - name: Run tests (regular suites, with coverage gate) + run: npx jest --coverage --forceExit --maxWorkers=1 --no-cache --testPathIgnorePatterns='/node_modules/|/tests/e2e/|aria-prod-load|kvstore-stress|aria-matrix-audit|aria-idx-flush-race' env: NODE_OPTIONS: --max-old-space-size=4096 @@ -56,12 +68,26 @@ jobs: - name: Build run: npm run build + # v0.8.0: 校验仓库内 dist/ 与源码同步。 + # 此前 dist/ 已提交入库(42 个提交都在改 dist),但 CI 只 build 到工作区、 + # 不校验是否与源码一致 → 发布产物可以静默落后于源码。 + # 仅在单一 Node 版本上校验(构建结果与 Node 版本无关)。 + - name: Verify dist is in sync with src + if: matrix.node-version == '20.x' + run: | + if ! git diff --quiet -- dist/; then + echo "::error::dist/ 与源码不同步。请在本地运行 npm run build 并提交 dist/。" + git diff --stat -- dist/ + exit 1 + fi + echo "dist/ 与源码同步 ✓" + e2e: runs-on: debian-latest # 与 MetonaEditor 一致:Playwright 官方镜像(自带 Chromium 与全部系统依赖), # 避免 runner 宿主(Debian 11)不被 Playwright 1.62 支持的问题 container: mcr.microsoft.com/playwright:v1.62.1-noble - timeout-minutes: 15 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - name: Use Node.js @@ -75,5 +101,6 @@ jobs: run: npm run build - name: Verify Playwright browsers run: npx playwright install chromium - - name: Run E2E smoke tests + # v0.8.0: e2e 覆盖真实崩溃注入(CDP Page.crash)与真实 OPFS 语义 + - name: Run E2E tests run: npm run test:e2e diff --git a/dist/metona-sqlark.cjs b/dist/metona-sqlark.cjs index 0e7f8f2..a3c623b 100644 --- a/dist/metona-sqlark.cjs +++ b/dist/metona-sqlark.cjs @@ -34,7 +34,7 @@ class DatabaseError extends Error { // --------------------------------------------------------------------------- // 版本 // --------------------------------------------------------------------------- -const VERSION = '0.7.4'; +const VERSION = '0.8.0'; /** * metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑 diff --git a/dist/metona-sqlark.d.ts b/dist/metona-sqlark.d.ts index 6d88bdb..5b1660d 100644 --- a/dist/metona-sqlark.d.ts +++ b/dist/metona-sqlark.d.ts @@ -164,7 +164,7 @@ interface MetonaPlugin { /** 销毁 */ destroy(): void; } -declare const VERSION = "0.7.4"; +declare const VERSION = "0.8.0"; /** * metona-sqlark Plugin — 插件系统 diff --git a/dist/metona-sqlark.esm.js b/dist/metona-sqlark.esm.js index d00dade..3a2f8f3 100644 --- a/dist/metona-sqlark.esm.js +++ b/dist/metona-sqlark.esm.js @@ -30,7 +30,7 @@ class DatabaseError extends Error { // --------------------------------------------------------------------------- // 版本 // --------------------------------------------------------------------------- -const VERSION = '0.7.4'; +const VERSION = '0.8.0'; /** * metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑 diff --git a/dist/metona-sqlark.js b/dist/metona-sqlark.js index f6b1d9a..2114bce 100644 --- a/dist/metona-sqlark.js +++ b/dist/metona-sqlark.js @@ -36,7 +36,7 @@ // --------------------------------------------------------------------------- // 版本 // --------------------------------------------------------------------------- - const VERSION = '0.7.4'; + const VERSION = '0.8.0'; /** * metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑 diff --git a/dist/metona-sqlark.min.js b/dist/metona-sqlark.min.js index 9d0fef2..0126bb3 100644 --- a/dist/metona-sqlark.min.js +++ b/dist/metona-sqlark.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MetonaSqlark={})}(this,function(e){"use strict";const t=["string","number","boolean","date","json"],s=Object.freeze({name:"metona-sqlark",mode:"hybrid",diskEngine:"opfs",version:1,maxRowsPerQuery:0,debug:!1,multiTabSync:!1,aria:void 0});class n extends Error{constructor(e,t,s){super(e),this.code=t,this.details=s,this.name="DatabaseError"}}const i="0.7.4",r=new Map;function a(e){if(!e)return!1;for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"===t)return!0;if("object"==typeof s&&null!==s&&!Array.isArray(s))for(const[,e]of Object.entries(s))if("object"==typeof e&&null!==e&&("$subquery"in e||"$col"in e))return!0}else if(a(s))return!0}else if(s.some(e=>a(e)))return!0;return!1}function o(e,t,s={}){for(const[n,i]of Object.entries(t))if("$caseResult"!==n){if("$exists"!==n){if("$and"===n){if(!i.every(t=>o(e,t,s)))return!1;continue}if("$or"===n){if(!i.some(t=>o(e,t,s)))return!1;continue}if("$not"!==n){if(!h(e[n],i,e,s))return!1}else if(o(e,i,s))return!1}else if(!0!==i)return!1}else if(!0!==i)return!1;return!0}function h(e,t,s,n){if("object"==typeof t&&null!==t&&"$and"in t)return t.$and.every(e=>o(s,e,n));if("object"==typeof t&&null!==t&&"$or"in t)return t.$or.some(e=>o(s,e,n));if("object"==typeof t&&null!==t&&"$not"in t)return!h(e,t.$not,s,n);if("object"!=typeof t||null===t||Array.isArray(t))return e===t;const i=t;if(n.$col&&"$col"in i&&1===Object.keys(i).length)return e===s[i.$col];for(const[t,r]of Object.entries(i)){let i=r;if(n.$col&&"object"==typeof r&&null!==r&&"$col"in r&&(i=s[r.$col]),!c(e,t,i))return!1}return!0}function c(e,t,s){switch(t){case"$eq":return e===s;case"$ne":return e!==s;case"$gt":return e>s;case"$gte":return e>=s;case"$lt":return e{for(const{column:n,direction:i,nulls:r}of t){const t=null===e[n]||void 0===e[n],a=null===s[n]||void 0===s[n];if(r&&(t||a)){if(t&&a)continue;return"first"===r?t?-1:1:t?1:-1}const o=u(e[n],s[n]);if(0!==o)return"desc"===i?-o:o}return 0})}function u(e,t){return e===t?0:null==e?1:null==t?-1:"string"==typeof e&&"string"==typeof t?e.localeCompare(t):"number"==typeof e&&"number"==typeof t?e-t:String(e).localeCompare(String(t))}function f(e,t){const s={};for(const n of t)if(n in e)s[n]=e[n];else for(const t of Object.keys(e))if(t.endsWith(`.${n}`)||t===n){s[n]=e[t];break}return s}function d(e,s){return function(e){if(0===Object.keys(e).length)throw new n("Table must have at least one column","SCHEMA_ERROR");let s=0;for(const[i,r]of Object.entries(e)){if("__proto__"===i)throw new n('Column name "__proto__" is not allowed',"SCHEMA_ERROR");if(!t.includes(r.type))throw new n(`Invalid type "${r.type}" for column "${i}". Valid types: ${t.join(", ")}`,"SCHEMA_ERROR");r.primaryKey&&s++}if(0===s)throw new n("Table must have at least one primary key column","SCHEMA_ERROR");if(s>1)throw new n(`Composite primary keys are not supported yet: table has ${s} primary key columns. Use a single primary key column (or a unique column combination) instead.`,"SCHEMA_ERROR")}(s),{name:e,columns:s}}function p(e){const t={};for(const[s,n]of Object.entries(e))void 0!==n&&(t[s]=n);return t}function y(e){return{type:e.type,primaryKey:e.primaryKey,unique:e.unique,required:e.required,default:e.default,index:e.index,maxLength:e.maxLength,min:e.min,max:e.max,references:e.references,onDelete:e.onDelete,onUpdate:e.onUpdate}}class m{constructor(){this.name="memory",this.tables=new Map,this.schemas=new Map,this.indexes=new Map,this.opened=!1,this.metaStore=new Map,this.uniqueIndexCols=new Set,this.snapshot=null}async open(e,t){this.opened||(this.opened=!0)}async close(){this.tables.clear(),this.schemas.clear(),this.indexes.clear(),this.metaStore.clear(),this.opened=!1}isOpen(){return this.opened}async repair(){}async clearAll(){const e=Array.from(this.schemas.keys());for(const t of e)await this.dropTable(t);this.metaStore.clear()}async getMeta(e){return this.metaStore.get(e)??null}async setMeta(e,t){this.metaStore.set(e,t)}async createTable(e){if(this.schemas.has(e.name))throw new n(`Table "${e.name}" already exists`,"TABLE_EXISTS");const t={name:e.name,columns:{}};for(const[s,n]of Object.entries(e.columns))t.columns[s]={...n};this.schemas.set(e.name,t),this.tables.set(e.name,new Map);const s=new Map;for(const[e,n]of Object.entries(t.columns))(n.index||n.unique)&&s.set(e,new Map);this.indexes.set(e.name,s)}async dropTable(e){this.ensureTable(e),this.schemas.delete(e),this.tables.delete(e),this.indexes.delete(e);const t=`${e}:`;for(const e of this.uniqueIndexCols)e.startsWith(t)&&this.uniqueIndexCols.delete(e)}async hasTable(e){return this.schemas.has(e)}async getTableNames(){return Array.from(this.schemas.keys())}async getTableSchema(e){return this.schemas.get(e)??null}async alterTable(e,t,s){if(this.snapshot)throw new n("ALTER TABLE is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e);if("ADD"===t){if(i.columns[s.name])throw new n(`Column "${s.name}" already exists in table "${e}"`,"COLUMN_EXISTS");return void(i.columns[s.name]=s)}if(!i.columns[s.name])throw new n(`Column "${s.name}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");(i.columns[s.name].index||i.columns[s.name].unique)&&this.indexes.get(e)?.delete(s.name),delete i.columns[s.name];const r=this.tables.get(e);for(const e of r.values())s.name in e&&delete e[s.name]}async insert(e,t){this.ensureTable(e);const s=this.schemas.get(e),i=this.tables.get(e),r=this.getPrimaryKey(s),a=[],o=[],h=new Set,c=new Map;for(const a of t){const t=this.validateRow(s,a),l=String(t[r]);if(i.has(l)||h.has(l))throw new n(`Duplicate primary key "${l}" in table "${e}"`,"DUPLICATE_KEY");h.add(l),this.checkInsertUniqueness(s,e,t,c),o.push(t)}for(const t of o){const s=String(t[r]);i.set(s,t),this.updateIndexes(e,t,s),a.push(s)}return a}getRow(e,t){const s=this.tables.get(e);return s?s.get(t)??null:null}async find(e,t){this.ensureTable(e);const s=this.tables.get(e);let n=this.tryIndexLookup(e,s,t);t.where&&Object.keys(t.where).length>0&&(n=n.filter(e=>o(e,t.where))),t.orderBy&&t.orderBy.length>0&&(n=l(n,t.orderBy));const i=t.offset??0,r=t.limit??n.length;return n=n.slice(i,i+r),t.columns&&t.columns.length>0&&"*"!==t.columns[0]&&(n=n.map(e=>f(e,t.columns))),n}async findStream(e,t,s){this.ensureTable(e);const n=this.tables.get(e),i=!!(t.where&&Object.keys(t.where).length>0),r=t.limit??1/0,a=t.offset??0,h=t.columns&&t.columns.length>0&&"*"!==t.columns[0]?e=>f(e,t.columns):null;let c=0,l=0;for(const e of n.values())if(!i||o(e,t.where))if(l=r)break;return c}async update(e,t,s){this.ensureTable(e);const i=this.schemas.get(e),r=this.tables.get(e),h=this.getPrimaryKey(i),c=p(s);if(a(t.where))throw new n("Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");for(const t of Object.keys(c))if(!i.columns[t])throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const l=[],u=new Map;for(const[s,a]of r){if(t.where&&Object.keys(t.where).length>0&&!o(a,t.where))continue;const f={...a,...c};this.validateRow(i,f),this.checkUpdateUniqueness(i,e,s,f,u);const d=String(f[h]);if(d!==s&&r.has(d))throw new n(`Duplicate primary key "${d}" in table "${e}" (cannot update key to existing value)`,"DUPLICATE_KEY");l.push({pk:s,row:a,updated:f,newPk:d})}for(const t of l)t.newPk!==t.pk&&this.checkUpdateRestrict(e,t.pk);let f=0;for(const{pk:t,row:s,updated:n,newPk:i}of l)this.removeIndexEntries(e,s,t),i!==t&&await this.applyUpdateCascade(e,t,i),r.delete(t),r.set(i,n),this.updateIndexes(e,n,i),f++;return f}checkInsertUniqueness(e,t,s,i){const r=this.indexes.get(t);for(const[t,a]of Object.entries(e.columns)){if(!a.unique)continue;const o=s[t];if(null==o)continue;let h=i.get(t);if(h||(h=new Set,i.set(t,h)),h.has(o))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION");if(h.add(o),!r)continue;const c=r.get(t);if(c&&c.has(o))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION")}}checkUpdateUniqueness(e,t,s,i,r){const a=this.indexes.get(t);for(const[t,o]of Object.entries(e.columns)){if(!o.unique)continue;const h=i[t];if(null==h)continue;let c=r.get(t);if(c||(c=new Set,r.set(t,c)),c.has(h))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION");if(c.add(h),!a)continue;const l=a.get(t);if(l&&l.has(h)){const i=l.get(h);if(1!==i.size||!i.has(s))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION")}}}checkUpdateRestrict(e,t){for(const[s,i]of this.schemas)if(s!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i!==e)continue;const o=this.tables.get(s);if(!o)continue;let h=!1;for(const[,i]of o)if(String(i[r])===t&&(h=!0,"RESTRICT"===a.onUpdate))throw new n(`Cannot update "${e}" key "${t}": foreign key "${r}" in "${s}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if(h&&"SET NULL"===a.onUpdate&&a.required)throw new n(`Cannot update "${e}" key "${t}": foreign key "${r}" in "${s}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION")}}async applyUpdateCascade(e,t,s){for(const[n,i]of this.schemas)if(n!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i!==e)continue;const o=this.tables.get(n);if(o&&("CASCADE"===a.onUpdate||"SET NULL"===a.onUpdate))for(const[e,i]of o)String(i[r])===t&&(this.removeIndexEntries(n,i,e),i[r]="CASCADE"===a.onUpdate?s:null,this.updateIndexes(n,i,e))}}async delete(e,t){if(this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const s=this.tables.get(e),i=[];for(const[e,n]of s)t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||i.push({pk:e,row:n});const r=new Set;for(const{pk:t}of i)this.checkCascadeRestrict(e,t,r);let h=0;for(const{pk:t,row:s}of i)this.removeIndexEntries(e,s,t),h+=await this.cascadeDelete(e,t,s);for(const{pk:e}of i)s.delete(e);return i.length+h}checkCascadeRestrict(e,t,s){const i=`${e}:${t}`;if(!s.has(i)){s.add(i);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onDelete)continue;const[r]=o.references.split(".");if(r!==e)continue;const h=this.tables.get(i);if(!h)continue;const c=[];for(const[e,s]of h)String(s[a])===t&&c.push(e);if("RESTRICT"===o.onDelete&&c.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("SET NULL"===o.onDelete&&o.required&&c.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===o.onDelete)for(const e of c)this.checkCascadeRestrict(i,e,s)}}}async count(e,t){this.ensureTable(e);const s=this.tables.get(e);if(!t?.where||0===Object.keys(t.where).length)return s.size;let n=0;for(const e of s.values())o(e,t.where)&&n++;return n}async clear(e){this.ensureTable(e),this.tables.get(e).clear();const t=this.indexes.get(e);if(t)for(const e of t.values())e.clear()}async createIndex(e,t,s){if(this.snapshot)throw new n("CREATE INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.index||i.unique)return;const r=this.indexes.get(e);r.has(t)||r.set(t,new Map);const a=r.get(t),o=this.tables.get(e);try{for(const[i,r]of o){const o=r[t];if(null!=o){if(s&&a.has(o))throw new n(`Unique index on column "${t}" in table "${e}" cannot be created: duplicate value "${String(o)}"`,"UNIQUE_VIOLATION");a.has(o)||a.set(o,new Set),a.get(o).add(i)}}}catch(e){throw r.delete(t),e}i.index=!0,s&&(i.unique=!0,this.uniqueIndexCols.add(`${e}:${t}`))}async dropIndex(e,t,s){if(this.snapshot)throw new n("DROP INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(!i.index&&!i.unique)throw new n(`Index on column "${t}" does not exist in table "${e}"`,"INDEX_NOT_FOUND");const r=`${e}:${t}`;if(i.unique&&!this.uniqueIndexCols.has(r))throw new n(`Cannot drop index on column "${t}" in table "${e}": UNIQUE constraint defined at table creation must be removed by recreating the table`,"NOT_SUPPORTED");i.index=!1,i.unique=!1,this.uniqueIndexCols.delete(r);const a=this.indexes.get(e);a&&a.delete(t)}async beginTransaction(){if(this.snapshot)throw new n("Transaction already in progress","TX_ACTIVE");this.snapshot={tables:this.deepCloneMapMap(this.tables),schemas:new Map(this.schemas),indexes:this.deepCloneIndexes(this.indexes)}}async commitTransaction(){if(!this.snapshot)throw new n("No active transaction","TX_NONE");this.snapshot=null}async rollbackTransaction(){if(!this.snapshot)throw new n("No active transaction","TX_NONE");this.tables=this.snapshot.tables,this.schemas=this.snapshot.schemas,this.indexes=this.snapshot.indexes,this.snapshot=null}deepCloneMapMap(e){const t=new Map;for(const[s,n]of e){const e=new Map;for(const[t,s]of n)e.set(t,{...s});t.set(s,e)}return t}deepCloneIndexes(e){const t=new Map;for(const[s,n]of e){const e=new Map;for(const[t,s]of n){const n=new Map;for(const[e,t]of s)n.set(e,new Set(t));e.set(t,n)}t.set(s,e)}return t}ensureTable(e){if(!this.tables.has(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND")}getPrimaryKey(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}validateRow(e,t){const s={};for(const[i,r]of Object.entries(e.columns)){let a=t[i];if(void 0===a&&void 0!==r.default&&(a=r.default),r.required&&null==a)throw new n(`Column "${i}" is required in table "${e.name}"`,"VALIDATION_ERROR");if(r.primaryKey&&null==a)throw new n(`Primary key column "${i}" in table "${e.name}" cannot be null or undefined`,"VALIDATION_ERROR");null!=a&&this.checkType(i,r.type,a),void 0!==a&&(s[i]=a)}return s}checkType(e,t,s){const i=typeof s;switch(t){case"string":if("string"!==i)throw new n(`Column "${e}" expects string, got ${i}`,"TYPE_ERROR");break;case"number":if("number"!==i)throw new n(`Column "${e}" expects number, got ${i}`,"TYPE_ERROR");break;case"boolean":if("boolean"!==i)throw new n(`Column "${e}" expects boolean, got ${i}`,"TYPE_ERROR");break;case"date":if("string"!==i||isNaN(Date.parse(s)))throw new n(`Column "${e}" expects valid date`,"TYPE_ERROR");break;case"json":if("object"!==i)throw new n(`Column "${e}" expects object/array, got ${i}`,"TYPE_ERROR")}}tryIndexLookup(e,t,s){const n=this.indexes.get(e);if(!n||!s.where)return Array.from(t.values());const i=[],r=e=>{for(const[t,s]of Object.entries(e))if("$and"!==t)"$or"!==t&&"$not"!==t&&i.push([t,s]);else for(const e of s)r(e)};r(s.where);for(const[e,s]of i){let i;if("object"!=typeof s||null===s)i=s;else{if(!("$eq"in s)||1!==Object.keys(s).length)continue;i=s.$eq}if(null==i)continue;const r=n.get(e);if(r){const e=r.get(i);if(e){const s=[];for(const n of e){const e=t.get(n);e&&s.push(e)}return s}return[]}}return Array.from(t.values())}updateIndexes(e,t,s){const n=this.indexes.get(e);if(n)for(const[e,i]of n){const n=t[e];null!=n&&(i.has(n)||i.set(n,new Set),i.get(n).add(s))}}removeIndexEntries(e,t,s){const n=this.indexes.get(e);if(n)for(const[e,i]of n){const n=t[e];if(null!=n){const e=i.get(n);e&&(e.delete(s),0===e.size&&i.delete(n))}}}async cascadeDelete(e,t,s,i=new Set){const r=`${e}:${t}`;if(i.has(r))return 0;i.add(r);let a=0;for(const[s,r]of this.schemas)if(s!==e)for(const[o,h]of Object.entries(r.columns)){if(!h.references||!h.onDelete)continue;const[r]=h.references.split(".");if(r!==e)continue;const c=this.tables.get(s);if(!c)continue;const l=[];for(const[e,s]of c)String(s[o])===t&&l.push(e);if("RESTRICT"===h.onDelete&&l.length>0)throw new n(`Cannot delete from "${e}": foreign key "${o}" in "${s}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===h.onDelete)for(const e of l){const t=c.get(e);t&&(this.removeIndexEntries(s,t,e),a+=await this.cascadeDelete(s,e,t,i)),c.delete(e),a++}else if("SET NULL"===h.onDelete)for(const e of l){const t=c.get(e);t&&(this.removeIndexEntries(s,t,e),t[o]=null,this.updateIndexes(s,t,e))}}return a}}const g=[".crswap",".tmp"];class w{constructor(){this.root=null,this.dbDir=null,this.dbName="",this.writeQueue=Promise.resolve()}async open(e){this.dbName=e,this.root=await navigator.storage.getDirectory(),this.dbDir=await this.root.getDirectoryHandle(e,{create:!0}),await this.cleanupStaleFiles()}async close(){try{await this.writeQueue}catch{}this.dbDir=null,this.root=null}isOpen(){return null!==this.dbDir}async cleanupStaleFiles(){if(this.dbDir)try{const e=this.dbDir,t=[];for await(const[s]of e.entries())g.some(e=>s.endsWith(e))&&t.push(s);for(const e of t)try{await this.dbDir.removeEntry(e)}catch{}}catch{}}async read(e){if(!this.dbDir)return null;try{const t=await this.dbDir.getFileHandle(e),s=await t.getFile();return await s.arrayBuffer()}catch{return null}}async write(e,t){if(!this.dbDir)return;const s=this.writeQueue.then(async()=>{const s=await this.dbDir.getFileHandle(e,{create:!0}),n=await s.createWritable();await n.write(t),await n.close()});return this.writeQueue=s.then(()=>{},()=>{}),s}async append(e,t){if(!this.dbDir)return;const s=this.writeQueue.then(async()=>{const s=await this.dbDir.getFileHandle(e,{create:!0}),n=await s.getFile(),i=await s.createWritable({keepExistingData:!0});await i.write({type:"write",position:n.size,data:t}),await i.close()});return this.writeQueue=s.then(()=>{},()=>{}),s}async writeMany(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{for(const[t,s]of Object.entries(e)){const e=await this.dbDir.getFileHandle(t,{create:!0}),n=await e.createWritable();await n.write(s),await n.close()}});return this.writeQueue=t.then(()=>{},()=>{}),t}async delete(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{try{await this.dbDir.removeEntry(e)}catch{}});return this.writeQueue=t.then(()=>{},()=>{}),t}async deleteMany(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{for(const t of e)try{await this.dbDir.removeEntry(t)}catch{}});return this.writeQueue=t.then(()=>{},()=>{}),t}async listKeys(){if(!this.dbDir)return[];const e=[],t=this.dbDir;for await(const[s]of t.entries())e.push(s);return e}async exists(e){if(!this.dbDir)return!1;try{return await this.dbDir.getFileHandle(e),!0}catch{return!1}}async clear(){if(!this.dbDir)return;const e=this.dbDir;for await(const[t]of e.entries())try{await this.dbDir.removeEntry(t)}catch{}}}const b=new Map;class T{constructor(){this.dbName="",this.chunks=new Map,this.materialized=new Map}static clearRegistry(){b.clear()}async open(e){this.dbName=e,b.has(e)||b.set(e,new Map),this.chunks=b.get(e),this.materialized=new Map}async close(){this.chunks=new Map,this.materialized=new Map}isOpen(){return""!==this.dbName}async read(e){if(this.materialized.has(e))return this.materialized.get(e);const t=this.chunks.get(e);if(!t||0===t.length)return null;if(1===t.length)return this.materialized.set(e,t[0]),t[0];const s=t.reduce((e,t)=>e+t.byteLength,0),n=new Uint8Array(s);let i=0;for(const e of t)n.set(new Uint8Array(e),i),i+=e.byteLength;const r=n.buffer;return this.materialized.set(e,r),r}async write(e,t){this.chunks.set(e,[t]),this.materialized.set(e,t)}async append(e,t){const s=this.chunks.get(e);s?s.push(t):this.chunks.set(e,[t]),this.materialized.delete(e)}async writeMany(e){for(const[t,s]of Object.entries(e))this.chunks.set(t,[s]),this.materialized.set(t,s)}async delete(e){this.chunks.delete(e),this.materialized.delete(e)}async deleteMany(e){for(const t of e)this.chunks.delete(t),this.materialized.delete(t)}async listKeys(){return Array.from(this.chunks.keys())}async exists(e){return this.chunks.has(e)}async clear(){this.chunks.clear(),this.materialized.clear()}}const E=(()=>{const e=new Uint32Array(256);for(let t=0;t<256;t++){let s=t;for(let e=0;e<8;e++)s=1&s?3988292384^s>>>1:s>>>1;e[t]=s>>>0}return e})(),k=4294967295;function x(e,t=0){const s=function(e,t){let s=e>>>0;for(let e=0;e>>8)>>>0;return s>>>0}(t^k,e);return function(e){return(e^k)>>>0}(s)}var S;!function(e){e[e.PUT=1]="PUT",e[e.DELETE=2]="DELETE",e[e.APPEND=3]="APPEND"}(S||(S={}));const I=1263948622;function A(e,t){const s=new TextEncoder,n=Array.from(t.keys()),i=[];let r=12;for(const e of n){const n=s.encode(e),a=new Uint8Array(t.get(e));i.push({key:n,value:a}),r+=4+n.byteLength+4+a.byteLength}r+=4;const a=new Uint8Array(r),o=new DataView(a.buffer);let h=0;o.setUint32(h,I,!1),h+=4,o.setUint32(h,e,!1),h+=4,o.setUint32(h,i.length,!1),h+=4;for(const e of i)o.setUint32(h,e.key.byteLength,!1),h+=4,a.set(e.key,h),h+=e.key.byteLength,o.setUint32(h,e.value.byteLength,!1),h+=4,a.set(e.value,h),h+=e.value.byteLength;const c=x(a.subarray(0,r-4));return o.setUint32(r-4,c,!1),a}function R(e){if(e.byteLength<16)return null;const t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(t.getUint32(0,!1)!==I)return null;if(t.getUint32(e.byteLength-4,!1)!==x(e.subarray(0,e.byteLength-4)))return null;const s=new TextDecoder,n=new Map;let i=4;const r=t.getUint32(i,!1);i+=4;const a=t.getUint32(i,!1);i+=4;for(let r=0;re.byteLength-4)return null;const r=t.getUint32(i,!1);if(i+=4,i+r+4>e.byteLength-4)return null;const a=s.decode(e.subarray(i,i+r));i+=r;const o=t.getUint32(i,!1);if(i+=4,i+o>e.byteLength-4)return null;const h=e.slice(i,i+o).buffer;i+=o,n.set(a,h)}return{seq:r,entries:n}}const C="__kv_log",O="__kv_snapshot",N="__kv_meta";class L{constructor(e,t=16777216){this.dbName="",this.opened=!1,this.index=new Map,this.seq=0,this.logBytes=0,this.opQueue=Promise.resolve(),this.lastBackgroundError=null,this.medium=e??function(){const e=globalThis.navigator;return void 0!==e&&e.storage&&"function"==typeof e.storage.getDirectory?new w:new T}(),this.checkpointThreshold=t}isOpen(){return this.opened}async open(e){if(this.opened)return;this.dbName=e,this.lastBackgroundError=null,await this.medium.open(e),this.index=new Map,this.seq=0,this.logBytes=0;let t=0;const s=await this.medium.read(O);if(s){const e=R(new Uint8Array(s));e?(this.index=new Map(e.entries),this.seq=e.seq,t=e.seq):(this.index=new Map,this.seq=0,t=0)}const n=await this.medium.read(C);if(n&&n.byteLength>0){const e=new Uint8Array(n),s=t,i=[];(function(e,t,s){let n=0,i=0;const r=new DataView(e.buffer,e.byteOffset,e.byteLength),a=new TextDecoder;for(;n+4<=e.byteLength;){const o=r.getUint32(n,!1);if(o<12||n+4+o>e.byteLength){if(s&&!s(n))break;break}const h=n,c=n+4+o,l=e.subarray(h,c),u=new DataView(e.buffer,e.byteOffset+h,o+4);if(u.getUint32(o,!1)!==x(l.subarray(0,o))){if(s&&!s(h))break;break}let f=4;const d=u.getUint32(f,!1);f+=4;const p=u.getUint32(f,!1);f+=4;const y=[];let m=!0;for(let e=0;eo+4){m=!1;break}const e=u.getUint8(f);f+=1;const t=u.getUint32(f,!1);if(f+=4,f+t+4>o+4){m=!1;break}const s=a.decode(l.subarray(f,f+t));f+=t;const n=u.getUint32(f,!1);if(f+=4,f+n>o+4){m=!1;break}const i=l.slice(f,f+n).buffer;f+=n,y.push({op:e,key:s,value:i})}if(!m){if(s&&!s(h))break;break}t({seq:d,entries:y,raw:l}),i++,n=c}return i}(e,e=>{e.seq<=s||(this.applyRecord(e.entries),this.seq=e.seq)},e=>(i.push(e),!0))>0||i.length>0)&&(this.logBytes=e.byteLength),i.length>0&&await this.truncateLog()}this.opened=!0}async reload(){if(this.opened){try{await this.opQueue}catch{}this.index=new Map,this.seq=0,this.logBytes=0,this.opened=!1,await this.open(this.dbName)}}async close(){if(this.opened){try{await this.opQueue}catch{}await this.medium.close(),this.index.clear(),this.seq=0,this.logBytes=0,this.lastBackgroundError=null,this.opened=!1}}async get(e){return this.index.get(e)??null}async getAll(){return Array.from(this.index.entries())}async listKeys(){return Array.from(this.index.keys())}async exists(e){return this.index.has(e)}size(){return this.index.size}async put(e,t){await this.enqueue(async()=>{await this.appendRecord({[e]:t},[])})}async putMany(e){0!==Object.keys(e).length&&await this.enqueue(async()=>{await this.appendRecord(e,[])})}async delete(e){await this.enqueue(async()=>{await this.appendRecord({},[e])})}async deleteMany(e){0!==e.length&&await this.enqueue(async()=>{await this.appendRecord({},e)})}async writeBatch(e,t){0===Object.keys(e).length&&0===t.length||await this.enqueue(async()=>{await this.appendRecord(e,t)})}async appendValue(e,t){0!==t.byteLength&&await this.enqueue(async()=>{await this.appendRecord({},[],{[e]:t})})}async checkpoint(){await this.enqueue(async()=>{if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=A(this.seq,this.index);await this.medium.write(O,e.buffer);const t={seq:this.seq};await this.medium.write(N,(new TextEncoder).encode(JSON.stringify(t)).buffer),await this.truncateLog()})}async clear(){await this.enqueue(async()=>{await this.medium.clear(),this.index.clear(),this.seq=0,this.logBytes=0,await this.medium.write(N,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(O);t&&!R(new Uint8Array(t))&&(await this.medium.delete(O),e++);const s=await this.medium.read(C);if(s&&s.byteLength>0){const t=new Uint8Array(s),n=this.findValidLogLength(t);if(n{},()=>{}),t}async appendRecord(e,t,s={}){this.seq++;const i=function(e,t,s=[],n={}){const i=new TextEncoder,r=[];for(const[e,s]of Object.entries(t))r.push({op:S.PUT,key:e,value:s});for(const e of s)r.push({op:S.DELETE,key:e,value:new ArrayBuffer(0)});for(const[e,t]of Object.entries(n))r.push({op:S.APPEND,key:e,value:t});const a=[];let o=12;for(const e of r){const t=i.encode(e.key),s=new Uint8Array(e.value);a.push({op:e.op,key:t,value:s}),o+=5+t.byteLength+4+s.byteLength}o+=4;const h=new Uint8Array(o),c=new DataView(h.buffer);let l=0;c.setUint32(l,o-4,!1),l+=4,c.setUint32(l,e,!1),l+=4,c.setUint32(l,a.length,!1),l+=4;for(const e of a)c.setUint8(l,e.op),l+=1,c.setUint32(l,e.key.byteLength,!1),l+=4,h.set(e.key,l),l+=e.key.byteLength,c.setUint32(l,e.value.byteLength,!1),l+=4,h.set(e.value,l),l+=e.value.byteLength;const u=x(h.subarray(0,o-4));return c.setUint32(o-4,u,!1),h}(this.seq,e,t,s);try{const e=i.buffer.slice(i.byteOffset,i.byteOffset+i.byteLength);if("function"==typeof this.medium.append)await this.medium.append(C,e);else{const t=await this.medium.read(C);if(t){const s=new Uint8Array(t.byteLength+e.byteLength);s.set(new Uint8Array(t),0),s.set(new Uint8Array(e),t.byteLength),await this.medium.write(C,s.buffer)}else await this.medium.write(C,e)}}catch(e){throw this.seq--,this.lastBackgroundError=e,new n("KVStore log append failed","KV_LOG_ERROR",e)}for(const[t,s]of Object.entries(e))this.index.set(t,s);for(const e of t)this.index.delete(e);for(const[e,t]of Object.entries(s)){const s=this.index.get(e);if(s){const n=new Uint8Array(s.byteLength+t.byteLength);n.set(new Uint8Array(s),0),n.set(new Uint8Array(t),s.byteLength),this.index.set(e,n.buffer)}else this.index.set(e,t)}if(this.logBytes+=i.byteLength,this.checkpointThreshold>0&&this.logBytes>=this.checkpointThreshold){await this.medium.write(O,A(this.seq,this.index).buffer);const e={seq:this.seq};await this.medium.write(N,(new TextEncoder).encode(JSON.stringify(e)).buffer),await this.truncateLog()}}async truncateLog(){try{await this.medium.write(C,new ArrayBuffer(0))}catch{}this.logBytes=0}applyRecord(e){for(const t of e)if(t.op===S.PUT)this.index.set(t.key,t.value);else if(t.op===S.APPEND){const e=this.index.get(t.key);if(e){const s=new Uint8Array(e.byteLength+t.value.byteLength);s.set(new Uint8Array(e),0),s.set(new Uint8Array(t.value),e.byteLength),this.index.set(t.key,s.buffer)}else this.index.set(t.key,t.value)}else this.index.delete(t.key)}findValidLogLength(e){let t=0;const s=new DataView(e.buffer,e.byteOffset,e.byteLength);for(;t+4<=e.byteLength;){const n=s.getUint32(t,!1);if(n<12||t+4+n>e.byteLength)break;const i=e.subarray(t,t+4+n),r=s.getUint32(t+n,!1);if(x(i.subarray(0,n))!==r)break;t+=4+n}return t}}const v="__schema",$="t:",D=e=>(new TextEncoder).encode(e).buffer,U=e=>(new TextDecoder).decode(e);class _{constructor(e,t){this.name="kv",this.memory=new m,this.dbName="",this.version=1,this.opened=!1,this.txActive=!1,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set,this.kv=new L(e,t)}rowKey(e,t){return`${$}${e}:${t}`}rowPrefix(e){return`${$}${e}:`}async open(e,t){if(this.opened)return;this.dbName=e,this.version=t,await this.kv.open(e),await this.memory.open(e,t);const s=await this.kv.get(v);if(s)try{const e=JSON.parse(U(s));for(const t of Object.values(e))await this.memory.createTable(t)}catch{throw new n("Corrupted schema in KVStore","KV_SCHEMA_ERROR")}const i=await this.kv.getAll();for(const[e,t]of i){if(!e.startsWith($))continue;const s=e.indexOf(":",2);if(s<0)continue;const n=e.slice(2,s);if(await this.memory.hasTable(n))try{const e=JSON.parse(U(t));await this.memory.insert(n,[e])}catch{}}const r=await this.memory.getTableNames();for(const e of r){const t=await this.memory.getTableSchema(e);if(t)for(const[s,n]of Object.entries(t.columns))(n.index||n.unique)&&await this.memory.createIndex(e,s,n.unique)}this.opened=!0}async close(){if(this.opened){if(this.txActive)try{await this.rollbackTransaction()}catch{}try{await this.kv.checkpoint()}catch{}await this.kv.close(),await this.memory.close(),this.opened=!1}}isOpen(){return this.opened}async reload(){this.opened&&(await this.kv.reload(),await this.memory.close(),await this.memory.open(this.dbName,this.version),this.opened=!1,await this.open(this.dbName,this.version))}async repair(){this.ensureOpen(),await this.kv.repair(),await this.memory.close(),await this.memory.open(this.dbName,this.version),this.opened=!1,await this.open(this.dbName,this.version)}async clearAll(){this.ensureOpen(),await this.kv.clear(),await this.memory.clearAll()}async getMeta(e){const t=await this.kv.get(`__meta:${e}`);return t?U(t):null}async setMeta(e,t){await this.kv.put(`__meta:${e}`,D(t))}async createTable(e){if(this.ensureOpen(),await this.memory.createTable(e),this.txActive)return this.txDirtyTables.add(e.name),void(this.txSchemaChanged=!0);await this.persistSchema()}async dropTable(e){if(this.ensureOpen(),await this.memory.dropTable(e),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema();const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}async hasTable(e){return this.ensureOpen(),this.memory.hasTable(e)}async getTableNames(){return this.ensureOpen(),this.memory.getTableNames()}async getTableSchema(e){return this.ensureOpen(),this.memory.getTableSchema(e)}async alterTable(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("ALTER TABLE is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.alterTable(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);if(await this.persistSchema(),"DROP"===t){const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}}async insert(e,t){this.ensureOpen();const s=await this.memory.insert(e,t);if(this.txActive){if(this.txDirtyTables.add(e),this.txClearedTables.has(e))return this.txClearedTables.delete(e),this.txFullTables.add(e),s;let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of s)t.set(e,"put");return s}if(!await this.memory.getTableSchema(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const i={};for(const t of s){const s=this.memory.getRow(e,t);s&&(i[this.rowKey(e,t)]=D(JSON.stringify(s)))}return await this.kv.putMany(i),s}async find(e,t){return this.ensureOpen(),this.memory.find(e,t)}async findStream(e,t,s){return this.ensureOpen(),this.memory.findStream(e,t,s)}async update(e,t,s){this.ensureOpen();const i=await this.memory.getTableSchema(e);if(!i)throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const r=this.getPK(i),a=p(s),o=r in a,h=o?[]:await this.collectMatchingPks(e,t),c=await this.memory.update(e,t,a);if(this.txActive){if(this.txDirtyTables.add(e),o)for(const t of await this.affectedTables(e))this.txFullTables.add(t),this.txDirtyTables.add(t);else{let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of h)t.set(e,"put");for(const t of await this.affectedTables(e))t!==e&&(this.txFullTables.add(t),this.txDirtyTables.add(t))}return c}const l={},u=[];if(o)for(const t of await this.affectedTables(e)){const e=await this.collectTableDiff(t);Object.assign(l,e.puts),u.push(...e.deletes)}else{const t=new Set(h),s=await this.memory.find(e,{table:e});for(const n of s){const s=String(n[r]);t.has(s)&&(l[this.rowKey(e,s)]=D(JSON.stringify(n)),t.delete(s))}for(const s of t)u.push(this.rowKey(e,s));for(const t of await this.affectedTables(e)){if(t===e)continue;const s=await this.collectTableDiff(t);Object.assign(l,s.puts),u.push(...s.deletes)}}return await this.kv.writeBatch(l,u),c}async delete(e,t){this.ensureOpen();const s=await this.collectMatchingPks(e,t),n=await this.memory.delete(e,t);if(this.txActive){this.txDirtyTables.add(e);let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of s)t.set(e,"delete");for(const t of await this.affectedTables(e))t!==e&&(this.txFullTables.add(t),this.txDirtyTables.add(t));return n}const i={},r=s.map(t=>this.rowKey(e,t));for(const t of await this.affectedTables(e)){if(t===e)continue;const s=await this.collectTableDiff(t);Object.assign(i,s.puts),r.push(...s.deletes)}return await this.kv.writeBatch(i,r),n}async count(e,t){return this.ensureOpen(),this.memory.count(e,t)}async clear(e){if(this.ensureOpen(),await this.memory.clear(e),this.txActive)return this.txDirtyTables.add(e),this.txClearedTables.add(e),void this.txChanges.delete(e);const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}async createIndex(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("CREATE INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.createIndex(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema()}async dropIndex(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("DROP INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.dropIndex(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema()}async beginTransaction(){this.ensureOpen(),await this.memory.beginTransaction(),this.txActive=!0,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}async commitTransaction(){if(this.ensureOpen(),!this.txActive)throw new n("No active transaction","TX_NONE");const e={},t=[];for(const s of this.txDirtyTables){if(!await this.memory.hasTable(s)){const e=await this.kv.getAll(),n=this.rowPrefix(s);for(const[s]of e)s.startsWith(n)&&t.push(s);continue}if(this.txClearedTables.has(s)){const e=await this.kv.getAll(),n=this.rowPrefix(s);for(const[s]of e)s.startsWith(n)&&t.push(s);continue}if(this.txFullTables.has(s)){const n=await this.collectTableDiff(s);Object.assign(e,n.puts),t.push(...n.deletes);continue}const n=this.txChanges.get(s);if(n&&n.size>0){const i=await this.memory.getTableSchema(s);if(!i)continue;const r=this.getPK(i),a=new Map(n),o=await this.memory.find(s,{table:s});for(const n of o){const i=String(n[r]),o=a.get(i);void 0!==o&&("put"===o?e[this.rowKey(s,i)]=D(JSON.stringify(n)):t.push(this.rowKey(s,i)),a.delete(i))}for(const[e,n]of a)"delete"===n&&t.push(this.rowKey(s,e))}}await this.kv.writeBatch(e,t),this.txSchemaChanged&&await this.persistSchema(),await this.memory.commitTransaction(),this.txActive=!1,this.txDirtyTables=new Set,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}async rollbackTransaction(){if(this.ensureOpen(),!this.txActive)throw new n("No active transaction","TX_NONE");await this.memory.rollbackTransaction(),this.txActive=!1,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}ensureOpen(){if(!this.opened)throw new n("Database not opened","DB_NOT_OPEN")}getPK(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}async collectMatchingPks(e,t){const s=await this.memory.getTableSchema(e);if(!s)throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const i=this.getPK(s);return(await this.memory.find(e,t)).map(e=>String(e[i]))}async affectedTables(e){const t=new Set([e]);let s=!0;for(;s;){s=!1;for(const e of await this.memory.getTableNames()){if(t.has(e))continue;const n=await this.memory.getTableSchema(e);if(n)for(const i of Object.values(n.columns))if(i.references){const n=i.references.split(".")[0];if(t.has(n)){t.add(e),s=!0;break}}}}return t}async persistSchema(){const e={};for(const t of await this.memory.getTableNames()){const s=await this.memory.getTableSchema(t);s&&(e[t]=s)}await this.kv.put(v,D(JSON.stringify(e)))}async collectTableDiff(e){const t=this.rowPrefix(e),s={},n=[],i=await this.memory.getTableSchema(e);if(i){const r=this.getPK(i),a=await this.memory.find(e,{table:e}),o=new Set;for(const t of a){const n=this.rowKey(e,String(t[r]));o.add(n),s[n]=D(JSON.stringify(t))}const h=await this.kv.getAll();for(const[e]of h)e.startsWith(t)&&!o.has(e)&&n.push(e)}else{const e=await this.kv.getAll();for(const[s]of e)s.startsWith(t)&&n.push(s)}return{puts:s,deletes:n}}}const M=4096;var P;!function(e){e[e.DATA=1]="DATA",e[e.INDEX=2]="INDEX",e[e.OVERFLOW=3]="OVERFLOW",e[e.META=4]="META"}(P||(P={}));const B=4194304;var K,q;!function(e){e[e.INSERT=1]="INSERT",e[e.UPDATE=2]="UPDATE",e[e.DELETE=3]="DELETE",e[e.BEGIN=4]="BEGIN",e[e.COMMIT=5]="COMMIT",e[e.ROLLBACK=6]="ROLLBACK",e[e.CREATE_TABLE=7]="CREATE_TABLE",e[e.DROP_TABLE=8]="DROP_TABLE"}(K||(K={})),function(e){e[e.ACTIVE=1]="ACTIVE",e[e.COMMITTED=2]="COMMITTED",e[e.ABORTED=3]="ABORTED"}(q||(q={}));const z={pageSize:M,bufferPoolPages:256,memtableSizeThreshold:B,levelSizeMultiplier:10,bloomFilterBitsPerKey:10,walEnabled:!0,walSyncMode:"full",checkpointInterval:1e3,compression:!1,storageBackend:"opfs",walSizeThreshold:16777216,maxMemoryMB:64,encryption:void 0,pageStorage:void 0};var F;!function(e){e[e.RED=0]="RED",e[e.BLACK=1]="BLACK"}(F||(F={}));class j{constructor(e,t){this.color=F.RED,this.left=null,this.right=null,this.parent=null,this.key=e,this.value=t}}class V{constructor(){this.root=null,this._size=0}get size(){return this._size}insert(e,t){const s=new j(e,t);if(!this.root)return this.root=s,s.color=F.BLACK,void this._size++;let n=null,i=this.root;for(;i;)if(n=i,ei.key))return void(i.value=t);i=i.right}s.parent=n,et.key))return t.value;t=t.right}return null}delete(e){const t=this.findNode(e);return!!t&&(this.deleteNode(t),this._size--,!0)}inorder(e){this._inorder(this.root,e)}rangeScan(e,t,s){this._rangeScan(this.root,e,t,s)}*scanLazy(e,t){const s=[];let n=this.root;for(;n;)n.key>=e?(s.push(n),n=n.left):n=n.right;for(;s.length>0;){const i=s.pop();if(i.key>t)break;for(i.key>=e&&(yield[i.key,i.value]),n=i.right;n;)s.push(n),n=n.left}}getAllEntries(){const e=[];return this.inorder((t,s)=>e.push([t,s])),e}clear(){this.root=null,this._size=0}findNode(e){let t=this.root;for(;t;)if(et.key))return t;t=t.right}return null}deleteNode(e){if(e.left||e.right)if(e.left)if(e.right){const t=this.minimum(e.right),s=t.right,n=t.parent;n!==e&&(this.transplant(t,s),t.right=e.right,t.right.parent=t),this.transplant(e,t),t.left=e.left,t.left.parent=t;const i=t.color;if(t.color=e.color,i===F.BLACK){const i=s,r=i?i.parent:n===e?t:n;this.fixDelete(i,r)}}else this.transplant(e,e.left),e.color===F.BLACK&&this.fixDelete(e.left,e.left.parent);else this.transplant(e,e.right),e.color===F.BLACK&&this.fixDelete(e.right,e.right.parent);else this.transplant(e,null),e.color===F.BLACK&&this.fixDelete(null,e.parent)}transplant(e,t){e.parent?e===e.parent.left?e.parent.left=t:e.parent.right=t:this.root=t,t&&(t.parent=e.parent)}minimum(e){for(;e.left;)e=e.left;return e}fixInsert(e){for(;e.parent&&e.parent.color===F.RED;){const t=e.parent,s=t.parent;if(!s)break;if(t===s.left){const n=s.right;n&&n.color===F.RED?(t.color=F.BLACK,n.color=F.BLACK,s.color=F.RED,e=s):(e===t.right&&(e=t,this.rotateLeft(e)),e.parent&&(e.parent.color=F.BLACK),e.parent?.parent&&(e.parent.parent.color=F.RED),e.parent?.parent&&this.rotateRight(e.parent.parent))}else{const n=s.left;n&&n.color===F.RED?(t.color=F.BLACK,n.color=F.BLACK,s.color=F.RED,e=s):(e===t.left&&(e=t,this.rotateRight(e)),e.parent&&(e.parent.color=F.BLACK),e.parent?.parent&&(e.parent.parent.color=F.RED),e.parent?.parent&&this.rotateLeft(e.parent.parent))}}this.root&&(this.root.color=F.BLACK)}fixDelete(e,t){let s=e,n=t;for(;(!s||s.color===F.BLACK)&&s!==this.root&&n;)if(s===n.left){let e=n.right;if(!e)break;if(e.color===F.RED&&(e.color=F.BLACK,n.color=F.RED,this.rotateLeft(n),e=n.right,!e))break;const t=e.left,i=e.right;if(t&&t.color!==F.BLACK||i&&i.color!==F.BLACK){if(!(i&&i.color!==F.BLACK||(t&&(t.color=F.BLACK),e.color=F.RED,this.rotateRight(e),e=n.right,e)))break;e.color=n.color,n.color=F.BLACK,e.right&&(e.right.color=F.BLACK),this.rotateLeft(n),s=this.root}else e.color=F.RED,s=n,n=s.parent}else{let e=n.left;if(!e)break;if(e.color===F.RED&&(e.color=F.BLACK,n.color=F.RED,this.rotateRight(n),e=n.left,!e))break;const t=e.left,i=e.right;if(t&&t.color!==F.BLACK||i&&i.color!==F.BLACK){if(!(t&&t.color!==F.BLACK||(i&&(i.color=F.BLACK),e.color=F.RED,this.rotateLeft(e),e=n.left,e)))break;e.color=n.color,n.color=F.BLACK,e.left&&(e.left.color=F.BLACK),this.rotateRight(n),s=this.root}else e.color=F.RED,s=n,n=s.parent}s&&(s.color=F.BLACK)}rotateLeft(e){const t=e.right;t&&(e.right=t.left,t.left&&(t.left.parent=e),t.parent=e.parent,e.parent?e===e.parent.left?e.parent.left=t:e.parent.right=t:this.root=t,t.left=e,e.parent=t)}rotateRight(e){const t=e.left;t&&(e.left=t.right,t.right&&(t.right.parent=e),t.parent=e.parent,e.parent?e===e.parent.right?e.parent.right=t:e.parent.left=t:this.root=t,t.right=e,e.parent=t)}_inorder(e,t){e&&(this._inorder(e.left,t),t(e.key,e.value),this._inorder(e.right,t))}_rangeScan(e,t,s,n){e&&(e.key>t&&this._rangeScan(e.left,t,s,n),e.key>=t&&e.key<=s&&n(e.key,e.value),e.key=this.maxSize}getAllEntries(){return this.tree.getAllEntries()}rangeScan(e,t){const s=[];return this.tree.rangeScan(e,t,(e,t)=>s.push([e,t])),s}scanLazy(e,t){return this.tree.scanLazy(e,t)}getEntryCount(){return this.tree.size}getEstimatedSize(){return this._estimatedSize}clear(){this.tree.clear(),this._estimatedSize=0}estimateEntrySize(e,t){if(!t)return 0;let s=2*e.length;for(const e of Object.entries(t)){s+=2*e[0].length;const t=e[1];s+="string"==typeof t?2*t.length:"number"==typeof t?8:"boolean"==typeof t||null==t?1:16}return s}}class H{constructor(e,t=10){this._inserted=0;const s=Math.max(64,e*t),n=Math.ceil(s/8);this.bits=new Uint8Array(n),this.numHashes=Math.max(1,Math.floor(.69*t))}static fromData(e,t){const s=new H(1);return s.bits=e,s.numHashes=t,s}insert(e){const t=this.getHashes(e);for(const e of t){const t=Math.floor(e/8),s=e%8;this.bits[t]|=1<>>0;return t}murmurSimple(e){let t=0;for(let s=0;s>>16)>>>0;return Math.abs(t)}}const G=1397969987;class X{constructor(e=4096){this.entries=[],this.blockSizeLimit=e}add(e,t){this.entries.push([e,t])}build(){const e=new TextEncoder,t=this.entries.map(([t,s])=>({key:t,keyBytes:e.encode(t),valueBytes:e.encode(JSON.stringify(s))})),s=this.splitIntoBlocks(t),n=new H(this.entries.length);let i=0;const r=[];for(const e of s)r.push(i),i+=this.computeBlockSize(e);const a=[];for(let e=0;e=this.blockSizeLimit&&s.length>1&&(t.push(s.slice(0,-1)),s=[n]);return s.length>0&&t.push(s),t}computeBlockSize(e){let t=4;for(const s of e)t+=4+s.keyBytes.length+4+s.valueBytes.length;return t}writeDataBlock(e,t,s,n){e.setUint32(t,s.length,!1),t+=4;for(const i of s){if(i.keyBytes.length>4294967295||i.valueBytes.length>4294967295)throw new Error("SSTable entry too large (exceeds u32 length field)");e.setUint32(t,i.keyBytes.length,!1),t+=4,new Uint8Array(e.buffer).set(i.keyBytes,t),t+=i.keyBytes.length,e.setUint32(t,i.valueBytes.length,!1),t+=4,new Uint8Array(e.buffer).set(i.valueBytes,t),t+=i.valueBytes.length,n.insert(i.key)}return t}estimateIndexBlockSize(e){let t=4;const s=new TextEncoder;for(const n of e)t+=4+s.encode(n.key).byteLength+8;return t}writeIndexBlock(e,t,s){e.setUint32(t,s.length,!1),t+=4;for(const n of s){const s=(new TextEncoder).encode(n.key);e.setUint32(t,s.length,!1),t+=4,new Uint8Array(e.buffer).set(s,t),t+=s.length,e.setUint32(t,n.blockOffset,!1),t+=4,e.setUint32(t,n.blockSize,!1),t+=4}return t}}class J{constructor(e,t){this.indexEntries=[],this.entryCount=0,this.bloomFilter=null,this.format=2,this.storedChecksum=0,this.data=e,this.view=new DataView(e.buffer,e.byteOffset,e.byteLength),this.meta=t,this.parseFooter()}verifyChecksum(){return 0===this.storedChecksum||!(this.data.byteLength<4)&&x(this.data.subarray(0,this.data.byteLength-4))===this.storedChecksum}lenFieldSize(){return 2===this.format?4:2}readLen(e){return 2===this.format?this.view.getUint32(e,!1):this.view.getUint16(e,!1)}get(e){if(this.bloomFilter&&!this.bloomFilter.mayContain(e))return null;const t=this.locateBlock(e);if(t<0)return null;const s=this.indexEntries[t],n=this.getBlockData(s);if(!n)return null;const i=new DataView(n.buffer,n.byteOffset,n.byteLength),r=this.lenFieldSize(),a=i.getUint32(0,!1);let o=4;for(let t=0;tn.byteLength);t++){const t=2===this.format?i.getUint32(o,!1):i.getUint16(o,!1);if(o+=r,o+t+r>n.byteLength)break;const s=(new TextDecoder).decode(n.slice(o,o+t));o+=t;const a=2===this.format?i.getUint32(o,!1):i.getUint16(o,!1);if(o+=r,o+a>n.byteLength)break;const h=n.slice(o,o+a);if(o+=a,s===e)try{return JSON.parse((new TextDecoder).decode(h))}catch{return null}}return null}rangeScan(e,t,s){for(const[n,i]of this.scanLazy(e,t))s(n,i)}*scanLazy(e,t){if(0===this.indexEntries.length)return;const s=Math.max(0,this.locateBlockGE(e)),n=Math.min(this.indexEntries.length-1,this.locateBlockLE(t)+1);if(s<0||n<0||s>n)return;const i=this.lenFieldSize();for(let r=s;r<=n&&r>=0;r++){const s=this.indexEntries[r],n=this.getBlockData(s);if(!n)continue;const a=new DataView(n.buffer,n.byteOffset,n.byteLength),o=a.getUint32(0,!1);let h=4;for(let s=0;sn.byteLength);s++){const s=2===this.format?a.getUint32(h,!1):a.getUint16(h,!1);if(h+=i,h+s+i>n.byteLength)break;const r=(new TextDecoder).decode(n.slice(h,h+s));h+=s;const o=2===this.format?a.getUint32(h,!1):a.getUint16(h,!1);if(h+=i,h+o>n.byteLength)break;const c=n.slice(h,h+o);if(h+=o,r>=e&&r<=t)try{const e=JSON.parse((new TextDecoder).decode(c));yield[r,e]}catch{}}}}scanAll(e){const t=this.lenFieldSize();for(const s of this.indexEntries){const n=this.getBlockData(s);if(!n)continue;const i=new DataView(n.buffer,n.byteOffset,n.byteLength),r=i.getUint32(0,!1);let a=4;for(let s=0;sn.byteLength);s++){const s=2===this.format?i.getUint32(a,!1):i.getUint16(a,!1);if(a+=t,a+s+t>n.byteLength)break;const r=(new TextDecoder).decode(n.slice(a,a+s));a+=s;const o=2===this.format?i.getUint32(a,!1):i.getUint16(a,!1);if(a+=t,a+o>n.byteLength)break;const h=n.slice(a,a+o);a+=o;try{e(r,JSON.parse((new TextDecoder).decode(h)))}catch{}}}}parseFooter(){if(this.data.byteLength<32)throw new Error("SSTable too small: missing footer");const e=this.data.byteLength-32,t=this.view.getUint32(e+24,!1);if(1397969986===t)this.format=1;else{if(t!==G)throw new Error(`Invalid SSTable magic: expected 1397969986 or 1397969987, got ${t}`);this.format=2}const s=this.view.getUint32(e,!1),n=this.view.getUint32(e+4,!1),i=this.view.getUint32(e+8,!1),r=this.view.getUint32(e+12,!1),a=this.view.getUint32(e+16,!1);if(this.entryCount=this.view.getUint32(e+20,!1),this.storedChecksum=this.view.getUint32(e+28,!1),!(s+4>this.data.byteLength||s+n>this.data.byteLength)&&(this.parseIndexBlock(s,n),i>0&&r>0&&i+r<=this.data.byteLength))try{const e=this.data.slice(i,i+r);this.bloomFilter=H.fromData(e,a||10)}catch{}}parseIndexBlock(e,t){const s=this.view.getUint32(e,!1);e+=4;const n=this.lenFieldSize();for(let t=0;tthis.data.byteLength);t++){const t=this.readLen(e);if((e+=n)+t+8>this.data.byteLength)break;const s=(new TextDecoder).decode(this.data.slice(e,e+t));e+=t;const i=this.view.getUint32(e,!1);e+=4;const r=this.view.getUint32(e,!1);e+=4,0===r||i+r>this.data.byteLength||this.indexEntries.push({key:s,blockOffset:i,blockSize:r})}}getBlockData(e){return e.blockSize<=0||e.blockOffset<0||e.blockOffset+e.blockSize>this.data.byteLength?null:new Uint8Array(this.data.buffer,this.data.byteOffset+e.blockOffset,e.blockSize)}locateBlock(e){let t=0,s=this.indexEntries.length-1;for(;t<=s;){const n=Math.floor((t+s)/2);if(e<=this.indexEntries[n].key){if(e>(0===n?"":this.indexEntries[n-1].key))return n;s=n-1}else t=n+1}return-1}locateBlockGE(e){let t=0,s=this.indexEntries.length;for(;t>1;this.indexEntries[n].key>1;this.indexEntries[n].key<=e?t=n+1:s=n}return t>0?t-1:0}}class Q{constructor(e){this.index=0,this.entries=e}next(){return this.index>=this.entries.length?null:this.entries[this.index++]}reset(){this.index=0}}class Y{constructor(e){this.iter=e}next(){const e=this.iter.next();return e.done?null:e.value}reset(){}}class Z{constructor(){this.heap=[]}push(e){this.heap.push(e),this.bubbleUp(this.heap.length-1)}pop(){if(0===this.heap.length)return null;if(1===this.heap.length)return this.heap.pop();const e=this.heap[0];return this.heap[0]=this.heap.pop(),this.bubbleDown(0),e}peek(){return this.heap.length>0?this.heap[0]:null}get size(){return this.heap.length}bubbleUp(e){for(;e>0;){const t=Math.floor((e-1)/2);if(this.heap[e].key>=this.heap[t].key)break;[this.heap[e],this.heap[t]]=[this.heap[t],this.heap[e]],e=t}}bubbleDown(e){const t=this.heap.length;for(;;){let s=e;const n=2*e+1,i=2*e+2;if(n=0&&e.level<7&&this.levels[e.level].push(e);for(let e=0;e<7;e++)this.levels[e].sort((e,t)=>t.id-e.id);this.initialized=!0}put(e,t){this.levels[0].length>=8&&this.enqueueCompact(0),this.memtable.put(e,t),this.operationCount++,this.memtable.shouldFlush()&&this.freezeMemtable()}delete(e){this.levels[0].length>=8&&this.enqueueCompact(0),this.memtable.put(e,{__tombstone:!0}),this.operationCount++,this.memtable.shouldFlush()&&this.freezeMemtable()}getEstimatedMemory(){let e=this.memtable.getEstimatedSize();for(const t of this.frozenMemtables)e+=t.getEstimatedSize();return e+=this.cacheSize,e}freezeMemtable(){if(this.immutableMemtable){const e=this.immutableMemtable;this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e))}this.immutableMemtable=this.memtable,this.frozenMemtables.push(this.immutableMemtable),this.memtable=new W(this.memtableSizeThreshold)}enqueueOnChain(e){return this.flushChain.then(e).catch(e=>{this.lastBackgroundError=e})}async drainChain(){for(;;){const e=this.flushChain;if(await e,this.flushChain===e)return}}async flushImmutableAsync(e){const t=e.getAllEntries();if(0===t.length)return void(this.immutableMemtable===e&&(this.immutableMemtable=null));const s=await this.sstableStore.allocateId(),n=new X(this.blockSize);for(const[e,s]of t)n.add(e,s);const{sstableData:i,indexEntries:r}=n.build(),a={id:s,level:0,minKey:t[0][0],maxKey:t[t.length-1][0],blockCount:r.length,totalSize:i.byteLength,bloomData:null};this.cacheSSTable(s,i),this.trimCache(),await this.sstableStore.save(s,i),await this.sstableStore.saveMeta(a),this.immutableMemtable===e&&(this.immutableMemtable=null),this.levels[0].unshift(a),this.frozenMemtables=this.frozenMemtables.filter(t=>t!==e),this.levels[0].length>=4&&!this.compacting&&this.scheduleCompact(0)}scheduleCompact(e){e>=6||this.compacting||(this.compacting=!0,this.flushChain=this.enqueueOnChain(()=>this.compactLevelAsync(e).finally(()=>{this.compacting=!1,this.levels[e].length>=4&&this.scheduleCompact(e),e+1<6&&this.levels[e+1].length>=4&&this.scheduleCompact(e+1)})))}enqueueCompact(e){e>=6||this.compacting||(this.compacting=!0,this.flushChain=this.flushChain.then(async()=>{try{await this.compactLevelAsync(e)}finally{this.compacting=!1}}).catch(e=>{this.compacting=!1,this.lastBackgroundError=e}))}async prefetchRange(e,t){await this.drainChain(),this.trimCache();const s=[];for(let n=0;n<7;n++)for(const i of this.levels[n])ti.maxKey||this.sstableCache.has(i.id)||s.push(i.id);for(const e of s){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}async prefetchKeys(e){if(0===e.length)return;await this.drainChain(),this.trimCache();const t=new Set;for(let s=0;s<7;s++)for(const n of this.levels[s])if(!this.sstableCache.has(n.id))for(const s of e)if(s>=n.minKey&&s<=n.maxKey){t.add(n.id);break}for(const e of t){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}async prefetchPrefixRanges(e){if(0===e.length)return;const t=new Set,s=[];for(const n of e){const e=`${n[0]}\0${n[1]}`;t.has(e)||(t.add(e),s.push(n))}if(0===s.length)return;await this.drainChain(),this.trimCache();const n=new Set;for(let e=0;e<7;e++)for(const t of this.levels[e])if(!this.sstableCache.has(t.id))for(const[e,i]of s)if(!(it.maxKey)){n.add(t.id);break}for(const e of n){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}findMetaById(e){for(let t=0;t<7;t++)for(const s of this.levels[t])if(s.id===e)return s}get(e){let t=this.memtable.get(e);if(null!==t)return this.unwrapTombstone(t);for(let s=this.frozenMemtables.length-1;s>=0;s--)if(t=this.frozenMemtables[s].get(e),null!==t)return this.unwrapTombstone(t);for(let t=0;t<7;t++)for(const s of this.levels[t]){if(es.maxKey)continue;const t=this.loadSSTableReader(s);if(!t)continue;const n=t.get(e);if(null!==n)return this.unwrapTombstone(n)}return null}rangeScan(e,t){const s=[];return this.rangeScanLazy(e,t,(e,t)=>{s.push([e,t])}),s}rangeScanLazy(e,t,s){const n=new ee;n.addSource(new Y(this.memtable.scanLazy(e,t)));for(let s=this.frozenMemtables.length-1;s>=0;s--)n.addSource(new Y(this.frozenMemtables[s].scanLazy(e,t)));for(let s=0;s<7;s++)for(const i of this.levels[s]){if(ti.maxKey)continue;const s=this.loadSSTableReader(i);s&&n.addSource(new Y(s.scanLazy(e,t)))}let i=n.next();for(;i;){const[e,t]=i;if(!t.__tombstone&&!1===s(e,t))return;i=n.next()}}async compactLevel(e){await this.compactLevelAsync(e,2)}async compactLevelAsync(e,t=4){if(e>=6)return;if(this.levels[e].lengthr.push([e,t])),n.addSource(new Q(r)),i.push(e)}const r=n.drain();if(0===r.length){for(const t of i)this.levels[e].push(t);return void this.levels[e].sort((e,t)=>t.id-e.id)}const a=await this.sstableStore.allocateId(),o=new X(this.blockSize);for(const[e,t]of r)o.add(e,t);const{sstableData:h,indexEntries:c}=o.build(),l={id:a,level:e+1,minKey:r[0][0],maxKey:r[r.length-1][0],blockCount:c.length,totalSize:h.byteLength,bloomData:null};this.cacheSSTable(a,h),this.trimCache(),await this.sstableStore.save(a,h),await this.sstableStore.saveMeta(l),this.levels[e+1].unshift(l);for(const e of s)this.sstableCache.delete(e.id),await this.sstableStore.delete(e.id),await this.sstableStore.deleteMeta(e.id)}async flush(){if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("AriaEngine background flush/compaction failed (data may be inconsistent)","ARIA_BACKGROUND_ERROR",e)}if(await this.drainChain(),this.immutableMemtable){const e=this.immutableMemtable;this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e)),this.immutableMemtable=null}if(this.memtable.getEntryCount()>0){this.freezeMemtable();const e=this.immutableMemtable;e&&(this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e)),this.immutableMemtable=null)}await this.drainChain(),this.frozenMemtables=this.frozenMemtables.filter(e=>e.getEntryCount()>0)}async clear(){await this.drainChain(),this.memtable.clear(),this.immutableMemtable=null,this.frozenMemtables=[];for(const e of this.levels)for(const t of e)this.sstableCache.delete(t.id),this.sstableStore.delete(t.id).catch(()=>{}),this.sstableStore.deleteMeta(t.id).catch(()=>{});this.levels=[];for(let e=0;e<7;e++)this.levels.push([]);this.sstableCache.clear(),this.cacheSize=0}getStats(){return{memtableSize:this.memtable.getEntryCount(),sstableCount:this.levels.reduce((e,t)=>e+t.length,0),levelCounts:this.levels.map(e=>e.length)}}async validateAll(){let e=0;for(let t=0;t<7;t++){const s=[];for(const n of this.levels[t])await this.validateSSTable(n)?s.push(n):(this.sstableCache.delete(n.id),e++);this.levels[t]=s}return e}async validateSSTable(e){try{const t=await this.sstableStore.load(e.id);if(!t)return this.dropInvalidSSTable(e),!1;if(t.byteLength<32)return this.dropInvalidSSTable(e),!1;try{if(!new J(t,e).verifyChecksum())return this.dropInvalidSSTable(e),!1}catch{return this.dropInvalidSSTable(e),!1}return!0}catch{return this.dropInvalidSSTable(e),!1}}async dropInvalidSSTable(e){try{await this.sstableStore.delete(e.id)}catch{}try{await this.sstableStore.deleteMeta(e.id)}catch{}}unwrapTombstone(e){return e?e.__tombstone?null:e:null}loadSSTableReader(e){const t=this.sstableCache.get(e.id);if(!t)return null;this.sstableCache.delete(e.id),this.sstableCache.set(e.id,t);try{return new J(t,e)}catch{return null}}async preloadSSTable(e,t){if(this.sstableCache.has(e))return;const s=await this.sstableStore.load(e);if(s){if(t)try{if(!new J(s,t).verifyChecksum())return void await this.dropInvalidSSTable(t)}catch{return void await this.dropInvalidSSTable(t)}this.cacheSSTable(e,s)}}cacheSSTable(e,t){this.sstableCache.has(e)&&(this.cacheSize-=this.sstableCache.get(e).byteLength,this.sstableCache.delete(e)),this.sstableCache.set(e,t),this.cacheSize+=t.byteLength}trimCache(){for(;this.cacheSize>this.cacheLimitBytes&&this.sstableCache.size>0;){const e=this.sstableCache.keys().next().value,t=this.sstableCache.get(e);this.cacheSize-=t.byteLength,this.sstableCache.delete(e)}}}class se{constructor(e,t=!0,s="batch"){this.lsn=0,this.buffer=[],this.bufferedBytes=0,this.store=e,this.enabled=t,this.syncMode=s}async append(e){if(!this.enabled)return;this.lsn++;const t={...e,lsn:this.lsn,checksum:0},s=this.encodeRecord(t);"full"===this.syncMode?(await this.store.append(s),this.bufferedBytes+=s.byteLength):"batch"===this.syncMode&&(this.buffer.push(s),this.bufferedBytes+=s.byteLength)}async appendBatch(e){if(!this.enabled||0===e.length)return;const t=[];for(const s of e)this.lsn++,t.push(this.encodeRecord({...s,lsn:this.lsn,checksum:0}));const s=this.mergeChunks(t);"full"===this.syncMode?(await this.store.append(s),this.bufferedBytes+=s.byteLength):"batch"===this.syncMode&&(this.buffer.push(s),this.bufferedBytes+=s.byteLength)}async flush(){if(!this.enabled||0===this.buffer.length)return;const e=this.mergeChunks(this.buffer);await this.store.append(e),this.buffer=[]}mergeChunks(e){if(1===e.length)return e[0];const t=e.reduce((e,t)=>e+t.byteLength,0),s=new Uint8Array(t);let n=0;for(const t of e)s.set(t,n),n+=t.byteLength;return s}async recover(e){if(!this.enabled)return 0;if(!await this.store.exists())return 0;const t=await this.store.readAll();if(0===t.byteLength)return 0;const s=this.decodeAllRecords(t);for(const t of s)e(t);return this.lsn=s.length>0?s[s.length-1].lsn:0,s.length}async checkpoint(){this.enabled&&(await this.flush(),await this.store.truncate(),this.lsn=0,this.bufferedBytes=0)}getBufferedCount(){return this.buffer.length}getBufferedBytes(){return this.bufferedBytes}legacyChecksum(e){let t=0;for(let s=0;s>>0}encodeRecord(e){const t=new TextEncoder,s=t.encode(e.tableName),n=t.encode(e.key),i=e.data?JSON.stringify(e.data):"",r=t.encode(i),a=11+s.length+2+n.length+4+r.length+4,o=new ArrayBuffer(a),h=new DataView(o);let c=0;h.setUint32(c,e.lsn,!1),c+=4,h.setUint8(c,e.type),c+=1,h.setUint32(c,e.txnId,!1),c+=4,h.setUint16(c,s.length,!1),c+=2,new Uint8Array(o).set(s,c),c+=s.length,h.setUint16(c,n.length,!1),c+=2,new Uint8Array(o).set(n,c),c+=n.length,h.setUint32(c,r.length,!1),c+=4,new Uint8Array(o).set(r,c),c+=r.length;const l=x(new Uint8Array(o,0,c));return h.setUint32(c,l,!1),new Uint8Array(o)}decodeAllRecords(e){const t=[],s=new DataView(e.buffer,e.byteOffset,e.byteLength);let n=0;for(;n+15<=e.byteLength;)try{const i=n,r=s.getUint32(n,!1);n+=4;const a=s.getUint8(n);n+=1;const o=s.getUint32(n,!1);n+=4;const h=s.getUint16(n,!1);if(n+=2,n+h>e.byteLength)break;const c=(new TextDecoder).decode(e.slice(n,n+h));n+=h;const l=s.getUint16(n,!1);if(n+=2,n+l>e.byteLength)break;const u=(new TextDecoder).decode(e.slice(n,n+l));n+=l;const f=s.getUint32(n,!1);if(n+=4,n+f>e.byteLength)break;let d;if(f>0){const t=(new TextDecoder).decode(e.slice(n,n+f));try{d=JSON.parse(t)}catch{}}n+=f;const p=s.getUint32(n,!1);n+=4;const y=e.slice(i,n-4),m=x(y),g=this.legacyChecksum(y);if(m>>>0!==p&&g>>>0!==p)continue;t.push({lsn:r,type:a,txnId:o,tableName:c,key:u,data:d,checksum:p})}catch{break}return t}}const ne="__wal_",ie=/^__wal_(\d{6})\.bin$/,re=/^__wal_(\d+)$/,ae="__wal_count";class oe{constructor(e,t=4194304){this.backend=e,this.currentSegment=0,this.currentSize=0,this.segmentSize=t}segmentKey(e){return`${ne}${String(e).padStart(6,"0")}.bin`}async append(e){if(0===e.byteLength)return;this.currentSize+e.byteLength>this.segmentSize&&(this.currentSegment++,this.currentSize=0);const t=this.segmentKey(this.currentSegment),s=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);if("function"==typeof this.backend.append)await this.backend.append(t,s);else{const e=await this.backend.read(t);if(e){const n=new ArrayBuffer(e.byteLength+s.byteLength);new Uint8Array(n).set(new Uint8Array(e),0),new Uint8Array(n).set(new Uint8Array(s),e.byteLength),await this.backend.write(t,n)}else await this.backend.write(t,s)}this.currentSize+=e.byteLength}async readAll(){const e=await this.backend.listKeys(),t=e.filter(e=>ie.test(e)).map(e=>({seq:Number(e.match(ie)[1]),key:e})).sort((e,t)=>e.seq-t.seq);let s=0;for(let e=0;ere.test(e)).map(e=>({seq:Number(e.match(re)[1]),key:e})).sort((e,t)=>e.seq-t.seq),r=[];for(const{key:e}of i){const t=await this.backend.read(e);t&&r.push(new Uint8Array(t))}for(const{key:e}of n){const t=await this.backend.read(e);t&&r.push(new Uint8Array(t))}if(n.length>0){this.currentSegment=n[n.length-1].seq;const e=await this.backend.read(n[n.length-1].key);this.currentSize=e?e.byteLength:0,i.length>0&&(this.currentSegment++,this.currentSize=0)}else i.length>0&&(this.currentSegment=0,this.currentSize=0);const a=r.reduce((e,t)=>e+t.byteLength,0),o=new Uint8Array(a);let h=0;for(const e of r)o.set(e,h),h+=e.byteLength;return o}async truncate(){const e=(await this.backend.listKeys()).filter(e=>e.startsWith(ne)||e===ae);e.length>0&&await this.backend.deleteMany(e),this.currentSegment=0,this.currentSize=0}async exists(){return(await this.backend.listKeys()).some(e=>e.startsWith(ne)||e===ae)}}class he{constructor(){this.acquired=!1,this.supported=!1,this.releaseResolve=null,this.releasePromise=null}async acquire(e){const t=globalThis.navigator,s=t?.locks;return s&&"function"==typeof s.request?(this.supported=!0,await new Promise((t,i)=>{s.request.bind(s)(function(e){return`metona-sqlark:${e}`}(e),{ifAvailable:!0,mode:"exclusive"},async s=>{if(!s)return void i(new n(`Database "${e}" is already open in another tab (locked)`,"ARIA_LOCKED"));this.acquired=!0;const r=new Promise(e=>{this.releaseResolve=e});this.releasePromise=r,t(),await r})}),!0):(this.supported=!1,!1)}async release(){if(this.acquired){if(this.releaseResolve){const e=this.releaseResolve,t=this.releasePromise;this.releaseResolve=null,this.releasePromise=null,e();try{await t}catch{}}this.acquired=!1}}isAcquired(){return this.acquired}isSupported(){return this.supported}}class ce{constructor(e,t,s=null,n=1e3,i=16777216){this.opCount=0,this.lsm=e,this.wal=t,this.flushable=s,this.interval=n,this.walSizeThreshold=i}async tick(){this.opCount++,(this.opCount>=this.interval||this.getWALEstimatedSize()>=this.walSizeThreshold)&&await this.checkpoint()}getWALEstimatedSize(){const e=this.wal;if("function"==typeof e.getBufferedBytes){const t=e.getBufferedBytes();if(t>0)return t}return 200*("function"==typeof e.getBufferedCount?e.getBufferedCount():0)}async checkpoint(){await this.lsm.flush(),this.flushable&&await this.flushable.flushAll(),await this.wal.checkpoint(),this.opCount=0}}class le{constructor(){this.store=new Map,this.opened=!1}async open(e){this.opened=!0}async close(){this.store.clear(),this.opened=!1}isOpen(){return this.opened}async read(e){return this.store.get(e)??null}async write(e,t){this.store.set(e,t)}async writeMany(e){for(const[t,s]of Object.entries(e))this.store.set(t,s)}async delete(e){this.store.delete(e)}async deleteMany(e){for(const t of e)this.store.delete(t)}async listKeys(){return Array.from(this.store.keys())}async exists(e){return this.store.has(e)}async clear(){this.store.clear()}}class ue{constructor(e,t){this.kv=new L(e,t)}getKV(){return this.kv}async open(e){await this.kv.open(e)}async close(){await this.kv.close()}isOpen(){return this.kv.isOpen()}async read(e){return this.kv.get(e)}async write(e,t){await this.kv.put(e,t)}async append(e,t){await this.kv.appendValue(e,t)}async writeMany(e){await this.kv.putMany(e)}async delete(e){await this.kv.delete(e)}async deleteMany(e){await this.kv.deleteMany(e)}async listKeys(){return this.kv.listKeys()}async exists(e){return this.kv.exists(e)}async clear(){await this.kv.clear()}}const fe="AES-GCM";class de{constructor(){this.cryptoKey=null,this._enabled=!1}get enabled(){return this._enabled}async init(e,t){const s=new TextEncoder,n=await crypto.subtle.importKey("raw",s.encode(e),"PBKDF2",!1,["deriveKey"]),i=t||crypto.getRandomValues(new Uint8Array(16));return this.cryptoKey=await crypto.subtle.deriveKey({name:"PBKDF2",salt:i,iterations:1e5,hash:"SHA-256"},n,{name:fe,length:256},!1,["encrypt","decrypt"]),this._enabled=!0,i}async encryptPage(e){if(!this.cryptoKey)throw new Error("Crypto not initialized");const t=crypto.getRandomValues(new Uint8Array(12)),s=e instanceof Uint8Array?e:new Uint8Array(e);return{iv:t,data:await crypto.subtle.encrypt({name:fe,iv:t},this.cryptoKey,s)}}async decryptPage(e,t){if(!this.cryptoKey)throw new Error("Crypto not initialized");const s=t instanceof Uint8Array?t:new Uint8Array(t);return crypto.subtle.decrypt({name:fe,iv:e},this.cryptoKey,s)}close(){this.cryptoKey=null,this._enabled=!1}}const pe=12,ye="__aria_keymeta";function me(e){if("undefined"!=typeof Buffer)return Buffer.from(e).toString("base64");let t="";for(let s=0;se!==ye))throw new n("Cannot open with encryption: existing database has no key metadata (database was created without encryption, or key metadata was lost)","ARIA_ENCRYPT_CONFIG_ERROR");const e=await this.crypto.init(this.password),t=await this.crypto.encryptPage((new TextEncoder).encode("metona-sqlark-encryption-verifier").buffer),s=new Uint8Array(pe+t.data.byteLength);s.set(t.iv,0),s.set(new Uint8Array(t.data),pe);const i=JSON.stringify({salt:me(e),verifier:me(s)});await this.inner.write(ye,(new TextEncoder).encode(i).buffer)}this.opened=!0}async close(){await this.inner.close(),this.crypto.close(),this.opened=!1}isOpen(){return this.opened}async read(e){this.ensureReady();const t=await this.inner.read(e);return null===t?null:this.decrypt(t)}async write(e,t){this.ensureReady(),await this.inner.write(e,await this.encrypt(t))}async writeMany(e){this.ensureReady();const t={};for(const[s,n]of Object.entries(e))t[s]=await this.encrypt(n);await this.inner.writeMany(t)}async delete(e){this.ensureReady(),await this.inner.delete(e)}async deleteMany(e){this.ensureReady(),await this.inner.deleteMany(e)}async listKeys(){return this.ensureReady(),this.inner.listKeys()}async exists(e){return this.ensureReady(),this.inner.exists(e)}async clear(){const e=(await this.inner.listKeys()).filter(e=>e!==ye);e.length>0&&await this.inner.deleteMany(e)}ensureReady(){if(!this.opened)throw new n("EncryptedBackend not opened","ARIA_DB_NOT_OPEN");if(!this.crypto.enabled)throw new n("EncryptedBackend key not initialized","ARIA_DECRYPT_ERROR")}async encrypt(e){const t=await this.crypto.encryptPage(e),s=new ArrayBuffer(pe+t.data.byteLength),n=new Uint8Array(s);return n.set(t.iv,0),n.set(new Uint8Array(t.data),pe),s}async decrypt(e){const t=new Uint8Array(e);if(t.byteLength<=pe)throw new n("Corrupted encrypted data block (too short)","ARIA_DECRYPT_ERROR");const s=t.subarray(0,pe),i=t.subarray(pe);try{return await this.crypto.decryptPage(s,i)}catch(e){if(e instanceof n)throw e;throw new n("Decryption failed (data corrupted or wrong key)","ARIA_DECRYPT_ERROR",e)}}}class be{constructor(e,t){this.fileManager=e,this.bufferPool=t,this.pageIds=new Map}async save(e,t){const s=Math.max(1,Math.ceil(t.byteLength/M)),n=await this.bufferPool.newPages(s,P.DATA),i=[];for(let e=0;ee+t.byteLength,0),r=new Uint8Array(Math.min(i,s));let a=0;for(const e of n){const t=Math.min(e.byteLength,r.byteLength-a);if(t<=0)break;r.set(e.subarray(0,t),a),a+=t}return this.pageIds.delete(e),r}async delete(e,t){for(const e of t){this.bufferPool.removePage(e);try{await this.fileManager.freePageId(e)}catch{}}this.pageIds.delete(e)}}class Te{constructor(e){this.nextPageId=0,this.metaLoaded=!1,this.dbName="",this.backend=e}async init(e){this.dbName=e;const t=await this.backend.read("__aria_meta");let s=1;t&&t instanceof ArrayBuffer&&t.byteLength>=4&&(s=new DataView(t).getUint32(0,!1));const n=await this.backend.listKeys();for(const e of n)if(e.startsWith("pg_")){const t=Number.parseInt(e.slice(3),10);!Number.isNaN(t)&&t+1>s&&(s=t+1)}this.nextPageId=s,t&&t instanceof ArrayBuffer&&!(t.byteLength<4)&&s===new DataView(t).getUint32(0,!1)||await this.saveMeta(),this.metaLoaded=!0}async readPage(e){const t=`pg_${e}`,s=await this.backend.read(t);if(!s)return null;if(s.byteLengtht.txnId!==e);0===n.length?this.versionStore.delete(t):this.versionStore.set(t,n)}this.activeTxns.delete(e),this.txnWriteKeys.delete(e)}rollbackTransaction(e){const t=this.activeTxns.get(e);if(!t)throw new Error(`Transaction ${e} not found`);t.state=q.ABORTED;const s=this.txnWriteKeys.get(e);if(s)for(const t of s){const s=this.versionStore.get(t);if(!s)continue;const n=s.filter(t=>t.txnId!==e);0===n.length?this.versionStore.delete(t):this.versionStore.set(t,n)}this.activeTxns.delete(e),this.txnWriteKeys.delete(e)}writeVersion(e,t,s,n){const i=`${e}.${t}`,r=this.versionStore.get(i)??[],a={txnId:n,data:s,prevVersion:r.length>0?r[r.length-1]:null,committed:!1};r.push(a),this.versionStore.set(i,r),this.txnWriteKeys.get(n)?.add(i)}deleteVersion(e,t,s){this.writeVersion(e,t,{__mvcc_tombstone:!0},s)}discardVersions(e){const t=this.txnWriteKeys.get(e);if(t)for(const s of t){const t=this.versionStore.get(s);if(!t)continue;const n=t.filter(t=>t.txnId!==e);0===n.length?this.versionStore.delete(s):this.versionStore.set(s,n)}}gc(e=100){for(const[t,s]of this.versionStore){if(s.length<=e)continue;const n=s.slice(s.length-e);this.versionStore.set(t,n)}}getGlobalLSN(){return this.globalCommitLsn}}function ke(e,t){const s=new ArrayBuffer(M);return function(e,t,s){const n=new DataView(e);n.setUint32(0,t,!1),n.setUint8(4,s),n.setUint16(5,16,!1),n.setUint16(7,e.byteLength,!1),n.setUint16(9,0,!1),n.setUint32(11,0,!1),n.setUint8(15,0)}(s,e,t),{pageId:e,type:t,data:s,dirty:!0,pins:0,prev:null,next:null,lastAccess:Date.now()}}class xe{constructor(){this.head=null,this.tail=null,this._size=0}get size(){return this._size}moveToHead(e){this.head!==e&&(null!==e.prev||null!==e.next||this.head===e||this.tail===e?this.detach(e):this._size++,e.prev=null,e.next=this.head,this.head&&(this.head.prev=e),this.head=e,this.tail||(this.tail=e))}remove(e){(null!==e.prev||null!==e.next||this.head===e||this.tail===e)&&(this.detach(e),this._size=Math.max(0,this._size-1))}detach(e){e.prev?e.prev.next=e.next:this.head===e&&(this.head=e.next),e.next?e.next.prev=e.prev:this.tail===e&&(this.tail=e.prev),e.prev=null,e.next=null}clear(){this.head=null,this.tail=null,this._size=0}getLRU(){return this.tail}}class Se{constructor(e,t,s){this.lru=new xe,this.capacity=e,this.onEvict=t,this.onRemove=s}access(e){e.lastAccess=Date.now(),this.lru.moveToHead(e)}add(e){this.access(e)}remove(e){this.lru.remove(e)}async evictIfNeeded(e){let t=0;for(;this.lru.size+e>this.capacity&&this.lru.size>0;){const e=this.findEvictionCandidate();if(!e)break;e.dirty&&(await this.onEvict(e),e.dirty=!1),this.lru.remove(e),this.onRemove?.(e),t++}return t}findEvictionCandidate(){let e=this.lru.getLRU();for(;e;){if(0===e.pins&&!e.dirty)return e;e=e.prev}for(e=this.lru.getLRU();e;){if(0===e.pins)return e;e=e.prev}return null}clear(){this.lru.clear()}}class Ie{constructor(e,t=256){this.pages=new Map,this.nextPageId=0,this.pageIO=e,this.eviction=new Se(t,async e=>{e.dirty&&(await this.pageIO.writePage(e.pageId,e.data),e.dirty=!1)},e=>{this.pages.delete(e.pageId)})}async getPage(e){let t=this.pages.get(e);if(t)return this.eviction.access(t),t.pins++,t;const s=await this.pageIO.readPage(e);return s?(await this.eviction.evictIfNeeded(1),t={pageId:e,type:new DataView(s).getUint8(4),data:s,dirty:!1,pins:1,prev:null,next:null,lastAccess:Date.now()},this.pages.set(e,t),this.eviction.add(t),t):null}async newPages(e,t=P.DATA){if(e<=0)return[];let s;if("function"==typeof this.pageIO.allocatePageIds)s=await this.pageIO.allocatePageIds(e);else{s=[];for(let t=0;t0&&e.pins--}markDirty(e){e.dirty=!0}async flushPage(e){const t=this.pages.get(e);t&&t.dirty&&(await this.pageIO.writePage(e,t.data),t.dirty=!1)}async flushAll(){for(const[,e]of this.pages)e.dirty&&(await this.pageIO.writePage(e.pageId,e.data),e.dirty=!1)}removePage(e){const t=this.pages.get(e);t&&(this.eviction.remove(t),this.pages.delete(e))}async clear(){await this.flushAll(),this.pages.clear(),this.eviction.clear()}}class Ae{constructor(e={}){this.name="aria",this.opened=!1,this.dbName="",this.dbLock=null,this.schemas=new Map,this.tablePKs=new Map,this.opCounter=0,this.secondaryIndexes=new Map,this.uniqueIndexCols=new Set,this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.gcCounter=0,this.savepoints=new Map,this.config={...z,...e}}async open(e,t){if(!this.opened)try{await this.openInternal(e)}catch(t){if(this.dbLock){try{await this.dbLock.release()}catch{}this.dbLock=null}if(t instanceof n)throw t;throw new n(`Failed to open AriaEngine database "${e}"`,"ARIA_OPEN_ERROR",t)}}async openInternal(e){this.dbName=e;const t=new he;let s;if(this.dbLock=t,await t.acquire(e),s="opfs"===this.config.storageBackend?new w:"kv"===this.config.storageBackend?new ue:new le,await s.open(e),this.config.encryption?.password)this.backend=new we(s,this.config.encryption.password);else if(this.backend=s,await s.exists("__aria_keymeta"))throw await s.close(),new n("Database is encrypted: provide encryption.password to open it","ARIA_ENCRYPT_REQUIRED");await this.backend.open(e),this.fileManager=new Te(this.backend),await this.fileManager.init(e),this.bufferPool=new Ie(this.fileManager,this.config.bufferPoolPages);const i=this.createSSTableStore("main");this.lsm=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:i}),this.wal=new se(new oe(this.backend),this.config.walEnabled,this.config.walSyncMode),await this.loadSchemas();for(const[e,t]of this.schemas){const s=this.tablePKs.get(e);for(const[n,i]of Object.entries(t.columns))if((i.index||i.unique)&&n!==s){const t=`${e}:idx:${n}`;if(!this.secondaryIndexes.has(t)){const s=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e}_${n}`)});await s.init(),this.secondaryIndexes.set(t,s)}}}await this.lsm.init();const r=new Set,a=[];await this.wal.recover(e=>a.push(e));for(const e of a)e.type===K.COMMIT&&r.add(e.txnId),e.type===K.ROLLBACK&&r.delete(e.txnId);for(const e of a)(0===e.txnId||r.has(e.txnId))&&(e.type===K.DROP_TABLE?await this.applyDropTableRecovery(e.tableName):this.applyWALRecord(e));if(a.length>0){await this.lsm.flush(),await this.wal.checkpoint();for(const e of this.schemas.keys())await this.reindexTableInternal(e)}this.checkpointManager=new ce(this.lsm,{checkpoint:async()=>{this.currentTxnId||await this.wal.checkpoint()},flush:async()=>{this.currentTxnId||await this.wal.flush()},getBufferedBytes:()=>this.wal.getBufferedBytes(),getBufferedCount:()=>this.wal.getBufferedCount()},{flushAll:async()=>{await this.lsm.flush();for(const e of this.secondaryIndexes.values())await e.flush()}},this.config.checkpointInterval,this.config.walSizeThreshold),this.opened=!0}async close(){if(this.opened){await this.persistSchemas(),await this.lsm.flush();for(const e of this.secondaryIndexes.values())await e.flush();await this.bufferPool.flushAll(),await this.wal.flush(),await this.wal.checkpoint(),await this.backend.close(),this.dbLock&&(await this.dbLock.release(),this.dbLock=null),this.schemas.clear(),this.tablePKs.clear(),this.secondaryIndexes.clear(),this.uniqueIndexCols.clear(),this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.savepoints.clear(),this.opCounter=0,this.opened=!1}}async repair(){this.ensureOpen(),await this.bufferPool.clear(),await this.lsm.validateAll(),await this.lsm.flush(),await this.wal.checkpoint();for(const e of this.schemas.keys())await this.reindexTable(e);const e=this.backend;if("function"==typeof e.cleanupStaleFiles)try{await e.cleanupStaleFiles()}catch{}await this.cleanupOrphanPages()}async cleanupOrphanPages(){const e=(await this.backend.listKeys()).filter(e=>/^pg_\d+$/.test(e));if(0===e.length)return;const t=new Set,s=async e=>{const s="main"===e?"__aria_lsm_meta":`__aria_lsm_meta_${e}`,n=await this.backend.read(s);if(n)try{const e=JSON.parse((new TextDecoder).decode(n));for(const s of e)if(s.pageIds)for(const e of s.pageIds)t.add(e)}catch{}};await s("main");for(const[e,t]of this.schemas)for(const[n,i]of Object.entries(t.columns))(i.index||i.unique)&&await s(`idx_${e}_${n}`);const n=e.map(e=>Number(e.slice(3))).filter(e=>!t.has(e));n.length>0&&await this.backend.deleteMany(n.map(e=>`pg_${e}`))}async clearAll(){this.ensureOpen(),await this.backend.clear(),await this.bufferPool.clear(),await this.fileManager.clearAll(),this.schemas.clear(),this.tablePKs.clear(),this.secondaryIndexes.clear(),this.uniqueIndexCols.clear(),this.lsm.clear(),this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.savepoints.clear(),this.opCounter=0,await this.persistSchemas(),await this.wal.checkpoint()}isOpen(){return this.opened}async getMeta(e){const t=await this.backend.read(`__meta_${e}`);return t?(new TextDecoder).decode(t):null}async setMeta(e,t){await this.backend.write(`__meta_${e}`,(new TextEncoder).encode(t).buffer)}async createTable(e){if(this.ensureOpen(),this.ensureNoDDLInTransaction("CREATE TABLE"),this.schemas.has(e.name))throw new n(`Table "${e.name}" already exists`,"TABLE_EXISTS");this.schemas.set(e.name,e),this.tablePKs.set(e.name,this.getPK(e));for(const[t,s]of Object.entries(e.columns))if(s.index||s.unique){const s=`${e.name}:idx:${t}`;if(!this.secondaryIndexes.has(s)){const n=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e.name}_${t}`)});await n.init(),this.secondaryIndexes.set(s,n)}}await this.persistSchemas(),await this.wal.append({type:K.CREATE_TABLE,txnId:0,tableName:e.name,key:"",data:{schema:JSON.stringify(e)}})}async dropTable(e){this.ensureOpen(),this.ensureNoDDLInTransaction("DROP TABLE"),this.ensureTable(e);const t=await this.getAllRows(e);for(const s of t){const t=this.tablePKs.get(e);this.lsm.delete(`${e}:${s[t]}`)}await this.cleanupTableIndexes(e);const s=`${e}:`;for(const e of this.uniqueIndexCols)e.startsWith(s)&&this.uniqueIndexCols.delete(e);this.schemas.delete(e),this.tablePKs.delete(e),await this.persistSchemas(),await this.wal.append({type:K.DROP_TABLE,txnId:0,tableName:e,key:""})}async cleanupTableIndexes(e){const t=`${e}:idx:`,s=[];for(const[e,n]of this.secondaryIndexes)if(e.startsWith(t)){s.push(e);try{await n.clear()}catch{}}for(const e of s)this.secondaryIndexes.delete(e)}async hasTable(e){return this.ensureOpen(),this.schemas.has(e)}async getTableNames(){return this.ensureOpen(),Array.from(this.schemas.keys())}async getTableSchema(e){return this.ensureOpen(),this.schemas.get(e)??null}async insert(e,t){this.ensureOpen(),this.ensureTable(e);const s=this.schemas.get(e),i=this.tablePKs.get(e),r=[],a=[],o=this.uniqueColumns(e,s),h=[];for(const n of t){const t=this.validateRow(s,n),r=String(t[i]);h.push({row:t,pkValue:r,key:`${e}:${r}`})}await this.lsm.prefetchKeys(h.map(e=>e.key));const c=new Set;for(const{pkValue:t,key:s}of h){if(c.has(t))throw new n(`Duplicate primary key "${t}" in table "${e}"`,"DUPLICATE_KEY");c.add(t);const i=this.currentTxnId?this.txnSnapshot?.get(s)??this.lsm.get(s):this.lsm.get(s);if(i&&!i.__txn_deleted)throw new n(`Duplicate primary key "${t}" in table "${e}"`,"DUPLICATE_KEY")}for(const t of o){const s=this.secondaryIndexes.get(`${e}:idx:${t}`);await s.prefetchPrefixRanges(h.map(e=>{const s=e.row[t];if(null==s)return null;const n=`${String(s)}:`;return[n,`${n}￿`]}).filter(e=>null!==e))}const l=new Map;for(const{row:t,pkValue:s}of h)for(const i of o){const r=t[i];if(null==r)continue;const a=String(r);let o=l.get(i);if(o||(o=new Set,l.set(i,o)),o.has(a))throw new n(`Unique constraint violation on column "${i}" in table "${e}"`,"UNIQUE_VIOLATION");o.add(a),this.checkUniqueSync(e,[i],t,s)}for(const{row:t,pkValue:s,key:n}of h)this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(n,t),this.mvcc.writeVersion(e,s,t,this.currentTxnId)):this.lsm.put(n,t),this.updateSecondaryIndexes(e,s,t,null),r.push(s),a.push({type:K.INSERT,txnId:this.currentTxnId??0,tableName:e,key:s,data:t});return await this.wal.appendBatch(a),this.opCounter+=t.length,this.checkMemoryBudget(),await this.checkpointManager.tick(),this.tryGC(),r}async find(e,t){let s;this.ensureOpen(),this.ensureTable(e);const n=await this.tryIndexLookup(e,t);s=null!==n?n:await this.getAllRows(e),s=this.mergeTxnSnapshot(e,s),t.where&&Object.keys(t.where).length>0&&(s=s.filter(e=>o(e,t.where))),t.orderBy&&t.orderBy.length>0&&(s=l(s,t.orderBy));const i=t.offset??0,r=t.limit??s.length;return s=s.slice(i,i+r),t.columns&&t.columns.length>0&&"*"!==t.columns[0]&&(s=s.map(e=>f(e,t.columns))),this.trimAllCaches(),s}async update(e,t,s){if(this.ensureOpen(),this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const i=this.schemas.get(e),r=await this.getAllRows(e);let h=0;const c=[],l=new Set,u=p(s);for(const t of Object.keys(u))if(!i.columns[t])throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const f=this.uniqueColumns(e,i);for(const t of f){const s=this.secondaryIndexes.get(`${e}:idx:${t}`),n=[];if(void 0!==u[t]&&null!==u[t]){const e=`${String(u[t])}:`;n.push([e,`${e}￿`])}else if(!(t in u))for(const e of r){const s=e[t];if(null==s)continue;const i=`${String(s)}:`;n.push([i,`${i}￿`])}await s.prefetchPrefixRanges(n)}const d=[],y=new Map;for(const s of r){const r=this.tablePKs.get(e),a=`${e}:${s[r]}`;if(t.where&&Object.keys(t.where).length>0&&!o(s,t.where))continue;const h={...s,...u};this.validateRow(i,h),this.checkBatchUnique(e,f,h,y),this.checkUniqueSync(e,f,h,String(s[r]));const c=String(h[r]),l=c!==String(s[r]);if(l){const t=`${e}:${c}`,s=this.currentTxnId?this.txnSnapshot?.get(t)??this.lsm.get(t):this.lsm.get(t);if(s&&!s.__txn_deleted)throw new n(`Duplicate primary key "${c}" in table "${e}" (cannot update key to existing value)`,"DUPLICATE_KEY")}d.push({row:s,pk:String(s[r]),key:a,updated:h,newPk:c,pkChanged:l})}for(const t of d)t.pkChanged&&await this.checkForeignKeyUpdateRestrict(e,t.pk,t.newPk);for(const{row:t,pk:s,key:n,updated:i,newPk:r,pkChanged:a}of d)a&&await this.applyForeignKeyUpdateRules(e,s,r,c,l),this.currentTxnId&&this.txnSnapshot?(a&&(this.txnSnapshot.set(n,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,s,this.currentTxnId)),this.txnSnapshot.set(`${e}:${r}`,i),this.mvcc.writeVersion(e,r,i,this.currentTxnId)):(a&&this.lsm.delete(n),this.lsm.put(`${e}:${r}`,i)),h++,a&&c.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:s}),c.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:e,key:r,data:i}),this.updateSecondaryIndexes(e,r,i,t);return await this.wal.appendBatch(c),this.opCounter+=h,await this.checkpointManager.tick(),this.trimAllCaches(),h}checkBatchUnique(e,t,s,i){for(const r of t){const t=s[r];if(null==t)continue;let a=i.get(r);if(a||(a=new Set,i.set(r,a)),a.has(t))throw new n(`Unique constraint violation on column "${r}" in table "${e}"`,"UNIQUE_VIOLATION");a.add(t)}}async checkForeignKeyUpdateRestrict(e,t,s){for(const[s,i]of this.schemas)if(s!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i===e&&("RESTRICT"===a.onUpdate||"SET NULL"===a.onUpdate&&a.required)){const i=await this.getAllRows(s);for(const o of i)if(String(o[r])===t){const i="RESTRICT"===a.onUpdate?`foreign key "${r}" in "${s}" has dependent rows`:`foreign key "${r}" in "${s}" is required (SET NULL violates constraint)`;throw new n(`Cannot update "${e}" key "${t}": ${i}`,"FOREIGN_KEY_VIOLATION")}}}}async applyForeignKeyUpdateRules(e,t,s,n,i){const r=`${e}:${t}`;if(!i.has(r)){i.add(r);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onUpdate)continue;const[r]=o.references.split(".");if(r!==e)continue;if("CASCADE"!==o.onUpdate&&"SET NULL"!==o.onUpdate)continue;const h=await this.getAllRows(i);for(const e of h){if(String(e[a])!==t)continue;const r=this.tablePKs.get(i),h=String(e[r]),c={...e,[a]:"CASCADE"===o.onUpdate?s:null},l=`${i}:${h}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(l,c),this.mvcc.writeVersion(i,h,c,this.currentTxnId)):this.lsm.put(l,c),this.updateSecondaryIndexes(i,h,c,e),n.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:i,key:h,data:c})}}}}async delete(e,t){if(this.ensureOpen(),this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const s=await this.getAllRows(e);let i=0;const r=[],h=new Set,c=[];for(const n of s)t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||c.push(String(n[this.tablePKs.get(e)]));const l=new Set;for(const t of c)await this.checkCascadeRestrict(e,t,l);for(const n of s){const s=this.tablePKs.get(e),a=`${e}:${n[s]}`;t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||(i+=await this.applyForeignKeyRules(e,String(n[s]),r,h),this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(a,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,String(n[s]),this.currentTxnId)):this.lsm.delete(a),i++,r.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:String(n[s])}),this.updateSecondaryIndexes(e,String(n[s]),null,n))}return await this.wal.appendBatch(r),this.opCounter+=i,await this.checkpointManager.tick(),this.trimAllCaches(),i}async checkCascadeRestrict(e,t,s){const i=`${e}:${t}`;if(!s.has(i)){s.add(i);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onDelete)continue;const[r]=o.references.split(".");if(r!==e)continue;const h=(await this.getAllRows(i)).filter(e=>String(e[a])===t);if("RESTRICT"===o.onDelete&&h.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("SET NULL"===o.onDelete&&o.required&&h.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===o.onDelete){const e=this.tablePKs.get(i);for(const t of h)await this.checkCascadeRestrict(i,String(t[e]),s)}}}}async applyForeignKeyRules(e,t,s,i){let r=0;const a=`${e}:${t}`;if(i.has(a))return 0;i.add(a);for(const[a,o]of this.schemas)if(a!==e)for(const[h,c]of Object.entries(o.columns)){if(!c.references||!c.onDelete)continue;const[o]=c.references.split(".");if(o!==e)continue;const l=(await this.getAllRows(a)).filter(e=>String(e[h])===t);if("RESTRICT"===c.onDelete&&l.length>0)throw new n(`Cannot delete from "${e}": foreign key "${h}" in "${a}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===c.onDelete){const e=this.tablePKs.get(a);for(const t of l){const n=String(t[e]);r+=await this.applyForeignKeyRules(a,n,s,i);const o=`${a}:${n}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(o,{__txn_deleted:!0}),this.mvcc.deleteVersion(a,n,this.currentTxnId)):this.lsm.delete(o),this.updateSecondaryIndexes(a,n,null,t),s.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:a,key:n}),r++}}else if("SET NULL"===c.onDelete){const e=this.tablePKs.get(a);for(const t of l){const n=String(t[e]),i={...t,[h]:null},r=`${a}:${n}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(r,i),this.mvcc.writeVersion(a,n,i,this.currentTxnId)):this.lsm.put(r,i),this.updateSecondaryIndexes(a,n,i,t),s.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:a,key:n,data:i})}}}return r}async findStream(e,t,s){this.ensureOpen(),this.ensureTable(e);const n=!!(t.where&&Object.keys(t.where).length>0),i=t.columns&&t.columns.length>0&&"*"!==t.columns[0]?e=>f(e,t.columns):null,r=t.limit??1/0,a=t.offset??0,h=this.tablePKs.get(e),c=`${e}:`;let l=0,u=0;const d=e=>!(!n||o(e,t.where))||(u{if(l>=r)return!1;const s={...t};return s[h]=e.slice(c.length),d(s)}),l}async count(e,t){this.ensureOpen(),this.ensureTable(e);const s=await this.getAllRows(e);return this.trimAllCaches(),t?.where&&0!==Object.keys(t.where).length?s.filter(e=>o(e,t.where)).length:s.length}async clear(e){this.ensureOpen(),this.ensureTable(e);const t=await this.getAllRows(e),s=[];for(const n of t){const t=this.tablePKs.get(e),i=`${e}:${n[t]}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(i,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,String(n[t]),this.currentTxnId)):this.lsm.delete(i),s.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:String(n[t])}),this.updateSecondaryIndexes(e,String(n[t]),null,n)}await this.wal.appendBatch(s),this.opCounter+=t.length,await this.checkpointManager.tick(),this.tryGC()}async alterTable(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("ALTER TABLE"),this.ensureTable(e);const i=this.schemas.get(e);if("ADD"===t){if(i.columns[s.name])throw new n(`Column "${s.name}" already exists in table "${e}"`,"COLUMN_EXISTS");return i.columns[s.name]=s,void await this.persistSchemas()}if(!i.columns[s.name])throw new n(`Column "${s.name}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.columns[s.name].index||i.columns[s.name].unique){const t=`${e}:idx:${s.name}`,n=this.secondaryIndexes.get(t);if(n){try{await n.clear()}catch{}this.secondaryIndexes.delete(t)}}delete i.columns[s.name],await this.persistSchemas();const r=`${e}:`,a=`${r}￿`;await this.lsm.prefetchRange(r,a);const o=this.lsm.rangeScan(r,a),h=[];for(const[t,n]of o){if(!(s.name in n))continue;const i={...n};delete i[s.name],this.lsm.put(t,i);const a=t.slice(r.length);this.updateSecondaryIndexes(e,a,i,n),h.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:e,key:a,data:i})}await this.wal.appendBatch(h),this.opCounter+=h.length,await this.checkpointManager.tick(),this.trimAllCaches()}async createIndex(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("CREATE INDEX"),this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const r=`${e}:idx:${t}`;if(this.secondaryIndexes.has(r))return;const a=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e}_${t}`)});await a.init(),this.secondaryIndexes.set(r,a);try{const i=this.tablePKs.get(e),r=await this.getAllRows(e),o=new Set;for(const h of r){const r=h[t];if(null!=r){const c=String(r);if(s&&o.has(c))throw new n(`Unique index on column "${t}" in table "${e}" cannot be created: duplicate value "${c}"`,"UNIQUE_VIOLATION");o.add(c),a.put(`${c}:${h[i]}`,{pk:h[i]})}}await a.flush()}catch(e){this.secondaryIndexes.delete(r);try{await a.clear()}catch{}throw e}i.index=!0,s&&(i.unique=!0,this.uniqueIndexCols.add(`${e}:${t}`)),await this.persistSchemas()}async dropIndex(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("DROP INDEX"),this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.primaryKey)throw new n(`Cannot drop primary key index on column "${t}"`,"NOT_SUPPORTED");if(!i.index&&!i.unique&&!this.secondaryIndexes.has(`${e}:idx:${t}`))throw new n(`Index on column "${t}" does not exist in table "${e}"`,"INDEX_NOT_FOUND");const r=`${e}:${t}`;if(i.unique&&!this.uniqueIndexCols.has(r))throw new n(`Cannot drop index on column "${t}" in table "${e}": UNIQUE constraint defined at table creation must be removed by recreating the table`,"NOT_SUPPORTED");i.index=!1,i.unique=!1,this.uniqueIndexCols.delete(r);const a=`${e}:idx:${t}`,o=this.secondaryIndexes.get(a);o&&(await o.clear(),this.secondaryIndexes.delete(a)),await this.persistSchemas()}async beginTransaction(){if(this.currentTxnId)throw new n("Transaction already in progress","TX_ACTIVE");this.currentTxnId=this.mvcc.beginTransaction(),this.txnSnapshot=new Map;try{await this.wal.append({type:K.BEGIN,txnId:this.currentTxnId,tableName:"",key:""})}catch(e){throw this.mvcc.rollbackTransaction(this.currentTxnId),this.currentTxnId=null,this.txnSnapshot=null,e}}async commitTransaction(){if(!this.currentTxnId)throw new n("No active transaction","TX_NONE");if(await this.wal.append({type:K.COMMIT,txnId:this.currentTxnId,tableName:"",key:""}),await this.wal.flush(),this.txnSnapshot)for(const[e,t]of this.txnSnapshot)t.__txn_deleted?this.lsm.delete(e):this.lsm.put(e,t);this.mvcc.commitTransaction(this.currentTxnId),this.currentTxnId=null,this.txnSnapshot=null}async rollbackTransaction(){if(!this.currentTxnId)throw new n("No active transaction","TX_NONE");const e=this.currentTxnId;await this.wal.append({type:K.ROLLBACK,txnId:e,tableName:"",key:""});const t=new Set;if(this.txnSnapshot)for(const e of this.txnSnapshot.keys()){const s=e.indexOf(":");s>0&&t.add(e.slice(0,s))}this.mvcc.rollbackTransaction(e),this.txnSnapshot=null,this.currentTxnId=null;for(const e of t)this.schemas.has(e)&&await this.reindexTable(e)}async savepoint(e){if(!this.currentTxnId)throw new n("No active transaction for savepoint","TX_NONE");if(this.savepoints.has(e))throw new n(`Savepoint "${e}" already exists`,"SAVEPOINT_EXISTS");this.savepoints.set(e,{txnId:this.currentTxnId,snapshot:this.txnSnapshot?new Map(this.txnSnapshot):null})}async rollbackToSavepoint(e){const t=this.savepoints.get(e);if(!t)throw new n(`Savepoint "${e}" not found`,"SAVEPOINT_NOT_FOUND");const s=new Set;if(this.txnSnapshot)for(const e of this.txnSnapshot.keys()){const t=e.indexOf(":");t>0&&s.add(e.slice(0,t))}this.txnSnapshot=t.snapshot?new Map(t.snapshot):null,this.mvcc.discardVersions(this.currentTxnId);let i=!1;for(const[t]of this.savepoints)t!==e?i&&this.savepoints.delete(t):i=!0;for(const e of s)this.schemas.has(e)&&await this.reindexTable(e)}async releaseSavepoint(e){if(!this.savepoints.has(e))throw new n(`Savepoint "${e}" not found`,"SAVEPOINT_NOT_FOUND");this.savepoints.delete(e)}async backup(){this.ensureOpen();const e={};for(const t of this.schemas.keys())e[t]=await this.getAllRows(t);return e}async getAllRows(e){const t=this.tablePKs.get(e),s=`${e}:`;await this.lsm.prefetchRange(s,`${s}￿`);const n=this.lsm.rangeScan(s,`${s}￿`).map(([e,n])=>{const i={...n};return i[t]=e.slice(s.length),i});return this.mergeTxnSnapshot(e,n)}mergeTxnSnapshot(e,t){if(!this.currentTxnId||!this.txnSnapshot)return t;const s=this.tablePKs.get(e),n=`${e}:`;for(const[e,i]of this.txnSnapshot){if(!e.startsWith(n))continue;const r=e.slice(n.length),a=i.__txn_deleted,o=t.findIndex(e=>e[s]===r);if(a)o>=0&&t.splice(o,1);else{const e={...i,[s]:r};o>=0?t[o]=e:t.push(e)}}return t}getPK(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}validateRow(e,t){const s={};for(const[i,r]of Object.entries(e.columns)){let a=t[i];if(void 0===a&&void 0!==r.default&&(a=r.default),r.required&&null==a)throw new n(`Column "${i}" is required in table "${e.name}"`,"VALIDATION_ERROR");if(r.primaryKey&&null==a)throw new n(`Primary key column "${i}" in table "${e.name}" cannot be null or undefined`,"VALIDATION_ERROR");null!=a&&this.checkType(i,r.type,a,r),void 0!==a&&(s[i]=a)}return s}checkType(e,t,s,i){!function(e,t,s,i,r){const a=typeof i;switch(s){case"string":if("string"!==a)throw new n(`Column "${t}" in table "" expects string, got ${a}`,"TYPE_ERROR");if(void 0!==r?.maxLength&&i.length>r.maxLength)throw new n(`Column "${t}" in table "" exceeds max length ${r.maxLength}`,"VALIDATION_ERROR");break;case"number":if("number"!==a)throw new n(`Column "${t}" in table "" expects number, got ${a}`,"TYPE_ERROR");if(void 0!==r?.min&&ir.max)throw new n(`Column "${t}" in table "" value ${i} above maximum ${r.max}`,"VALIDATION_ERROR");break;case"boolean":if("boolean"!==a)throw new n(`Column "${t}" in table "" expects boolean, got ${a}`,"TYPE_ERROR");break;case"date":if("string"!==a||isNaN(Date.parse(i)))throw new n(`Column "${t}" in table "" expects valid date string, got ${typeof i}`,"TYPE_ERROR");break;case"json":if("object"!==a)throw new n(`Column "${t}" in table "" expects object/array, got ${a}`,"TYPE_ERROR")}}(0,e,t,s,i)}async persistSchemas(){const e={};for(const[t,s]of this.schemas)e[t]=s.columns;const t=JSON.stringify(e),s=(new TextEncoder).encode(t).buffer;await this.backend.write("__aria_schemas",s)}async loadSchemas(){const e=await this.backend.read("__aria_schemas");if(e)try{const t=(new TextDecoder).decode(e),s=JSON.parse(t);for(const[e,t]of Object.entries(s)){const s={name:e,columns:t};this.schemas.set(e,s),this.tablePKs.set(e,this.getPK(s))}}catch{}}createSSTableStore(e){const t="main"===e?"sst_":`sst_${e}_`,s="main"===e?"__aria_lsm_meta":`__aria_lsm_meta_${e}`;let n=0,i=!1;const r=this.isPageStorage()?new be(this.fileManager,this.bufferPool):null,a=e=>(new TextEncoder).encode(e).buffer,o=async()=>{const e=await this.backend.read(s);if(!e)return[];try{return JSON.parse((new TextDecoder).decode(e))}catch{return[]}};return{save:async(e,s)=>{if(r)return void await r.save(e,s);let n=s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength);if(this.config.compression){const e=function(e){if(0===e.byteLength){const e=new Uint8Array(4);return new DataView(e.buffer).setUint32(0,0,!0),e}const t=e.byteLength+Math.ceil(e.byteLength/15)+8,s=new Uint8Array(t);let n=0,i=0,r=0;for(;n=4&&i>t&&(t=i,a=n-s)}if(t>4&&n-r<=15){const o=n-r,h=t-4;s[i++]=(15&o)<<4|15&h;for(let t=0;t>8&255,n+=t,r=n}else if(n++,n-r>=15){s[i++]=240;for(let t=0;t<15;t++)s[i++]=e[r+t];r=n}}let a=n-r;for(;a>0;){const t=Math.min(a,15);s[i++]=(15&t)<<4;for(let n=0;n{if(r){const t=(await o()).find(t=>t.id===e);if(t&&t.pageIds&&t.pageIds.length>0)return r.load(e,t.pageIds,t.totalSize)}const s=await this.backend.read(`${t}${e}`);if(!s)return null;let n=new Uint8Array(s);return this.config.compression&&(n=function(e){if(e.byteLength<4)throw new Error("LZ4 stream too short: missing header");const t=new DataView(e.buffer,e.byteOffset,e.byteLength).getUint32(0,!0);if(0===t&&4===e.byteLength)return new Uint8Array(0);if(t<=0||t>1073741823)throw new Error("Invalid LZ4 header: bad original size");const s=e.subarray(4),n=new Uint8Array(t);let i=0,r=0;for(;i>4&15,o=15&e;for(let e=0;e=s.byteLength)break;const h=s[i++]|s[i++]<<8,c=o+4;for(let e=0;e{if(r){const t=(await o()).find(t=>t.id===e);t&&t.pageIds&&t.pageIds.length>0&&await r.delete(e,t.pageIds)}await this.backend.delete(`${t}${e}`)},allocateId:async()=>{if(!i){const e=await o();n=e.reduce((e,t)=>Math.max(e,t.id),0),i=!0}return++n},listMeta:o,saveMeta:async e=>{const t=await o(),n=r?.getPageIds(e.id),i=n&&n.length>0?{...e,pageIds:n}:e,h=t.findIndex(t=>t.id===e.id);h>=0?t[h]=i:t.push(i),await this.backend.write(s,a(JSON.stringify(t)))},deleteMeta:async e=>{const t=(await o()).filter(t=>t.id!==e);await this.backend.write(s,a(JSON.stringify(t)))}}}isPageStorage(){return!0===this.config.pageStorage||!1!==this.config.pageStorage&&("opfs"===this.config.storageBackend||"kv"===this.config.storageBackend)}applyWALRecord(e){switch(e.type){case K.INSERT:case K.UPDATE:e.data&&this.lsm.put(`${e.tableName}:${e.key}`,e.data);break;case K.DELETE:this.lsm.delete(`${e.tableName}:${e.key}`);break;case K.CREATE_TABLE:if(e.data?.schema)try{const t=JSON.parse(e.data.schema);this.schemas.has(t.name)||(this.schemas.set(t.name,t),this.tablePKs.set(t.name,this.getPK(t)))}catch{}case K.COMMIT:case K.ROLLBACK:case K.BEGIN:}}async applyDropTableRecovery(e){if(!e)return;await this.cleanupTableIndexes(e),this.schemas.delete(e),this.tablePKs.delete(e);const t=`${e}:`,s=`${t}￿`;await this.lsm.prefetchRange(t,s);const n=this.lsm.rangeScan(t,s);for(const[e]of n)this.lsm.delete(e)}uniqueColumns(e,t){const s=[];for(const[n,i]of Object.entries(t.columns))i.unique&&this.secondaryIndexes.has(`${e}:idx:${n}`)&&s.push(n);return s}checkUniqueSync(e,t,s,i){for(const r of t){const t=s[r];if(null==t)continue;const a=this.secondaryIndexes.get(`${e}:idx:${r}`);if(!a)continue;const o=`${String(t)}:`,h=a.rangeScan(o,`${o}￿`);for(const[,t]of h){const s=t.pk;if(void 0!==s&&s!==i)throw new n(`Unique constraint violation on column "${r}" in table "${e}"`,"UNIQUE_VIOLATION")}}}updateSecondaryIndexes(e,t,s,n){const i=this.schemas.get(e);if(i)for(const[r,a]of Object.entries(i.columns)){if(!a.index&&!a.unique)continue;const i=`${e}:idx:${r}`,o=this.secondaryIndexes.get(i);if(o){if(n){const e=n[r];null!=e&&o.delete(`${String(e)}:${t}`)}if(s){const e=s[r];null!=e&&o.put(`${String(e)}:${t}`,{pk:t})}}}}async tryIndexLookup(e,t){if(!t.where)return null;const s=this.schemas.get(e);if(!s)return null;const n=this.tablePKs.get(e),i=[],r=e=>{for(const[t,s]of Object.entries(e))if("$and"!==t)"$or"!==t&&"$not"!==t&&"$exists"!==t&&i.push([t,s]);else for(const e of s)r(e)};r(t.where);for(const[t,r]of i){const i=s.columns[t];if(!(i&&(i.index||i.unique||i.primaryKey)||t===n))continue;if(t===n){if("object"!=typeof r||null===r){const t=`${e}:${r}`;await this.lsm.prefetchKeys([t]);const s=this.lsm.get(t);return s?[{...s,[n]:r}]:[]}const t=r;if("$eq"in t){const s=`${e}:${t.$eq}`;await this.lsm.prefetchKeys([s]);const i=this.lsm.get(s);return i?[{...i,[n]:t.$eq}]:[]}if("$in"in t&&Array.isArray(t.$in)){const s=t.$in.map(t=>`${e}:${t}`);await this.lsm.prefetchKeys(s);const i=[],r=new Set;for(const s of t.$in){const t=String(s);if(r.has(t))continue;const a=this.lsm.get(`${e}:${t}`);a&&(r.add(t),i.push({...a,[n]:t}))}return i}if("$gt"in t||"$gte"in t||"$lt"in t||"$lte"in t){const t=`${e}:`;await this.lsm.prefetchRange(t,`${t}￿`);const s=this.lsm.rangeScan(t,`${t}￿`),i=[];for(const[e,a]of s){const s={...a,[n]:e.slice(t.length)};o(s,{[n]:r})&&i.push(s)}return i}}const a=`${e}:idx:${t}`,h=this.secondaryIndexes.get(a);if(!h)continue;if("object"!=typeof r||null===r){if(null===r)continue;return this.indexScanToRows(e,n,h,String(r),String(r))}const c=r;if("$eq"in c){if(null===c.$eq)continue;const t=String(c.$eq);return this.indexScanToRows(e,n,h,t,t)}if("$in"in c&&Array.isArray(c.$in)){if(c.$in.some(e=>null===e))continue;const t=c.$in.map(e=>String(e));await h.prefetchPrefixRanges(t.map(e=>[e,`${e}￿`]));const s=[],i=new Set,r=[];for(const e of t){const t=h.rangeScan(e,`${e}￿`);for(const[,e]of t){const t=e.pk;t&&!i.has(t)&&(i.add(t),r.push(t))}}await this.lsm.prefetchKeys(r.map(t=>`${e}:${t}`));for(const t of r){const i=this.lsm.get(`${e}:${t}`);i&&s.push({...i,[n]:t})}return s}if("$gt"in c||"$gte"in c||"$lt"in c||"$lte"in c)return(await this.indexScanToRows(e,n,h,"","￿")).filter(e=>o(e,{[t]:r}))}return null}async indexScanToRows(e,t,s,n,i){const r=i.includes("￿")?i:`${i}￿`;await s.prefetchRange(n,r);const a=s.rangeScan(n,r),o=[];for(const[,e]of a){const t=e.pk;t&&o.push(t)}await this.lsm.prefetchKeys(o.map(t=>`${e}:${t}`));const h=[];for(const s of o){const n=this.lsm.get(`${e}:${s}`);n&&h.push({...n,[t]:s})}return h}tryGC(){this.gcCounter++,this.gcCounter>=10&&(this.mvcc.gc(100),this.gcCounter=0)}trimAllCaches(){this.lsm.trimCache();for(const e of this.secondaryIndexes.values())e.trimCache()}checkMemoryBudget(){const e=1024*this.config.maxMemoryMB*1024;this.lsm.getEstimatedMemory()>e&&(this.lsm.flush().catch(()=>{}),this.mvcc.gc(50))}async analyzeTable(e){this.ensureOpen(),this.ensureTable(e);const t=await this.getAllRows(e);let s=this.lsm.getStats().sstableCount,n=this.lsm.getStats().memtableSize,i=this.lsm.getStats().levelCounts.filter(e=>e>0).length;for(const[t,r]of this.secondaryIndexes){if(!t.startsWith(`${e}:idx:`))continue;const a=r.getStats();s+=a.sstableCount,n+=a.memtableSize,i=Math.max(i,a.levelCounts.filter(e=>e>0).length)}const r={table:e,rowCount:t.length,avgRowSize:t.length>0?Math.round(t.reduce((e,t)=>e+JSON.stringify(t).length,0)/t.length):0,indexDepth:i,sstableCount:s,memtableSize:n,estimatedMemory:this.lsm.getEstimatedMemory()},a=this.schemas.get(e);if(a&&t.length>0){const e={};for(const s of Object.keys(a.columns)){const n=new Set(t.map(e=>String(e[s])));e[s]={distinctValues:n.size}}r.columnStats=e}return r}async reindexTable(e){return this.ensureOpen(),this.ensureTable(e),this.reindexTableInternal(e)}async reindexTableInternal(e){const t=this.schemas.get(e);if(!t)return 0;let s=0;const n=this.tablePKs.get(e),i=Object.entries(t.columns).filter(([,e])=>e.index||e.unique);if(0===i.length)return 0;const r=await this.getAllRows(e);for(const[t]of i){const i=`${e}:idx:${t}`,a=this.secondaryIndexes.get(i);if(a){await a.clear(),s++;for(const e of r){const s=e[t];null!=s&&a.put(`${String(s)}:${e[n]}`,{pk:e[n]})}}}return s}async vacuum(){this.ensureOpen(),await this.lsm.flush();for(let e=0;e<6;e++)this.lsm.getStats().levelCounts[e]>=2&&await this.lsm.compactLevel(e);const e=this.mvcc.getGlobalLSN();return this.mvcc.gc(10),{compactedLevels:6,gcVersions:e}}ensureOpen(){if(!this.opened)throw new n("AriaEngine not opened","DB_NOT_OPEN")}ensureNoDDLInTransaction(e){if(this.currentTxnId)throw new n(`${e} is not supported inside a transaction (AriaEngine DDL is not transactional)`,"NOT_SUPPORTED")}ensureTable(e){if(!this.schemas.has(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND")}}class Re{constructor(e="opfs"){this.name="hybrid",this.dbName="",this.version=1,this.memoryEngine=new m,this.diskEngineType=e,this.diskEngine=new _}async open(e,t){this.dbName=e,this.version=t,await this.diskEngine.open(e,t),await this.memoryEngine.open(e,t),await this.reloadMemoryFromDisk()}async reloadMemoryFromDisk(){await this.memoryEngine.close(),await this.memoryEngine.open(this.dbName,this.version);const e=this.diskEngine;"function"==typeof e.reload&&await e.reload();const t=await this.diskEngine.getTableNames();for(const e of t){const t=await this.diskEngine.getTableSchema(e);if(!t)continue;await this.memoryEngine.createTable(t);const s=await this.diskEngine.find(e,{table:e});if(s.length>0)try{await this.memoryEngine.insert(e,s)}catch(e){}}}async close(){await this.memoryEngine.close(),await this.diskEngine.close()}isOpen(){return this.memoryEngine.isOpen()&&this.diskEngine.isOpen()}async repair(){"function"==typeof this.diskEngine.repair&&await this.diskEngine.repair(),await this.reloadMemoryFromDisk()}async clearAll(){if("function"==typeof this.diskEngine.clearAll)await this.diskEngine.clearAll();else{const e=await this.diskEngine.getTableNames();for(const t of e)await this.diskEngine.dropTable(t)}if("function"==typeof this.memoryEngine.clearAll)await this.memoryEngine.clearAll();else{const e=await this.memoryEngine.getTableNames();for(const t of e)await this.memoryEngine.dropTable(t)}}async getMeta(e){return"function"==typeof this.diskEngine.getMeta?this.diskEngine.getMeta(e):null}async setMeta(e,t){"function"==typeof this.diskEngine.setMeta&&await this.diskEngine.setMeta(e,t)}async createTable(e){await this.memoryEngine.createTable(e);try{await this.diskEngine.createTable(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async dropTable(e){await this.memoryEngine.dropTable(e);try{await this.diskEngine.dropTable(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async hasTable(e){return this.memoryEngine.hasTable(e)}async getTableNames(){return this.memoryEngine.getTableNames()}async getTableSchema(e){return this.memoryEngine.getTableSchema(e)}async alterTable(e,t,s){await this.memoryEngine.alterTable(e,t,s);try{if("function"==typeof this.diskEngine.alterTable)await this.diskEngine.alterTable(e,t,s);else{const n=await this.diskEngine.getTableSchema(e);n&&"DROP"===t&&delete n.columns[s.name]}}catch(e){await this.recoverMemoryAfterDiskError(e)}}async recoverMemoryAfterDiskError(e){try{await this.reloadMemoryFromDisk()}catch{}throw e}async insert(e,t){const s=await this.memoryEngine.insert(e,t);try{await this.diskEngine.insert(e,t)}catch(e){await this.recoverMemoryAfterDiskError(e)}return s}async find(e,t){return this.memoryEngine.find(e,t)}async findStream(e,t,s){return this.memoryEngine.findStream(e,t,s)}async update(e,t,s){const n=await this.memoryEngine.update(e,t,s);try{await this.diskEngine.update(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}return n}async delete(e,t){const s=await this.memoryEngine.delete(e,t);try{await this.diskEngine.delete(e,t)}catch(e){await this.recoverMemoryAfterDiskError(e)}return s}async count(e,t){return this.memoryEngine.count(e,t)}async clear(e){await this.memoryEngine.clear(e);try{await this.diskEngine.clear(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async createIndex(e,t,s){await this.memoryEngine.createIndex(e,t,s);try{"function"==typeof this.diskEngine.createIndex&&await this.diskEngine.createIndex(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async dropIndex(e,t,s){await this.memoryEngine.dropIndex(e,t,s);try{"function"==typeof this.diskEngine.dropIndex&&await this.diskEngine.dropIndex(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async beginTransaction(){await this.memoryEngine.beginTransaction();try{await this.diskEngine.beginTransaction()}catch(e){throw await this.memoryEngine.rollbackTransaction(),e}}async commitTransaction(){await this.diskEngine.commitTransaction();try{await this.memoryEngine.commitTransaction()}catch(e){throw new n("Hybrid commit failed: memory engine error after disk commit (disk data is committed)","TX_COMMIT_ERROR",e)}}async rollbackTransaction(){await this.memoryEngine.rollbackTransaction(),await this.diskEngine.rollbackTransaction()}getDiskEngineType(){return this.diskEngineType}getMemoryEngine(){return this.memoryEngine}}class Ce{constructor(e,t,s=["*"],n){this.engine=e,this.tableName=t,this._columns=s,this._where={},this._orderBy=[],this._joins=[],this._executor=n}as(e){return this._alias=e,this}innerJoin(e,t,s){return this._addJoin("INNER",e,t,s)}leftJoin(e,t,s){return this._addJoin("LEFT",e,t,s)}rightJoin(e,t,s){return this._addJoin("RIGHT",e,t,s)}crossJoin(e,t){return this._addJoin("CROSS",e,{},t)}join(e,t,s){return this._addJoin("INNER",e,t,s)}_addJoin(e,t,s,n){return this._joins.push({type:e,table:t,on:s,alias:n}),this}where(e){return this._where={...this._where,...e},this}orderBy(e,t="asc"){return this._orderBy.push({column:e,direction:t}),this}limit(e){return this._limit=e,this}offset(e){return this._offset=e,this}async execute(){if(this._joins.length>0&&this._executor){const e=this.toAST();return this._executor.execute(e)}return this.engine.find(this.tableName,{table:this.tableName,columns:this._columns,where:this._where,orderBy:this._orderBy.length>0?this._orderBy:void 0,limit:this._limit,offset:this._offset})}toAST(){return{type:"SELECT",columns:this._columns,from:this.tableName,alias:this._alias,joins:this._joins.length>0?[...this._joins]:void 0,where:this._where,orderBy:this._orderBy.length>0?this._orderBy:void 0,limit:this._limit,offset:this._offset}}}class Oe{constructor(e,t,s,n,i){this.engine=e,this.tableName=t,this._updates=s,this._where={},this.onWrite=n,this.onHooks=i}where(e){return this._where={...this._where,...e},this}async execute(){const e={table:this.tableName,where:this._where};await(this.onHooks?.("beforeUpdate",[e,this._updates]));const t=await this.engine.update(this.tableName,e,this._updates);return this.onWrite?.(this.tableName),await(this.onHooks?.("afterUpdate",[e,this._updates,t])),t}toAST(){return{type:"UPDATE",table:this.tableName,sets:this._updates,where:this._where}}}class Ne{constructor(e,t,s,n){this.engine=e,this.tableName=t,this._where={},this.onWrite=s,this.onHooks=n}where(e){return this._where={...this._where,...e},this}async execute(){const e={table:this.tableName,where:this._where};await(this.onHooks?.("beforeDelete",[e]));const t=await this.engine.delete(this.tableName,e);return this.onWrite?.(this.tableName),await(this.onHooks?.("afterDelete",[e,t])),t}toAST(){return{type:"DELETE",from:this.tableName,where:this._where}}}class Le{constructor(e,t,s,n,i){this.schema=null,this.engine=e,this.name=t,this.executor=s,this.onWrite=n,this.onHooks=i}async getSchema(){if(!this.schema){const e=await this.engine.getTableSchema(this.name);if(!e)throw new n(`Table "${this.name}" does not exist`,"TABLE_NOT_FOUND");this.schema=e}return this.schema}async insert(e){await(this.onHooks?.("beforeInsert",[[e]]));const t=await this.engine.insert(this.name,[e]);return this.onWrite?.(this.name),await(this.onHooks?.("afterInsert",[[e],t])),t[0]}async insertMany(e){await(this.onHooks?.("beforeInsert",[e]));const t=await this.engine.insert(this.name,e);return this.onWrite?.(this.name),await(this.onHooks?.("afterInsert",[e,t])),t}select(e=["*"]){return new Ce(this.engine,this.name,e,this.executor)}async stream(e,t={}){if("function"!=typeof this.engine.findStream){const s=await this.engine.find(this.name,{table:this.name,where:t.where,limit:t.limit,offset:t.offset,columns:t.columns});for(const t of s)e(t);return s.length}return this.engine.findStream(this.name,{table:this.name,where:t.where,limit:t.limit,offset:t.offset,columns:t.columns},e)}update(e){return new Oe(this.engine,this.name,e,this.onWrite,this.onHooks)}delete(){return new Ne(this.engine,this.name,this.onWrite,this.onHooks)}async count(e){return this.engine.count(this.name,e?{table:this.name,where:e}:void 0)}async clear(){await this.engine.clear(this.name),this.onWrite?.(this.name)}async drop(){await this.engine.dropTable(this.name),this.onWrite?.(this.name)}}function ve(e){switch(e.type){case"SELECT":return function(e){return{table:e.from,columns:e.columns,where:e.where,orderBy:e.orderBy?.length?e.orderBy:void 0,limit:e.limit,offset:e.offset}}(e);case"DELETE":return function(e){return{table:e.from,where:e.where}}(e);case"UPDATE":return function(e){return{table:e.table,where:e.where}}(e);default:throw new n(`Cannot compile statement type "${e.type}" to QueryPlan`,"COMPILE_ERROR")}}var $e;!function(e){e.SELECT="SELECT",e.FROM="FROM",e.WHERE="WHERE",e.INSERT="INSERT",e.INTO="INTO",e.VALUES="VALUES",e.UPDATE="UPDATE",e.SET="SET",e.DELETE="DELETE",e.CREATE="CREATE",e.TABLE="TABLE",e.DROP="DROP",e.ORDER="ORDER",e.BY="BY",e.ASC="ASC",e.DESC="DESC",e.LIMIT="LIMIT",e.OFFSET="OFFSET",e.AND="AND",e.OR="OR",e.NOT="NOT",e.LIKE="LIKE",e.IN="IN",e.PRIMARY="PRIMARY",e.KEY="KEY",e.UNIQUE="UNIQUE",e.DEFAULT="DEFAULT",e.NULL="NULL",e.TRUE="TRUE",e.REFERENCES="REFERENCES",e.CASCADE="CASCADE",e.BETWEEN="BETWEEN",e.IF="IF",e.EXISTS="EXISTS",e.FALSE="FALSE",e.ALTER="ALTER",e.ADD="ADD",e.TRUNCATE="TRUNCATE",e.INNER="INNER",e.LEFT="LEFT",e.RIGHT="RIGHT",e.CROSS="CROSS",e.JOIN="JOIN",e.ON="ON",e.AS="AS",e.OUTER="OUTER",e.GROUP="GROUP",e.HAVING="HAVING",e.COUNT="COUNT",e.SUM="SUM",e.AVG="AVG",e.MIN="MIN",e.MAX="MAX",e.DISTINCT="DISTINCT",e.BEGIN="BEGIN",e.COMMIT="COMMIT",e.ROLLBACK="ROLLBACK",e.UNION="UNION",e.ALL="ALL",e.INDEX="INDEX",e.CASE="CASE",e.WHEN="WHEN",e.THEN="THEN",e.ELSE="ELSE",e.END="END",e.EXPLAIN="EXPLAIN",e.ANALYZE="ANALYZE",e.REINDEX="REINDEX",e.VACUUM="VACUUM",e.SAVEPOINT="SAVEPOINT",e.RELEASE="RELEASE",e.TO="TO",e.IDENTIFIER="IDENTIFIER",e.STRING="STRING",e.NUMBER="NUMBER",e.COMMA="COMMA",e.LPAREN="LPAREN",e.RPAREN="RPAREN",e.SEMICOLON="SEMICOLON",e.EQ="EQ",e.NEQ="NEQ",e.GT="GT",e.GTE="GTE",e.LT="LT",e.LTE="LTE",e.STAR="STAR",e.DOT="DOT",e.EOF="EOF",e.ILLEGAL="ILLEGAL"}($e||($e={}));const De={SELECT:$e.SELECT,FROM:$e.FROM,WHERE:$e.WHERE,INSERT:$e.INSERT,INTO:$e.INTO,VALUES:$e.VALUES,UPDATE:$e.UPDATE,SET:$e.SET,DELETE:$e.DELETE,CREATE:$e.CREATE,TABLE:$e.TABLE,DROP:$e.DROP,ORDER:$e.ORDER,BY:$e.BY,ASC:$e.ASC,DESC:$e.DESC,LIMIT:$e.LIMIT,OFFSET:$e.OFFSET,AND:$e.AND,OR:$e.OR,NOT:$e.NOT,LIKE:$e.LIKE,IN:$e.IN,PRIMARY:$e.PRIMARY,KEY:$e.KEY,UNIQUE:$e.UNIQUE,DEFAULT:$e.DEFAULT,NULL:$e.NULL,TRUE:$e.TRUE,FALSE:$e.FALSE,REFERENCES:$e.REFERENCES,CASCADE:$e.CASCADE,BETWEEN:$e.BETWEEN,IF:$e.IF,EXISTS:$e.EXISTS,ALTER:$e.ALTER,ADD:$e.ADD,TRUNCATE:$e.TRUNCATE,INNER:$e.INNER,LEFT:$e.LEFT,RIGHT:$e.RIGHT,CROSS:$e.CROSS,JOIN:$e.JOIN,ON:$e.ON,AS:$e.AS,OUTER:$e.OUTER,GROUP:$e.GROUP,HAVING:$e.HAVING,COUNT:$e.COUNT,SUM:$e.SUM,AVG:$e.AVG,MIN:$e.MIN,MAX:$e.MAX,DISTINCT:$e.DISTINCT,BEGIN:$e.BEGIN,COMMIT:$e.COMMIT,ROLLBACK:$e.ROLLBACK,UNION:$e.UNION,ALL:$e.ALL,INDEX:$e.INDEX,CASE:$e.CASE,WHEN:$e.WHEN,THEN:$e.THEN,ELSE:$e.ELSE,END:$e.END,EXPLAIN:$e.EXPLAIN,ANALYZE:$e.ANALYZE,REINDEX:$e.REINDEX,VACUUM:$e.VACUUM,SAVEPOINT:$e.SAVEPOINT,RELEASE:$e.RELEASE,TO:$e.TO};class Ue{constructor(e){this.position=0,this.readPosition=0,this.ch="",this.input=e,this.readChar()}nextToken(){let e;switch(this.skipWhitespace(),this.ch){case",":e=this.makeToken($e.COMMA,",");break;case"(":e=this.makeToken($e.LPAREN,"(");break;case")":e=this.makeToken($e.RPAREN,")");break;case";":e=this.makeToken($e.SEMICOLON,";");break;case"*":e=this.makeToken($e.STAR,"*");break;case".":e=this.makeToken($e.DOT,".");break;case"=":e=this.makeToken($e.EQ,"=");break;case"!":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.NEQ,"!=")):e=this.makeToken($e.ILLEGAL,"!");break;case">":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.GTE,">=")):e=this.makeToken($e.GT,">");break;case"<":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.LTE,"<=")):">"===this.peekChar()?(this.readChar(),e=this.makeToken($e.NEQ,"<>")):e=this.makeToken($e.LT,"<");break;case"'":case'"':e=this.readString(this.ch);break;case"":e={type:$e.EOF,value:"",position:this.position};break;default:if("-"===this.ch&&"-"===this.peekChar())return this.skipLineComment(),this.nextToken();if("/"===this.ch&&"*"===this.peekChar())return this.skipBlockComment(),this.nextToken();if(this.isLetter(this.ch)){const t=this.readIdentifier();return e={type:De[t.toUpperCase()]??$e.IDENTIFIER,value:t,position:this.position-t.length},e}if(this.isDigit(this.ch)||"-"===this.ch&&this.isDigit(this.peekChar())){const t=this.readNumber();return e={type:$e.NUMBER,value:t,position:this.position-t.length},e}e=this.makeToken($e.ILLEGAL,this.ch)}return this.readChar(),e}readChar(){this.readPosition>=this.input.length?this.ch="":this.ch=this.input[this.readPosition],this.position=this.readPosition,this.readPosition++}peekChar(){return this.readPosition>=this.input.length?"":this.input[this.readPosition]}skipWhitespace(){for(;" "===this.ch||"\t"===this.ch||"\n"===this.ch||"\r"===this.ch;)this.readChar()}skipLineComment(){for(;"\n"!==this.ch&&"\r"!==this.ch&&""!==this.ch;)this.readChar()}skipBlockComment(){for(this.readChar(),this.readChar();""!==this.ch&&("*"!==this.ch||"/"!==this.peekChar());)this.readChar();""!==this.ch&&(this.readChar(),this.readChar())}readIdentifier(){const e=this.position;for(;this.isLetter(this.ch)||this.isDigit(this.ch)||"_"===this.ch;)this.readChar();return this.input.slice(e,this.position)}readNumber(){const e=this.position;for("-"===this.ch&&this.readChar();this.isDigit(this.ch);)this.readChar();if("."===this.ch&&this.isDigit(this.peekChar()))for(this.readChar();this.isDigit(this.ch);)this.readChar();return this.input.slice(e,this.position)}readString(e){const t=this.position+1;this.readChar();let s="";for(;""!==this.ch;){if(this.ch===e){if(this.peekChar()===e){s+=e,this.readChar(),this.readChar();continue}break}"\\"!==this.ch||this.peekChar()!==e?(s+=this.ch,this.readChar()):(this.readChar(),s+=e,this.readChar())}if(""===this.ch)throw new n(`Unterminated string literal at position ${t}`,"PARSE_ERROR");return{type:$e.STRING,value:s,position:t}}isLetter(e){return/[a-zA-Z_]/.test(e)}isDigit(e){return/[0-9]/.test(e)}makeToken(e,t){return{type:e,value:t,position:this.position}}}class _e{constructor(e){this.sql=e,this.lexer=new Ue(e),this.nextToken(),this.nextToken()}parseStatement(){switch(this.curToken.type){case $e.SELECT:return this.parseSelect();case $e.INSERT:return this.parseInsert();case $e.UPDATE:return this.parseUpdate();case $e.DELETE:return this.parseDelete();case $e.CREATE:return this.parseCreateStatement();case $e.DROP:return this.parseDropStatement();case $e.ALTER:return this.parseAlterTable();case $e.TRUNCATE:return this.parseTruncateTable();case $e.BEGIN:return this.parseBegin();case $e.COMMIT:return this.parseCommit();case $e.ROLLBACK:return this.parseRollback();case $e.EXPLAIN:return this.parseExplain();case $e.ANALYZE:return this.parseAnalyze();case $e.REINDEX:return this.parseReindex();case $e.VACUUM:return this.parseVacuum();case $e.SAVEPOINT:case $e.RELEASE:return this.parseSavepoint();default:throw this.error(`Unexpected token "${this.curToken.value}"`)}}parseAllStatements(){const e=[];for(;!this.curTokenIs($e.EOF);){for(;this.curTokenIs($e.SEMICOLON);)this.nextToken();if(this.curTokenIs($e.EOF))break;if(e.push(this.parseStatement()),this.curTokenIs($e.SEMICOLON))this.nextToken();else if(!this.curTokenIs($e.EOF))throw this.error(`Expected ';' after statement, got "${this.curToken.value}"`)}return e}parseExplain(){if(this.expect($e.EXPLAIN),this.curTokenIs($e.EXPLAIN))throw this.error("Nested EXPLAIN is not allowed");return{type:"EXPLAIN",query:this.parseStatement()}}parseAnalyze(){return this.expect($e.ANALYZE),this._isKeywordAsIdent()&&"TABLE"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ANALYZE",table:this.expectIdentifier("table name")}}parseReindex(){return this.expect($e.REINDEX),this._isKeywordAsIdent()&&"TABLE"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"REINDEX",table:this.expectIdentifier("table name")}}parseVacuum(){return this.expect($e.VACUUM),{type:"VACUUM"}}parseSavepoint(){let e;return this.curTokenIs($e.RELEASE)?(e="RELEASE",this.nextToken()):(e="SAVE",this.expect($e.SAVEPOINT)),this.curTokenIs($e.SAVEPOINT)&&this.nextToken(),{type:"SAVEPOINT",name:this.expectIdentifier("savepoint name"),action:e}}parseBegin(){return this.expect($e.BEGIN),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"BEGIN"}}parseCommit(){return this.expect($e.COMMIT),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"COMMIT"}}parseRollback(){return this.expect($e.ROLLBACK),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()?(this.nextToken(),{type:"ROLLBACK"}):this.curTokenIs($e.TO)||this._isKeywordAsIdent()&&"TO"===this.curToken.value.toUpperCase()?(this.nextToken(),(this.curTokenIs($e.SAVEPOINT)||this._isKeywordAsIdent()&&"SAVEPOINT"===this.curToken.value.toUpperCase())&&this.nextToken(),{type:"SAVEPOINT",name:this.expectIdentifier("savepoint name"),action:"ROLLBACK"}):{type:"ROLLBACK"}}parseCreateStatement(){if(this.expect($e.CREATE),this.curTokenIs($e.TABLE))return this.parseCreateTable();if(this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())return this.parseCreateIndex();if(this.curTokenIs($e.UNIQUE)&&(this.nextToken(),this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())){const e=this.parseCreateIndex();return e.unique=!0,e}throw this.error(`Expected TABLE or INDEX after CREATE, got "${this.curToken.value}"`)}parseCreateIndex(){this.expect($e.INDEX);const e=this.expectIdentifier("index name");this.expect($e.ON);const t=this.expectIdentifier("table name");this.expect($e.LPAREN);const s=this.expectIdentifier("column name");return this.expect($e.RPAREN),{type:"CREATE_INDEX",name:e,table:t,column:s}}parseDropStatement(){if(this.expect($e.DROP),this.curTokenIs($e.TABLE))return this.parseDropTable();if(this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())return this.parseDropIndex();throw this.error(`Expected TABLE or INDEX after DROP, got "${this.curToken.value}"`)}parseDropIndex(){this.expect($e.INDEX);const e=this.expectIdentifier("index name");let t="",s="";return this.curTokenIs($e.ON)&&(this.nextToken(),t=this.expectIdentifier("table name"),this.curTokenIs($e.LPAREN)&&(this.nextToken(),s=this.expectIdentifier("column name"),this.expect($e.RPAREN))),{type:"DROP_INDEX",name:e,table:t,column:s}}parseSelect(){this.expect($e.SELECT);let e=!1;this.curTokenIs($e.DISTINCT)&&(e=!0,this.nextToken());const t=[];if(this.curTokenIs($e.STAR))for(t.push("*"),this.nextToken();this.curTokenIs($e.COMMA);)this.nextToken(),t.push(this.parseColumnWithAlias());else t.push(...this.parseColumnList());let s,n,i="";this.curTokenIs($e.FROM)&&(this.nextToken(),this.curTokenIs($e.LPAREN)?(this.nextToken(),s=this.parseSelect(),this.expect($e.RPAREN),this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isReservedAfterFrom()||(n=this.curToken.value,this.nextToken())):(i=this.expectIdentifier("table name"),this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isReservedAfterFrom()||(n=this.curToken.value,this.nextToken())));const r={type:"SELECT",columns:t,distinct:e||void 0,from:i,alias:n,where:{}};s&&(r.fromSubquery=s);const a=this.parseJoinClauses();return a.length>0&&(r.joins=a),this.curTokenIs($e.WHERE)&&(this.nextToken(),r.where=this.parseCondition()),this.curTokenIs($e.GROUP)&&(this.nextToken(),this.expect($e.BY),r.groupBy=this.parseIdentifierList()),this.curTokenIs($e.HAVING)&&(this.nextToken(),r.having=this.parseCondition()),this.curTokenIs($e.ORDER)&&(this.nextToken(),this.expect($e.BY),r.orderBy=this.parseOrderByList()),this.curTokenIs($e.LIMIT)&&(this.nextToken(),r.limit=this.expectNumber("LIMIT value")),this.curTokenIs($e.OFFSET)&&(this.nextToken(),r.offset=this.expectNumber("OFFSET value")),this.curTokenIs($e.UNION)?this.parseUnion(r):r}parseUnion(e){this.expect($e.UNION);let t=!1;this.curTokenIs($e.ALL)&&(t=!0,this.nextToken());const s={type:"SELECT_UNION",left:e,right:this.parseSelect(),all:t||void 0};return this.curTokenIs($e.UNION)?this.parseUnionChain(s):s}parseUnionChain(e){this.expect($e.UNION);let t=!1;this.curTokenIs($e.ALL)&&(t=!0,this.nextToken());const s={type:"SELECT_UNION",left:e,right:this.parseSelect(),all:t||void 0};return this.curTokenIs($e.UNION)?this.parseUnionChain(s):s}parseJoinClauses(){const e=[];for(;this._isJoinKeyword();)e.push(this.parseJoinClause());return e}_isJoinKeyword(){return this.curTokenIs($e.INNER)||this.curTokenIs($e.LEFT)||this.curTokenIs($e.RIGHT)||this.curTokenIs($e.CROSS)||this.curTokenIs($e.JOIN)}parseJoinClause(){let e="INNER";this.curTokenIs($e.INNER)?(e="INNER",this.nextToken()):this.curTokenIs($e.LEFT)?(e="LEFT",this.nextToken(),this.curTokenIs($e.OUTER)&&this.nextToken()):this.curTokenIs($e.RIGHT)?(e="RIGHT",this.nextToken(),this.curTokenIs($e.OUTER)&&this.nextToken()):this.curTokenIs($e.CROSS)&&(e="CROSS",this.nextToken()),this.expect($e.JOIN);const t=this.expectIdentifier("table name");let s;this.curTokenIs($e.AS)?(this.nextToken(),s=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isJoinReserved()||(s=this.curToken.value,this.nextToken());let n={};return"CROSS"!==e&&this.curTokenIs($e.ON)&&(this.nextToken(),n=this.parseCondition()),{type:e,table:t,alias:s,on:n}}_isReservedAfterFrom(){return this.curTokenIs($e.WHERE)||this.curTokenIs($e.ORDER)||this.curTokenIs($e.LIMIT)||this.curTokenIs($e.OFFSET)||this.curTokenIs($e.GROUP)||this._isJoinKeyword()}_isJoinReserved(){return this.curTokenIs($e.ON)||this.curTokenIs($e.WHERE)||this.curTokenIs($e.ORDER)||this.curTokenIs($e.LIMIT)||this._isJoinKeyword()}parseInsert(){this.expect($e.INSERT),this.expect($e.INTO);const e=this.expectIdentifier("table name");let t;if(this.curTokenIs($e.LPAREN)&&(this.nextToken(),t=this.parseIdentifierList(),this.expect($e.RPAREN)),this.curTokenIs($e.SELECT))return{type:"INSERT",into:e,columns:t,select:this.parseSelect()};this.expect($e.VALUES);const s=[];do{this.curTokenIs($e.COMMA)&&this.nextToken(),this.expect($e.LPAREN);const e=this.parseValueList();this.expect($e.RPAREN),s.push(e)}while(this.curTokenIs($e.COMMA));return{type:"INSERT",into:e,columns:t,values:s}}parseUpdate(){this.expect($e.UPDATE);const e=this.expectIdentifier("table name");this.expect($e.SET);const t={};do{this.curTokenIs($e.COMMA)&&this.nextToken();const e=this.expectIdentifier("column name");this.expect($e.EQ),t[e]=this.parseValue()}while(this.curTokenIs($e.COMMA));let s={};return this.curTokenIs($e.WHERE)&&(this.nextToken(),s=this.parseCondition()),{type:"UPDATE",table:e,sets:t,where:s}}parseDelete(){this.expect($e.DELETE),this.expect($e.FROM);const e=this.expectIdentifier("table name");let t={};return this.curTokenIs($e.WHERE)&&(this.nextToken(),t=this.parseCondition()),{type:"DELETE",from:e,where:t}}parseCreateTable(){this.expect($e.TABLE);let e=!1;this.curTokenIs($e.IF)&&(this.nextToken(),this.expect($e.NOT),this.expect($e.EXISTS),e=!0);const t=this.expectIdentifier("table name");this.expect($e.LPAREN);const s=[];do{this.curTokenIs($e.COMMA)&&this.nextToken(),s.push(this.parseColumnDef())}while(this.curTokenIs($e.COMMA));return this.expect($e.RPAREN),{type:"CREATE_TABLE",name:t,columns:s,ifNotExists:e||void 0}}parseColumnDef(){const e={name:this.expectIdentifier("column name"),type:this.expectIdentifier("column type").toLowerCase()};for(;this.curTokenIs($e.PRIMARY)||this.curTokenIs($e.UNIQUE)||this.curTokenIs($e.NOT)||this.curTokenIs($e.DEFAULT)||this.curTokenIs($e.REFERENCES);)if(this.curTokenIs($e.PRIMARY))this.nextToken(),this.expect($e.KEY),e.primaryKey=!0;else if(this.curTokenIs($e.UNIQUE))this.nextToken(),e.unique=!0;else if(this.curTokenIs($e.NOT))this.nextToken(),this.expect($e.NULL),e.required=!0;else if(this.curTokenIs($e.DEFAULT))this.nextToken(),e.default=this.parseValue();else{if(!this.curTokenIs($e.REFERENCES))break;{this.nextToken();const t=this.expectIdentifier("referenced table");this.expect($e.LPAREN);const s=this.expectIdentifier("referenced column");for(this.expect($e.RPAREN),e.references=`${t}.${s}`;this.curTokenIs($e.ON);)if(this.nextToken(),this.curTokenIs($e.DELETE))this.nextToken(),e.onDelete=this.parseCascadeAction();else{if(!this.curTokenIs($e.UPDATE))break;this.nextToken(),e.onUpdate=this.parseCascadeAction()}}}return e}parseCascadeAction(){return this.curTokenIs($e.CASCADE)?(this.nextToken(),"CASCADE"):this.curTokenIs($e.SET)?(this.nextToken(),this.expect($e.NULL),"SET NULL"):this.curToken.type===$e.IDENTIFIER&&"RESTRICT"===this.curToken.value.toUpperCase()?(this.nextToken(),"RESTRICT"):"RESTRICT"}parseAlterTable(){this.expect($e.ALTER),this.expect($e.TABLE);const e=this.expectIdentifier("table name");let t;if(this.curTokenIs($e.ADD))return t="ADD",this.nextToken(),this.curToken.type===$e.IDENTIFIER&&"COLUMN"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ALTER_TABLE",name:e,action:t,column:this.parseColumnDef()};if(this.curTokenIs($e.DROP)||this._isKeywordAsIdent()&&"DROP"===this.curToken.value.toUpperCase())return t="DROP",this.nextToken(),this.curToken.type===$e.IDENTIFIER&&"COLUMN"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ALTER_TABLE",name:e,action:t,column:{name:this.expectIdentifier("column name"),type:"string"}};throw this.error("Expected ADD or DROP in ALTER TABLE")}parseTruncateTable(){return this.expect($e.TRUNCATE),this.expect($e.TABLE),{type:"TRUNCATE_TABLE",name:this.expectIdentifier("table name")}}parseDropTable(){this.expect($e.TABLE);let e=!1;return this.curTokenIs($e.IF)&&(this.nextToken(),this.expect($e.EXISTS),e=!0),{type:"DROP_TABLE",name:this.expectIdentifier("table name"),ifExists:e||void 0}}parseCondition(){let e=this.parseSimpleCondition();for(;this.curTokenIs($e.AND)||this.curTokenIs($e.OR);){const t=this.curTokenIs($e.AND);this.nextToken();const s=this.parseSimpleCondition();e=t?{$and:[e,s]}:{$or:[e,s]}}return e}parseWhere(){return this.parseCondition()}parseSimpleCondition(){if(this.curTokenIs($e.EXISTS)||this._isKeywordAsIdent()&&"EXISTS"===this.curToken.value.toUpperCase())return this.nextToken(),this.parseExistsCondition(!1);if(this.curTokenIs($e.NOT)&&this._peekIsExists())return this.nextToken(),this.nextToken(),this.parseExistsCondition(!0);if(this.curTokenIs($e.NOT)&&!this._isNotInOrLike())return this.nextToken(),{$not:this.parseSimpleCondition()};if(this.curTokenIs($e.LPAREN)){this.nextToken();const e=this.parseCondition();return this.expect($e.RPAREN),e}const e=this.parseColumnRef();if(this.curTokenIs($e.IDENTIFIER)&&"IS"===this.curToken.value.toUpperCase()){this.nextToken();const t=this.curTokenIs($e.NOT);t&&this.nextToken(),this.expect($e.NULL);const s={};return s[e]=t?{$ne:null}:{$eq:null},s}if(this.curTokenIs($e.BETWEEN)){this.nextToken();const t=this.parseValue();this.expect($e.AND);const s=this.parseValue(),n={};return n[e]={$gte:t,$lte:s},n}if(this.curTokenIs($e.NOT)&&this.peekTokenIs($e.BETWEEN)){this.nextToken(),this.nextToken();const t=this.parseValue();this.expect($e.AND);const s=this.parseValue(),n={};return n[e]={$not:{$gte:t,$lte:s}},n}if(this.curTokenIs($e.NOT)){if(this.peekTokenIs($e.IN)){if(this.nextToken(),this.nextToken(),this.expect($e.LPAREN),this.curTokenIs($e.SELECT)){const t=this.parseSelect();this.expect($e.RPAREN);const s={};return s[e]={$nin:{$subquery:t}},s}const t=this.parseValueList();this.expect($e.RPAREN);const s={};return s[e]={$nin:t},s}if(this.peekTokenIs($e.LIKE)){this.nextToken(),this.nextToken();const t=this.parseValue(),s={};return s[e]={$not:{$like:t}},s}}if(this.curTokenIs($e.LIKE)){this.nextToken();const t=this.parseValue(),s={};return s[e]={$like:t},s}if(this.curTokenIs($e.IN)){if(this.nextToken(),this.expect($e.LPAREN),this.curTokenIs($e.SELECT)){const t=this.parseSelect();this.expect($e.RPAREN);const s={};return s[e]={$in:{$subquery:t}},s}const t=this.parseValueList();this.expect($e.RPAREN);const s={};return s[e]={$in:t},s}if(this.curTokenIs($e.AND)||this.curTokenIs($e.OR)||this.curTokenIs($e.RPAREN)||this.curTokenIs($e.EOF)||this.curToken.type===$e.IDENTIFIER&&["THEN","END","ELSE","NULLS","LIMIT","OFFSET","ORDER","GROUP","HAVING","UNION","WHERE"].includes(this.curToken.value.toUpperCase())){const t={};return t[e]={$eq:!0},t}const t=this.parseComparisonOp();if(this.curTokenIs($e.LPAREN)&&this.peekTokenIs($e.SELECT)){this.nextToken();const s=this.parseSelect();this.expect($e.RPAREN);const n={};return n[e]={[t]:{$subquery:s}},n}let s;s=(this.curToken.type===$e.IDENTIFIER||this._isKeywordAsIdent())&&this.peekTokenIs($e.DOT)?{$col:this.parseColumnRef()}:this.parseValue();const n={};return n[e]={[t]:s},n}parseExistsCondition(e){this.expect($e.LPAREN);const t=this.parseSelect();return this.expect($e.RPAREN),{$exists:{$subquery:t,$negate:e||void 0}}}_peekIsExists(){return this.peekToken.type===$e.EXISTS||this.peekToken.type===$e.IDENTIFIER&&"EXISTS"===this.peekToken.value.toUpperCase()}_isNotInOrLike(){return this.peekTokenIs($e.IN)||this.peekTokenIs($e.LIKE)}peekTokenIs(e){return this.peekToken.type===e}parseComparisonOp(){switch(this.curToken.type){case $e.EQ:return this.nextToken(),"$eq";case $e.NEQ:return this.nextToken(),"$ne";case $e.GT:return this.nextToken(),"$gt";case $e.GTE:return this.nextToken(),"$gte";case $e.LT:return this.nextToken(),"$lt";case $e.LTE:return this.nextToken(),"$lte";default:throw this.error(`Expected comparison operator, got "${this.curToken.value}"`)}}parseColumnList(){const e=[];for(e.push(this.parseColumnWithAlias());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseColumnWithAlias());return e}parseColumnWithAlias(){let e=this.parseColumnRef();if(this.curTokenIs($e.AS))this.nextToken(),e=`${e} AS ${this.expectIdentifier("alias")}`;else if(this.curToken.type===$e.IDENTIFIER&&!this._isReservedAfterFrom()&&!this._isJoinKeyword()){const t=this.curToken.value;this.nextToken(),e=`${e} AS ${t}`}return e}parseColumnRef(){if(this.curTokenIs($e.CASE))return this.parseCaseExpressionText();if(this.curTokenIs($e.NUMBER)){const e=this.curToken.value;return this.nextToken(),e}if(this.curTokenIs($e.STRING)){const e=this.curToken.value;return this.nextToken(),`'${e}'`}if(this.curTokenIs($e.COUNT)||this.curTokenIs($e.SUM)||this.curTokenIs($e.AVG)||this.curTokenIs($e.MIN)||this.curTokenIs($e.MAX))return this.parseAggregateCall();const e=this.expectIdentifier("column name");return this.curTokenIs($e.DOT)?(this.nextToken(),`${e}.${this.expectIdentifier("column name")}`):e}parseCaseExpressionText(){const e=this.curToken.position;this.nextToken();let t=1,s=e+4;for(;!this.curTokenIs($e.EOF)&&t>0&&(this.curTokenIs($e.CASE)&&t++,!this.curTokenIs($e.END)||(t--,s=this.curToken.position+3,this.nextToken(),0!==t));)s=this.curToken.position+this.curToken.value.length,this.nextToken();let n=this.sql.slice(e,s);return this.curTokenIs($e.AS)?(this.nextToken(),n+=` AS ${this.expectIdentifier("alias")}`):this.curToken.type!==$e.IDENTIFIER||this.curTokenIs($e.COMMA)||this._isReservedAfterFrom()||(n+=` AS ${this.curToken.value}`,this.nextToken()),n}parseAggregateCall(){const e=this.curToken.value.toUpperCase();this.nextToken(),this.expect($e.LPAREN);let t,s=!1;this.curTokenIs($e.DISTINCT)&&(s=!0,this.nextToken()),this.curTokenIs($e.STAR)?(t="*",this.nextToken()):t=this.parseColumnRef(),this.expect($e.RPAREN);let n="";this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type===$e.IDENTIFIER&&this._isAggregateAlias()&&(n=this.curToken.value,this.nextToken());const i=s?`DISTINCT ${t}`:t;return n?`${e}(${i}) AS ${n}`:`${e}(${i})`}_isAggregateAlias(){return!this._isReservedAfterFrom()&&!this._isJoinKeyword()}parseIdentifierList(){const e=[];for(e.push(this.parseIdentifierWithDot());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseIdentifierWithDot());return e}parseValueList(){const e=[];for(e.push(this.parseValue());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseValue());return e}parseOrderByList(){const e=[];for(e.push(this.parseOrderBy());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseOrderBy());return e}parseOrderBy(){const e=this.parseIdentifierWithDot();let t,s="asc";return this.curTokenIs($e.ASC)?this.nextToken():this.curTokenIs($e.DESC)&&(s="desc",this.nextToken()),this.curTokenIs($e.IDENTIFIER)&&"NULLS"===this.curToken.value.toUpperCase()&&(this.nextToken(),this.curTokenIs($e.IDENTIFIER)&&"FIRST"===this.curToken.value.toUpperCase()?(t="first",this.nextToken()):this.curTokenIs($e.IDENTIFIER)&&"LAST"===this.curToken.value.toUpperCase()&&(t="last",this.nextToken())),{column:e,direction:s,...t?{nulls:t}:{}}}parseIdentifierWithDot(){const e=this.expectIdentifier("identifier");return this.curTokenIs($e.DOT)?(this.nextToken(),`${e}.${this.expectIdentifier("identifier")}`):e}parseValue(){switch(this.curToken.type){case $e.STRING:{const e=this.curToken.value;return this.nextToken(),e}case $e.NUMBER:{const e=Number(this.curToken.value);return this.nextToken(),e}case $e.TRUE:return this.nextToken(),!0;case $e.FALSE:return this.nextToken(),!1;case $e.NULL:return this.nextToken(),null;default:throw this.error(`Expected value, got "${this.curToken.value}"`)}}nextToken(){this.curToken=this.peekToken,this.peekToken=this.lexer.nextToken()}curTokenIs(e){return this.curToken.type===e}expect(e){if(!this.curTokenIs(e))throw this.error(`Expected ${e}, got "${this.curToken.value}"`);this.nextToken()}expectIdentifier(e){if(this.curToken.type===$e.IDENTIFIER||this._isKeywordAsIdent()){const e=this.curToken.value;return this.nextToken(),e}throw this.error(`Expected ${e}, got "${this.curToken.value}"`)}_isKeywordAsIdent(){return this.curToken.type!==$e.EOF&&this.curToken.type!==$e.ILLEGAL&&this.curToken.type!==$e.STRING&&this.curToken.type!==$e.NUMBER&&this.curToken.type!==$e.COMMA&&this.curToken.type!==$e.LPAREN&&this.curToken.type!==$e.RPAREN&&this.curToken.type!==$e.SEMICOLON&&this.curToken.type!==$e.EQ&&this.curToken.type!==$e.NEQ&&this.curToken.type!==$e.GT&&this.curToken.type!==$e.GTE&&this.curToken.type!==$e.LT&&this.curToken.type!==$e.LTE&&this.curToken.type!==$e.DOT&&this.curToken.type!==$e.STAR}expectNumber(e){if(this.curToken.type===$e.NUMBER){const e=Number(this.curToken.value);return this.nextToken(),e}throw this.error(`Expected ${e}, got "${this.curToken.value}"`)}error(e){return new n(`Parse error at position ${this.curToken.position}: ${e}`,"PARSE_ERROR")}}function Me(e){return new _e(e).parseAllStatements()}function Pe(e){return new _e(e).parseWhere()}function Be(e){return null===e?"n":void 0===e?"u":"string"==typeof e?`s${e}`:"number"==typeof e?`d${e}`:"boolean"==typeof e?`b${e}`:"object"==typeof e?`o${JSON.stringify(e)}`:`x${String(e)}`}function Ke(e){const t=e.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);if(!t)return null;const s=t[1],n=t[2]??null,i=[],r=/WHEN\s+([\s\S]*?)\s+THEN\s+([\s\S]*?)(?=\s+WHEN\s+|\s+ELSE\s+|\s*$)/gi;let a;for(;null!==(a=r.exec(s));){let e=null;try{e=Pe(a[1].trim())}catch{}i.push({cond:e,value:a[2].trim()})}let o=null;const h=s.match(/\sELSE\s+([\s\S]*)$/i);return h&&(o=h[1].trim()),{whens:i,elseValue:o,alias:n}}function qe(e,t){const s=e.trim();if("null"===s)return null;if("true"===s)return!0;if("false"===s)return!1;const n=Number(s);if(""!==s&&!isNaN(n))return n;const i=s.match(/^'(.*)'$/s)||s.match(/^"(.*)"$/s);return i?i[1]:/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(s)?t[s]??null:s}function ze(e,t){for(const{cond:s,value:n}of e.whens)if(s&&o(t,s))return qe(n,t);return null!==e.elseValue?qe(e.elseValue,t):null}class Fe{constructor(e,t=0){this.engine=e,this.maxRowsPerQuery=t}async execute(e){switch(e.type){case"SELECT":return this.executeSelect(e);case"SELECT_UNION":return this.executeSelectUnion(e);case"EXPLAIN":return this.executeExplain(e);case"INSERT":return this.executeInsert(e);case"UPDATE":return this.executeUpdate(e);case"DELETE":return this.executeDelete(e);case"CREATE_TABLE":return this.executeCreateTable(e);case"DROP_TABLE":return this.executeDropTable(e);case"ALTER_TABLE":return this.executeAlterTable(e);case"TRUNCATE_TABLE":return this.executeTruncateTable(e);case"CREATE_INDEX":return this.executeCreateIndex(e);case"DROP_INDEX":return this.executeDropIndex(e);case"BEGIN":return this.executeBegin();case"COMMIT":return this.executeCommit();case"ROLLBACK":return this.executeRollback();case"SAVEPOINT":return this.executeSavepoint(e);case"ANALYZE":return this.executeAnalyze(e);case"REINDEX":return this.executeReindex(e);case"VACUUM":return this.executeVacuum();default:throw new n("Unknown statement type","UNKNOWN_STATEMENT")}}async executeSelectUnion(e){const t=await this.executeSelectPart(e.left),s=await this.executeSelectPart(e.right),n=t.length>0?Object.keys(t[0]):[],i=t.map(e=>e);if(e.all){for(const e of s)i.push(this.projectUnionRow(e,n));return i}const r=new Set,a=[];for(const e of i){const t=Object.values(e).map(Be).join("");r.has(t)||(r.add(t),a.push(e))}for(const e of s){const t=this.projectUnionRow(e,n),s=Object.values(t).map(Be).join("");r.has(s)||(r.add(s),a.push(t))}return a}async executeSelectPart(e){return"SELECT_UNION"===e.type?this.executeSelectUnion(e):this.executeSelect(e)}projectUnionRow(e,t){if(0===t.length)return e;const s=Object.values(e),n={};for(let e=0;e0)try{const e=await this.engine.getTableSchema(r.table);if(e){const t=s=>{for(const[n,i]of Object.entries(s)){if("$and"===n){for(const e of i){const s=t(e);if(s)return s}continue}if("$or"===n||"$not"===n)continue;const s=e.columns[n];if(s){if(s.primaryKey)return"pk";if(s.index||s.unique)return`index:${n}`}}return null};a=t(r.where)??"none"}}catch{}return{type:e.query.type,table:r?.table,columns:r?.columns,where:r?.where||{},orderBy:r?.orderBy||[],limit:r?.limit,offset:r?.offset,usingIndex:a,estimatedRows:n,actualTimeMs:i}}async executeSelect(e){const t=!!(e.groupBy&&e.groupBy.length>0),s=!t&&this._hasAggregateColumn(e.columns);let n;const i=!!(e.joins&&e.joins.length>0),r=this.hasCaseColumn(e.columns)||!!e.where&&this.whereHasCase(e.where),a=this.orderByUsesSelectAlias(e),h=e.columns.some(e=>/\s+AS\s+\w+$/i.test(e));if(e.fromSubquery){const t=await this.executeSelectPart(e.fromSubquery);n=i?await this.executeJoinSelect(e,t.map(t=>this.prefixRow(t,e.alias??""))):t,!i&&e.where&&Object.keys(e.where).length>0&&(e.where=await this.resolveSubqueries(e.where),n=n.filter(t=>o(t,e.where)))}else if(e.from||i)if(i)n=await this.executeJoinSelect(e);else{e.where&&Object.keys(e.where).length>0&&(e.where=this.normalizeWhereColumns(e.where,[e.alias??e.from]));const i=[e.alias??e.from].filter(Boolean);if(e.orderBy&&e.orderBy.length>0&&(e.orderBy=e.orderBy.map(e=>({...e,column:this.stripAlias(e.column,i)}))),e.groupBy&&e.groupBy.length>0&&(e.groupBy=e.groupBy.map(e=>this.stripAlias(e,i))),e.columns=e.columns.map(e=>{if("*"===e||/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e)||/^\s*CASE\b/i.test(e)||/^'/.test(e))return e;const t=e.match(/^(.+?)\s+AS\s+(\w+)$/i);if(t){const s=this.stripAlias(t[1].trim(),i);return s===t[1].trim()?e:`${s} AS ${t[2]}`}return this.stripAlias(e,i)}),e.where&&this.hasCorrelatedRefs(e.where)){const i=ve(t||s?{...e,columns:["*"]}:e);i.columns=["*"],a&&(i.orderBy=void 0,i.limit=void 0,i.offset=void 0),n=await this.engine.find(i.table,{...i,where:this.stripCorrelatedExists(e.where)}),n=await this.filterCorrelated(n,e.where)}else{e.where&&Object.keys(e.where).length>0&&(e.where=await this.resolveSubqueries(e.where));const i=ve(t||s?{...e,columns:["*"]}:e);(r||h)&&(i.columns=["*"]),a&&(i.orderBy=void 0,i.limit=void 0,i.offset=void 0),n=await this.engine.find(i.table,i)}}else n=[{}];if(s&&(n=[this.computeSingleAggregate(n,e)]),t&&(n=this.executeGroupBy(n,e)),e.distinct&&(n=this.executeDistinct(n)),e.having&&Object.keys(e.having).length>0){e.having=await this.resolveSubqueries(e.having);const t=e._aggAliasMap;if(t&&t.size>0){const s={};for(const[n,i]of Object.entries(e.having))s[t.get(n)??n]=i;e.having=s}n=n.filter(t=>o(t,e.having))}e.orderBy&&e.orderBy.length>0&&(n=l(n,e.orderBy)),t||s||!(e.columns.length>0)||1===e.columns.length&&"*"===e.columns[0]||(n=n.map(t=>this.projectRow(t,e.columns))),a&&e.orderBy&&e.orderBy.length>0&&(n=l(n,e.orderBy));const c=e.offset??0,u=e.limit??n.length;return n=n.slice(c,c+u),this.maxRowsPerQuery>0&&n.length>this.maxRowsPerQuery&&(n=n.slice(0,this.maxRowsPerQuery)),n}async executeJoinSelect(e,t){const s=e.alias??e.from;let n;if(t)n=t;else{const{pushable:t}=this.extractPushableWhere(e.where??{},s);n=(await this.engine.find(e.from,{table:e.from,where:Object.keys(t).length>0?t:void 0})).map(e=>this.prefixRow(e,s))}let i=n;for(const t of e.joins){const e=t.alias??t.table,n=await this.tryHashJoin(i,t,e,s);if(n){i=n;continue}const r=(await this.engine.find(t.table,{table:t.table})).map(t=>this.prefixRow(t,e));i=this.joinRows(i,r,t)}return e.where&&Object.keys(e.where).length>0&&(this.hasCorrelatedRefs(e.where)?i=await this.filterCorrelated(i,e.where):(e.where=await this.resolveSubqueries(e.where),i=i.filter(t=>o(t,e.where)))),i}prefixRow(e,t){const s={};for(const[n,i]of Object.entries(e))s[`${t}.${n}`]=i;return s}extractPushableWhere(e,t){const s={};if(!t)return{pushable:s};const n=`${t}.`;for(const[t,i]of Object.entries(e))t.startsWith(n)&&("object"==typeof i&&null!==i&&("$col"in i||"$subquery"in i||"$and"in i||"$or"in i||"$not"in i)||(s[t.slice(n.length)]=i));return{pushable:s}}async tryHashJoin(e,t,s,n){if("CROSS"===t.type||"RIGHT"===t.type)return null;const i=[],r=e=>{for(const[t,s]of Object.entries(e)){if("$and"===t){if(!s.every(r))return!1;continue}if("$or"===t||"$not"===t)return!1;let e=null;if("object"==typeof s&&null!==s){const t=s;"$eq"in t&&"object"==typeof t.$eq&&null!==t.$eq&&"$col"in t.$eq?e=String(t.$eq.$col):"$col"in t&&1===Object.keys(t).length&&(e=String(t.$col))}if(!e)return!1;const a=!!n&&t.startsWith(`${n}.`);i.push({leftCol:a?t:e,rightCol:a?e:t})}return!0};if(!r(t.on))return null;if(0===i.length)return null;const a=await this.engine.getTableSchema(t.table);if(!a)return null;const o=i.find(e=>{const t=e.rightCol.split(".").pop(),s=a.columns[t];return s&&(s.primaryKey||s.index||s.unique)});if(!o)return null;const h=o.rightCol.split(".").pop(),c=Array.from(new Set(e.map(e=>e[o.leftCol]).filter(e=>null!=e)));if(0===c.length)return null;const l=await this.engine.find(t.table,{table:t.table,where:{[h]:{$in:c}}}),u=new Map;for(const e of l){const t=i.map(t=>String(e[t.rightCol.split(".").pop()]??"\0")).join("");u.has(t)||u.set(t,[]),u.get(t).push(e)}const f={};for(const e of Object.keys(a.columns))f[e]=null;const d=[];for(const n of e){const e=i.map(e=>String(n[e.leftCol]??"\0")).join(""),r=u.get(e);if(r&&r.length>0)for(const e of r)d.push({...n,...this.prefixRow(e,s)});else"LEFT"===t.type&&d.push({...n,...this.prefixRow(f,s)})}return d}joinRows(e,t,s){if("CROSS"===s.type){const s=[];for(const n of e)for(const e of t)s.push({...n,...e});return s}const n=[];for(const i of e){let e=!1;for(const r of t){const t={...i,...r};o(t,s.on,{$col:!0})&&(n.push(t),e=!0)}if(!e&&"LEFT"===s.type){const e={};for(const s of Object.keys(t[0]??{}))e[s]=null;n.push({...i,...e})}}if("RIGHT"===s.type)for(const i of t)if(!e.some(e=>o({...e,...i},s.on,{$col:!0}))){const t={};for(const s of Object.keys(e[0]??{}))t[s]=null;n.push({...t,...i})}return n}executeGroupBy(e,t){const s=new Map;for(const n of e){const e=t.groupBy.map(e=>Be(n[e])).join("");s.has(e)||s.set(e,[]),s.get(e).push(n)}const n=[],i=new Map;for(const e of s.values()){const s={};for(const n of t.groupBy)s[n]=e[0][n];for(const n of t.columns){if("*"===n)continue;const r=n.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);if(r){const[,t,a,o]=r,h=this.computeAggregate(t.toUpperCase(),e,a.trim()),c=`${t.toUpperCase()}(${a.trim()})`,l=o||n;l!==c&&i.set(c,l),s[l]=h}else if(/^\s*CASE\b/i.test(n)){const t=Ke(n);s[t?.alias??n]=t?ze(t,e[0]):null}else t.groupBy.includes(n)||(s[n]=e[0][n])}n.push(s)}return t._aggAliasMap=i,n}computeAggregate(e,t,s){const n=/^\s*CASE\b/i.test(s)?Ke(s):null,i=!n&&/^\s*DISTINCT\s+/i.test(s),r=i?s.replace(/^\s*DISTINCT\s+/i,"").trim():s,a=t.map(e=>n?ze(n,e):e[r]).filter(e=>null!=e);if("COUNT"===e)return"*"===r?t.length:i?new Set(a.map(e=>"object"==typeof e?JSON.stringify(e):String(e))).size:a.length;const o=a.map(Number),h=i?Array.from(new Set(o)):o;switch(e){case"SUM":return h.reduce((e,t)=>e+t,0);case"AVG":return 0===h.length?0:h.reduce((e,t)=>e+t,0)/h.length;case"MIN":return 0===h.length?0:Math.min(...h);case"MAX":return 0===h.length?0:Math.max(...h);default:return 0}}executeDistinct(e){const t=new Set;return e.filter(e=>{const s=Object.values(e).map(Be).join("");return!t.has(s)&&(t.add(s),!0)})}async executeInsert(e){const t=await this.engine.getTableSchema(e.into);if(!t)throw new n(`Table "${e.into}" does not exist`,"TABLE_NOT_FOUND");const s=e.columns??Object.keys(t.columns);if(e.select){const t=await this.executeSelectPart(e.select);let n=[];const i=e.select;if("SELECT"===i.type)if(i.columns&&i.columns.length>0&&"*"!==i.columns[0])n=i.columns.map(e=>e.split(".").pop());else if(i.from){const e=await this.engine.getTableSchema(i.from);n=e?Object.keys(e.columns):[]}0===n.length&&t.length>0&&(n=Object.keys(t[0]));const r=t.map(e=>{const t={};for(let i=0;i{const t={};for(let n=0;ne.primaryKey))throw new n(`Composite primary keys are not supported yet: table "${e.name}" already has a primary key column`,"SCHEMA_ERROR");if("function"==typeof this.engine.alterTable)return this.engine.alterTable(e.name,e.action,{...y(e.column),name:e.column.name});if("ADD"===e.action){if(t.columns[e.column.name])throw new n(`Column "${e.column.name}" already exists in table "${e.name}"`,"COLUMN_EXISTS");t.columns[e.column.name]=y(e.column)}else if("DROP"===e.action){if(!t.columns[e.column.name])throw new n(`Column "${e.column.name}" does not exist in table "${e.name}"`,"COLUMN_NOT_FOUND");delete t.columns[e.column.name];const s=await this.engine.find(e.name,{table:e.name}),i=e.column.name;for(const e of s)i in e&&delete e[i]}}}async executeTruncateTable(e){if(!await this.engine.hasTable(e.name))throw new n(`Table "${e.name}" does not exist`,"TABLE_NOT_FOUND");return this.engine.clear(e.name)}async executeCreateIndex(e){if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");const t=await this.engine.getTableSchema(e.table);if(t&&!t.columns[e.column])throw new n(`Column "${e.column}" does not exist in table "${e.table}"`,"COLUMN_NOT_FOUND");if("function"!=typeof this.engine.createIndex)throw new n(`Engine "${this.engine.name}" does not support CREATE INDEX`,"NOT_SUPPORTED");return this.engine.createIndex(e.table,e.column,e.unique)}async executeDropIndex(e){if("function"!=typeof this.engine.dropIndex)throw new n(`Engine "${this.engine.name}" does not support DROP INDEX`,"NOT_SUPPORTED");return this.engine.dropIndex(e.table,e.column,e.name)}async executeBegin(){return this.engine.beginTransaction()}async executeCommit(){return this.engine.commitTransaction()}async executeRollback(){return this.engine.rollbackTransaction()}async executeSavepoint(e){const t=this.engine;if("SAVE"===e.action){if("function"!=typeof t.savepoint)throw new n(`Engine "${this.engine.name}" does not support SAVEPOINT`,"NOT_SUPPORTED");return t.savepoint(e.name)}if("ROLLBACK"===e.action){if("function"!=typeof t.rollbackToSavepoint)throw new n(`Engine "${this.engine.name}" does not support ROLLBACK TO SAVEPOINT`,"NOT_SUPPORTED");return t.rollbackToSavepoint(e.name)}if("function"!=typeof t.releaseSavepoint)throw new n(`Engine "${this.engine.name}" does not support RELEASE SAVEPOINT`,"NOT_SUPPORTED");return t.releaseSavepoint(e.name)}async executeAnalyze(e){const t=this.engine;if("function"!=typeof t.analyzeTable)throw new n(`Engine "${this.engine.name}" does not support ANALYZE`,"NOT_SUPPORTED");if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");return t.analyzeTable(e.table)}async executeReindex(e){const t=this.engine;if("function"!=typeof t.reindexTable)throw new n(`Engine "${this.engine.name}" does not support REINDEX`,"NOT_SUPPORTED");if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");return t.reindexTable(e.table)}async executeVacuum(){const e=this.engine;if("function"!=typeof e.vacuum)throw new n(`Engine "${this.engine.name}" does not support VACUUM`,"NOT_SUPPORTED");return e.vacuum()}hasCaseColumn(e){return e.some(e=>/^\s*CASE\b/i.test(e))}orderByUsesSelectAlias(e){if(!e.orderBy||0===e.orderBy.length)return!1;const t=new Set;for(const s of e.columns){const e=s.match(/\s+AS\s+(\w+)$/i);if(e)t.add(e[1]);else if(/^\s*CASE\b/i.test(s)){const e=Ke(s);e?.alias&&t.add(e.alias)}}return 0!==t.size&&e.orderBy.some(e=>t.has(e.column))}whereHasCase(e){for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if(/^\s*CASE\b/i.test(t))return!0}else if(this.whereHasCase(s))return!0}else if(s.some(e=>this.whereHasCase(e)))return!0;return!1}getEngine(){return this.engine}projectRow(e,t){const s=[],n=[],i=[],r=[];let a=!1;for(const e of t){if("*"===e){a=!0;continue}const t=Ke(e);if(t){i.push({alias:t.alias??e,expr:t});continue}const o=e.match(/^(.+?)\s+AS\s+(\w+)$/i);if(o){n.push({alias:o[2],source:o[1].trim()});continue}const h=e.match(/^'(.*)'$/s);if(h){const t=h[1].replace(/''/g,"'");r.push({key:e,value:t});continue}s.push(e)}const o=a?{...e}:s.length>0?f(e,s):{};for(const{alias:t,source:s}of n)if("*"===s)Object.assign(o,e);else{const n=s.match(/^'(.*)'$/s);o[t]=n?n[1].replace(/''/g,"'"):e[s]}for(const{key:e,value:t}of r)o[e]=t;for(const{alias:t,expr:s}of i)o[t]=ze(s,e);return o}_hasAggregateColumn(e){return e.some(e=>/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e))}computeSingleAggregate(e,t){const s={};for(const n of t.columns){if("*"===n)continue;const t=n.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);if(t){const[,i,r,a]=t;s[a||n]=this.computeAggregate(i.toUpperCase(),e,r.trim())}else s[n]=e.length>0?e[0][n]:null}return s}normalizeWhereColumns(e,t){const s={};for(const[n,i]of Object.entries(e))"$and"!==n&&"$or"!==n?"$not"!==n?"$exists"!==n?s[this.stripAlias(n,t)]=this.normalizeFieldValue(i,t):s[n]=this.normalizeExistsValue(i,t):s.$not=this.normalizeWhereColumns(i,t):s[n]=i.map(e=>this.normalizeWhereColumns(e,t));return s}normalizeExistsValue(e,t){if("object"!=typeof e||null===e)return e;const s=e;if(s.$subquery){const e=s.$subquery,n=[e.alias??e.from,...t].filter(Boolean);return{...s,$subquery:{...e,where:this.normalizeWhereColumns(e.where,n)}}}return e}normalizeFieldValue(e,t){if("object"!=typeof e||null===e||Array.isArray(e))return e;const s={};for(const[n,i]of Object.entries(e))"$and"===n||"$or"===n?s[n]=i.map(e=>this.normalizeWhereColumns(e,t)):"$not"===n&&"object"==typeof i&&null!==i?s[n]=this.normalizeFieldValue(i,t):"$col"===n?s[n]=this.stripAlias(String(i),t):"object"==typeof i&&null!==i&&!Array.isArray(i)&&"$col"in i?s[n]={$col:this.stripAlias(String(i.$col),t)}:s[n]=i;return s}stripAlias(e,t){for(const s of t){if(!s)continue;const t=`${s}.`;if(e.startsWith(t))return e.slice(t.length)}return e}hasCorrelatedRefs(e){for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"!==t){if(/^\s*CASE\b/i.test(t))return!0;if(this.fieldHasColRef(s))return!0}else if("object"==typeof s&&null!==s&&"$subquery"in s)return!0}else if(this.hasCorrelatedRefs(s))return!0}else if(s.some(e=>this.hasCorrelatedRefs(e)))return!0;return!1}fieldHasColRef(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;const t=e;if("$col"in t)return!0;if("$and"in t||"$or"in t)return(t.$and??t.$or).some(e=>this.hasCorrelatedRefs(e));if("$not"in t&&"object"==typeof t.$not&&null!==t.$not)return this.fieldHasColRef(t.$not);for(const[,e]of Object.entries(t))if("object"==typeof e&&null!==e&&!Array.isArray(e)){if("$col"in e)return!0;if(this.fieldHasColRef(e))return!0}return!1}stripCorrelatedExists(e){const t={};for(const[s,n]of Object.entries(e))if("$and"!==s&&"$or"!==s){if("$not"===s){const e=this.stripCorrelatedExists(n);Object.keys(e).length>0&&(t.$not=e);continue}"$exists"!==s&&(/^\s*CASE\b/i.test(s)||(t[s]=n))}else t[s]=n.map(e=>this.stripCorrelatedExists(e));return t}async filterCorrelated(e,t){const s=[];for(const n of e){let e=this.resolveCaseKeys(t,n);e=await this.resolveSubqueries(e,n),o(n,e)&&s.push(n)}return s}resolveCaseKeys(e,t){const s={};for(const[n,i]of Object.entries(e))if("$and"!==n&&"$or"!==n)if("$not"!==n){if(/^\s*CASE\b/i.test(n)){const e=Ke(n);if(!e)continue;const r=ze(e,t);if(!this.caseConditionMatches(r,i))return{$caseResult:!1};s.$caseResult=!0;continue}s[n]=i}else s.$not=this.resolveCaseKeys(i,t);else s[n]=i.map(e=>this.resolveCaseKeys(e,t));return s}caseConditionMatches(e,t){if("object"!=typeof t||null===t||Array.isArray(t))return e===t;const s=t;for(const[t,n]of Object.entries(s))switch(t){case"$eq":if(e!==n)return!1;break;case"$ne":if(e===n)return!1;break;case"$gt":if(!(e>n))return!1;break;case"$gte":if(!(e>=n))return!1;break;case"$lt":if(!(ethis.bindWhereRefs(e,t)):"$not"===n&&"object"==typeof i&&null!==i?s[n]=this.bindColumnRefs(i,t):"object"==typeof i&&null!==i&&!Array.isArray(i)&&"$col"in i?s[n]=t[String(i.$col)]??null:s[n]=i;return s}bindWhereRefs(e,t){const s={};for(const[n,i]of Object.entries(e))"$and"===n||"$or"===n?s[n]=i.map(e=>this.bindWhereRefs(e,t)):"$not"===n?s.$not=this.bindWhereRefs(i,t):s[n]="$exists"===n?i:this.bindColumnRefs(i,t);return s}async resolveSubqueries(e,t){t&&(e=this.bindWhereRefs(e,t));const s={};for(const[n,i]of Object.entries(e)){if("$exists"===n&&"object"==typeof i&&null!==i){const e=i,n=e.$subquery,r=!!e.$negate;let a=n.where;this.hasCorrelatedRefs(a)&&(a=this.bindWhereRefs(a,t??{}));const o=await this.executeSelectPart({...n,where:a});s.$exists=o.length>0!==r;continue}"$and"===n&&Array.isArray(i)?s.$and=await Promise.all(i.map(e=>this.resolveSubqueries(e,t))):"$or"===n&&Array.isArray(i)?s.$or=await Promise.all(i.map(e=>this.resolveSubqueries(e,t))):"$not"!==n||"object"!=typeof i||null===i?s[n]="object"==typeof i&&null!==i?await this.resolveOperatorSubqueries(i):i:s.$not=await this.resolveSubqueries(i,t)}return s}async resolveOperatorSubqueries(e){const t={};for(const[s,n]of Object.entries(e))if("$and"===s&&Array.isArray(n))t.$and=await Promise.all(n.map(e=>this.resolveSubqueries(e)));else if("$or"===s&&Array.isArray(n))t.$or=await Promise.all(n.map(e=>this.resolveSubqueries(e)));else if("$not"!==s)if("object"==typeof n&&null!==n&&"$subquery"in n){const e=n.$subquery,i=await this.executeSelect(e);if("$in"===s||"$nin"===s){const e=Object.keys(i[0]||{})[0],n=i.map(t=>t[e]);t[s]=n}else if(0===i.length)t[s]=null;else{const e=Object.keys(i[0])[0];t[s]=i[0][e]}}else t[s]=n;else t.$not="object"==typeof n&&null!==n?await this.resolveOperatorSubqueries(n):n;return t}}function je(e){if(null==e)return"NULL";if("number"==typeof e)return Number.isFinite(e)?String(e):"NULL";if("boolean"==typeof e)return e?"TRUE":"FALSE";if("string"==typeof e)return`'${e.replace(/'/g,"''")}'`;throw new n("Object/array query parameters are not supported by SQL binding (pass JSON strings explicitly)","PARAM_ERROR")}class Ve{constructor(e){this.tables=new Map,this.completed=!1,this.engine=e}table(e){let t=this.tables.get(e);return t||(t=new Le(this.engine,e),this.tables.set(e,t)),t}_markCompleted(){this.completed=!0}isCompleted(){return this.completed}}class We{constructor(e){this.engine=e}async execute(e){const t=new Ve(this.engine);await this.engine.beginTransaction();try{const s=await e(t);return await this.engine.commitTransaction(),t._markCompleted(),s}catch(e){try{await this.engine.rollbackTransaction()}catch{}if(e instanceof n)throw e;throw new n(`Transaction failed: ${e.message}`,"TRANSACTION_ERROR",e)}}}class He{constructor(){this.plugins=[],this.hooks=new Map}register(e,t){const s=e.priority??0,n=this.plugins.findIndex(e=>(e.priority??0)t.name===e);-1!==t&&(this.plugins[t].destroy(),this.plugins.splice(t,1))}getPlugins(){return[...this.plugins]}on(e,t){const s=this.hooks.get(e)??[];s.push(t),this.hooks.set(e,s)}off(e,t){const s=this.hooks.get(e);if(s){const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}async trigger(e,...t){const s=this.hooks.get(e);if(s)for(const e of s)await e(...t)}destroy(){for(const e of this.plugins)try{e.destroy()}catch(e){}this.plugins=[],this.hooks.clear()}}class Ge{static async create(e){const t=new Ge(e);return await t.init(),t}get version(){return this._version}get maxRowsPerQuery(){return this.config.maxRowsPerQuery??0}get debug(){return this.config.debug??!1}constructor(e){this.ready=!1,this.tableCache=new Map,this.channel=null,this.listeners=new Map,this.migrations=new Map,this.config=e,this.name=e.name??s.name,this.mode=e.mode??s.mode,this._version=e.version??s.version,this.pluginManager=new He,e.multiTabSync&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`metona-sqlark:${this.name}`),this.channel.onmessage=e=>{const t=e.data;t&&"change"===t.type&&(this.emit(t.table??"",{type:"external",table:t.table??""}),this.engine instanceof Re&&this.engine.reloadMemoryFromDisk().catch(()=>{}))})}async init(){if(this.engine=this.createEngine(),await this.engine.open(this.name,this.version),"function"==typeof this.engine.getMeta)try{const e=await this.engine.getMeta("__metona_version");null!=e&&Number(e)>=1&&(this._version=Math.max(this._version,Math.floor(Number(e))))}catch{}if(this.executor=new Fe(this.engine,this.maxRowsPerQuery),this.transactionManager=new We(this.engine),this.config.plugins)for(const e of this.config.plugins)this.pluginManager.register(e,this);this.ready=!0,this.config.onReady&&this.config.onReady(this)}isReady(){return this.ready}async defineTable(e,t){this.ensureReady();const s=d(e,t);try{await this.pluginManager.trigger("beforeCreateTable",s),await this.engine.createTable(s),await this.pluginManager.trigger("afterCreateTable",s)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}table(e){this.ensureReady();let t=this.tableCache.get(e);return t||(t=new Le(this.engine,e,this.executor,e=>this.broadcastChange(e),(e,t)=>this.pluginManager.trigger(e,...t)),this.tableCache.set(e,t)),t}async dropTable(e){this.ensureReady();try{await this.pluginManager.trigger("beforeDropTable",e),await this.engine.dropTable(e),await this.pluginManager.trigger("afterDropTable",e)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}async getTableNames(){return this.ensureReady(),this.engine.getTableNames()}async query(e,t){this.ensureReady();const s=this.debug?Date.now():0;let i;await this.pluginManager.trigger("beforeQuery",e);try{const s=function(e,t){if(!t)return e;let s="",i=0,r=0,a=null;for(;i=t.length)throw new n(`Too few query parameters: placeholder #${r+1} has no value (got ${t.length} total)`,"PARAM_ERROR");s+=je(t[r]),r++,i++}else{for(s+=e[i]+e[i+1],i+=2;i/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e)),o=e=>{if(!e)return!1;for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"===t)return!0;if("object"==typeof s&&null!==s)for(const[,e]of Object.entries(s))if("object"==typeof e&&null!==e&&("$subquery"in e||"$col"in e))return!0}else if(o(s))return!0}else if(s.some(e=>o(e)))return!0;return!1};if(!(r.joins||r.groupBy||r.having||r.distinct||a||r.orderBy&&r.orderBy.length>0||o(r.where))&&"function"==typeof this.engine.findStream&&"AsyncFunction"!==t.constructor?.name){const e=this.normalizeWhereForStream(r),s=r.columns.filter(e=>!/\s+AS\s+\w+$/i.test(e));return this.engine.findStream(r.from,{table:r.from,columns:s.length>0&&"*"!==s[0]?s:["*"],where:e&&Object.keys(e).length>0?e:void 0,limit:r.limit,offset:r.offset},t)}const h=await this.query(e);if(Array.isArray(h)){for(const e of h)await t(e);return h.length}return 0}normalizeWhereForStream(e){const t=[e.alias??e.from].filter(Boolean),s=e=>{for(const s of t)if(e.startsWith(`${s}.`))return e.slice(s.length+1);return e},n=e=>{const t={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?t[i]=r.map(n):"$not"===i&&"object"==typeof r&&null!==r?t.$not=n(r):t[s(i)]=r;return t};return n(e.where??{})}async transaction(e){this.ensureReady(),await this.pluginManager.trigger("beforeTransaction");try{const t=await this.transactionManager.execute(e);return await this.pluginManager.trigger("afterTransaction"),t}catch(e){throw this._onError(e),e}}async exportTable(e){return this.ensureReady(),this.engine.find(e,{table:e})}async importTable(e,t){this.ensureReady();try{return await this.engine.insert(e,t)}catch(e){throw this._onError(e),e}}async exportAll(){this.ensureReady();const e={},t=await this.engine.getTableNames();for(const s of t)e[s]=await this.engine.find(s,{table:s});return e}async backup(){return this.ensureReady(),"function"==typeof this.engine.backup?this.engine.backup():this.exportAll()}subscribe(e,t){const s=`change:${e}`;return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(t),()=>this.listeners.get(s)?.delete(t)}emit(e,t){const s=`change:${e}`;this.listeners.get(s)?.forEach(e=>e(t))}broadcastChange(e){if(this.channel)try{this.channel.postMessage({type:"change",table:e})}catch{}}writeStatementTable(e){switch(e.type){case"INSERT":return e.into;case"UPDATE":case"CREATE_INDEX":case"DROP_INDEX":return e.table;case"DELETE":return e.from;case"CREATE_TABLE":case"DROP_TABLE":case"TRUNCATE_TABLE":case"ALTER_TABLE":return e.name;default:return null}}async triggerStatementHooks(e,t,s){switch(e.type){case"INSERT":{let n=e.columns??[];if(0===n.length)try{const t=await this.engine.getTableSchema(e.into);n=t?Object.keys(t.columns):[]}catch{n=[]}const i=(e.values??[]).map(e=>{const t={};for(let s=0;se[0]-t[0]))t<=e&&t>this._version&&(await s(this),this._version=t);if("function"==typeof this.engine.setMeta)try{await this.engine.setMeta("__metona_version",String(this._version))}catch{}}async repair(){if(this.ensureReady(),"function"==typeof this.engine.repair)return await this.engine.repair(),void this.tableCache.clear();this.tableCache.clear()}async clearAll(){if(this.ensureReady(),"function"==typeof this.engine.clearAll)await this.engine.clearAll();else{const e=await this.engine.getTableNames();for(const t of e)await this.engine.dropTable(t)}this.tableCache.clear()}getPluginManager(){return this.pluginManager}on(e,t){this.pluginManager.on(e,t)}async close(){this.channel&&(this.channel.close(),this.channel=null),this.pluginManager.destroy(),this.engine&&await this.engine.close(),this.tableCache.clear(),this.ready=!1}getEngine(){return this.engine}createEngine(){const e=this.mode,t=this.config.diskEngine??"opfs";switch(e){case"memory":return new m;case"disk":return new _;case"aria":return new Ae({storageBackend:"memory"===t?"memory":"kv"===t?"kv":"opfs",...this.config.aria??{}});case"hybrid":return new Re(t);default:throw new n(`Unknown storage mode: ${e}`,"CONFIG_ERROR")}}ensureReady(){if(!this.ready)throw new n("Database not initialized. Call await db.init() first.","DB_NOT_READY")}_onError(e){if(this.config.onError)try{this.config.onError(e)}catch{}}_debug(e,...t){this.debug}}const Xe=new class{constructor(){this.connections=new Map,this.refCount=new Map}async connect(e){const t=e.name,s=this.connections.get(t);if(s&&s.isReady()){const e=(this.refCount.get(t)??0)+1;return this.refCount.set(t,e),s}const n=new Ge(e);return await n.init(),this.connections.set(t,n),this.refCount.set(t,1),n.disconnect=async()=>{await this.release(t)},n}async release(e){const t=(this.refCount.get(e)??1)-1;if(t<=0){const t=this.connections.get(e);t&&(await t.close(),this.connections.delete(e)),this.refCount.delete(e)}else this.refCount.set(e,t)}async forceClose(e){const t=this.connections.get(e);t&&(await t.close(),this.connections.delete(e)),this.refCount.delete(e)}async closeAll(){for(const[,e]of this.connections)try{await e.close()}catch{}this.connections.clear(),this.refCount.clear()}getActiveConnections(){return Array.from(this.connections.keys())}},Je=Ge;async function Qe(e){return Ge.create(e)}Je.connect=e=>Xe.connect(e),Je.disconnect=e=>Xe.release(e),Je.disconnectAll=()=>Xe.closeAll(),Je.getActiveConnections=()=>Xe.getActiveConnections();const Ye={VERSION:i,version:i,create:Qe,MetonaSqlark:Ge,MeSqlark:Ge};"undefined"!=typeof window&&(window.MetonaSqlark=Ye,window.MeSqlark=Ye);const Ze=Ge;e.AriaEngine=Ae,e.HybridEngine=Re,e.KVStoreEngine=_,e.MeSqlark=Ze,e.MemoryEngine=m,e.MetonaSqlark=Ge,e.OPFSBackend=w,e.Table=Le,e.VERSION=i,e.api=Ye,e.create=Qe,e.default=Ye,e.parse=function(e){return new _e(e).parseStatement()},e.parseAll=Me,e.tokenize=function(e){const t=new Ue(e),s=[];let n=t.nextToken();for(;n.type!==$e.EOF;)s.push(n),n=t.nextToken();return s.push(n),s},Object.defineProperty(e,"__esModule",{value:!0})}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MetonaSqlark={})}(this,function(e){"use strict";const t=["string","number","boolean","date","json"],s=Object.freeze({name:"metona-sqlark",mode:"hybrid",diskEngine:"opfs",version:1,maxRowsPerQuery:0,debug:!1,multiTabSync:!1,aria:void 0});class n extends Error{constructor(e,t,s){super(e),this.code=t,this.details=s,this.name="DatabaseError"}}const i="0.8.0",r=new Map;function a(e){if(!e)return!1;for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"===t)return!0;if("object"==typeof s&&null!==s&&!Array.isArray(s))for(const[,e]of Object.entries(s))if("object"==typeof e&&null!==e&&("$subquery"in e||"$col"in e))return!0}else if(a(s))return!0}else if(s.some(e=>a(e)))return!0;return!1}function o(e,t,s={}){for(const[n,i]of Object.entries(t))if("$caseResult"!==n){if("$exists"!==n){if("$and"===n){if(!i.every(t=>o(e,t,s)))return!1;continue}if("$or"===n){if(!i.some(t=>o(e,t,s)))return!1;continue}if("$not"!==n){if(!h(e[n],i,e,s))return!1}else if(o(e,i,s))return!1}else if(!0!==i)return!1}else if(!0!==i)return!1;return!0}function h(e,t,s,n){if("object"==typeof t&&null!==t&&"$and"in t)return t.$and.every(e=>o(s,e,n));if("object"==typeof t&&null!==t&&"$or"in t)return t.$or.some(e=>o(s,e,n));if("object"==typeof t&&null!==t&&"$not"in t)return!h(e,t.$not,s,n);if("object"!=typeof t||null===t||Array.isArray(t))return e===t;const i=t;if(n.$col&&"$col"in i&&1===Object.keys(i).length)return e===s[i.$col];for(const[t,r]of Object.entries(i)){let i=r;if(n.$col&&"object"==typeof r&&null!==r&&"$col"in r&&(i=s[r.$col]),!c(e,t,i))return!1}return!0}function c(e,t,s){switch(t){case"$eq":return e===s;case"$ne":return e!==s;case"$gt":return e>s;case"$gte":return e>=s;case"$lt":return e{for(const{column:n,direction:i,nulls:r}of t){const t=null===e[n]||void 0===e[n],a=null===s[n]||void 0===s[n];if(r&&(t||a)){if(t&&a)continue;return"first"===r?t?-1:1:t?1:-1}const o=u(e[n],s[n]);if(0!==o)return"desc"===i?-o:o}return 0})}function u(e,t){return e===t?0:null==e?1:null==t?-1:"string"==typeof e&&"string"==typeof t?e.localeCompare(t):"number"==typeof e&&"number"==typeof t?e-t:String(e).localeCompare(String(t))}function f(e,t){const s={};for(const n of t)if(n in e)s[n]=e[n];else for(const t of Object.keys(e))if(t.endsWith(`.${n}`)||t===n){s[n]=e[t];break}return s}function d(e,s){return function(e){if(0===Object.keys(e).length)throw new n("Table must have at least one column","SCHEMA_ERROR");let s=0;for(const[i,r]of Object.entries(e)){if("__proto__"===i)throw new n('Column name "__proto__" is not allowed',"SCHEMA_ERROR");if(!t.includes(r.type))throw new n(`Invalid type "${r.type}" for column "${i}". Valid types: ${t.join(", ")}`,"SCHEMA_ERROR");r.primaryKey&&s++}if(0===s)throw new n("Table must have at least one primary key column","SCHEMA_ERROR");if(s>1)throw new n(`Composite primary keys are not supported yet: table has ${s} primary key columns. Use a single primary key column (or a unique column combination) instead.`,"SCHEMA_ERROR")}(s),{name:e,columns:s}}function p(e){const t={};for(const[s,n]of Object.entries(e))void 0!==n&&(t[s]=n);return t}function y(e){return{type:e.type,primaryKey:e.primaryKey,unique:e.unique,required:e.required,default:e.default,index:e.index,maxLength:e.maxLength,min:e.min,max:e.max,references:e.references,onDelete:e.onDelete,onUpdate:e.onUpdate}}class m{constructor(){this.name="memory",this.tables=new Map,this.schemas=new Map,this.indexes=new Map,this.opened=!1,this.metaStore=new Map,this.uniqueIndexCols=new Set,this.snapshot=null}async open(e,t){this.opened||(this.opened=!0)}async close(){this.tables.clear(),this.schemas.clear(),this.indexes.clear(),this.metaStore.clear(),this.opened=!1}isOpen(){return this.opened}async repair(){}async clearAll(){const e=Array.from(this.schemas.keys());for(const t of e)await this.dropTable(t);this.metaStore.clear()}async getMeta(e){return this.metaStore.get(e)??null}async setMeta(e,t){this.metaStore.set(e,t)}async createTable(e){if(this.schemas.has(e.name))throw new n(`Table "${e.name}" already exists`,"TABLE_EXISTS");const t={name:e.name,columns:{}};for(const[s,n]of Object.entries(e.columns))t.columns[s]={...n};this.schemas.set(e.name,t),this.tables.set(e.name,new Map);const s=new Map;for(const[e,n]of Object.entries(t.columns))(n.index||n.unique)&&s.set(e,new Map);this.indexes.set(e.name,s)}async dropTable(e){this.ensureTable(e),this.schemas.delete(e),this.tables.delete(e),this.indexes.delete(e);const t=`${e}:`;for(const e of this.uniqueIndexCols)e.startsWith(t)&&this.uniqueIndexCols.delete(e)}async hasTable(e){return this.schemas.has(e)}async getTableNames(){return Array.from(this.schemas.keys())}async getTableSchema(e){return this.schemas.get(e)??null}async alterTable(e,t,s){if(this.snapshot)throw new n("ALTER TABLE is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e);if("ADD"===t){if(i.columns[s.name])throw new n(`Column "${s.name}" already exists in table "${e}"`,"COLUMN_EXISTS");return void(i.columns[s.name]=s)}if(!i.columns[s.name])throw new n(`Column "${s.name}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");(i.columns[s.name].index||i.columns[s.name].unique)&&this.indexes.get(e)?.delete(s.name),delete i.columns[s.name];const r=this.tables.get(e);for(const e of r.values())s.name in e&&delete e[s.name]}async insert(e,t){this.ensureTable(e);const s=this.schemas.get(e),i=this.tables.get(e),r=this.getPrimaryKey(s),a=[],o=[],h=new Set,c=new Map;for(const a of t){const t=this.validateRow(s,a),l=String(t[r]);if(i.has(l)||h.has(l))throw new n(`Duplicate primary key "${l}" in table "${e}"`,"DUPLICATE_KEY");h.add(l),this.checkInsertUniqueness(s,e,t,c),o.push(t)}for(const t of o){const s=String(t[r]);i.set(s,t),this.updateIndexes(e,t,s),a.push(s)}return a}getRow(e,t){const s=this.tables.get(e);return s?s.get(t)??null:null}async find(e,t){this.ensureTable(e);const s=this.tables.get(e);let n=this.tryIndexLookup(e,s,t);t.where&&Object.keys(t.where).length>0&&(n=n.filter(e=>o(e,t.where))),t.orderBy&&t.orderBy.length>0&&(n=l(n,t.orderBy));const i=t.offset??0,r=t.limit??n.length;return n=n.slice(i,i+r),t.columns&&t.columns.length>0&&"*"!==t.columns[0]&&(n=n.map(e=>f(e,t.columns))),n}async findStream(e,t,s){this.ensureTable(e);const n=this.tables.get(e),i=!!(t.where&&Object.keys(t.where).length>0),r=t.limit??1/0,a=t.offset??0,h=t.columns&&t.columns.length>0&&"*"!==t.columns[0]?e=>f(e,t.columns):null;let c=0,l=0;for(const e of n.values())if(!i||o(e,t.where))if(l=r)break;return c}async update(e,t,s){this.ensureTable(e);const i=this.schemas.get(e),r=this.tables.get(e),h=this.getPrimaryKey(i),c=p(s);if(a(t.where))throw new n("Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");for(const t of Object.keys(c))if(!i.columns[t])throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const l=[],u=new Map;for(const[s,a]of r){if(t.where&&Object.keys(t.where).length>0&&!o(a,t.where))continue;const f={...a,...c};this.validateRow(i,f),this.checkUpdateUniqueness(i,e,s,f,u);const d=String(f[h]);if(d!==s&&r.has(d))throw new n(`Duplicate primary key "${d}" in table "${e}" (cannot update key to existing value)`,"DUPLICATE_KEY");l.push({pk:s,row:a,updated:f,newPk:d})}for(const t of l)t.newPk!==t.pk&&this.checkUpdateRestrict(e,t.pk);let f=0;for(const{pk:t,row:s,updated:n,newPk:i}of l)this.removeIndexEntries(e,s,t),i!==t&&await this.applyUpdateCascade(e,t,i),r.delete(t),r.set(i,n),this.updateIndexes(e,n,i),f++;return f}checkInsertUniqueness(e,t,s,i){const r=this.indexes.get(t);for(const[t,a]of Object.entries(e.columns)){if(!a.unique)continue;const o=s[t];if(null==o)continue;let h=i.get(t);if(h||(h=new Set,i.set(t,h)),h.has(o))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION");if(h.add(o),!r)continue;const c=r.get(t);if(c&&c.has(o))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION")}}checkUpdateUniqueness(e,t,s,i,r){const a=this.indexes.get(t);for(const[t,o]of Object.entries(e.columns)){if(!o.unique)continue;const h=i[t];if(null==h)continue;let c=r.get(t);if(c||(c=new Set,r.set(t,c)),c.has(h))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION");if(c.add(h),!a)continue;const l=a.get(t);if(l&&l.has(h)){const i=l.get(h);if(1!==i.size||!i.has(s))throw new n(`Unique constraint violation on column "${t}" in table "${e.name}"`,"UNIQUE_VIOLATION")}}}checkUpdateRestrict(e,t){for(const[s,i]of this.schemas)if(s!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i!==e)continue;const o=this.tables.get(s);if(!o)continue;let h=!1;for(const[,i]of o)if(String(i[r])===t&&(h=!0,"RESTRICT"===a.onUpdate))throw new n(`Cannot update "${e}" key "${t}": foreign key "${r}" in "${s}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if(h&&"SET NULL"===a.onUpdate&&a.required)throw new n(`Cannot update "${e}" key "${t}": foreign key "${r}" in "${s}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION")}}async applyUpdateCascade(e,t,s){for(const[n,i]of this.schemas)if(n!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i!==e)continue;const o=this.tables.get(n);if(o&&("CASCADE"===a.onUpdate||"SET NULL"===a.onUpdate))for(const[e,i]of o)String(i[r])===t&&(this.removeIndexEntries(n,i,e),i[r]="CASCADE"===a.onUpdate?s:null,this.updateIndexes(n,i,e))}}async delete(e,t){if(this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const s=this.tables.get(e),i=[];for(const[e,n]of s)t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||i.push({pk:e,row:n});const r=new Set;for(const{pk:t}of i)this.checkCascadeRestrict(e,t,r);let h=0;for(const{pk:t,row:s}of i)this.removeIndexEntries(e,s,t),h+=await this.cascadeDelete(e,t,s);for(const{pk:e}of i)s.delete(e);return i.length+h}checkCascadeRestrict(e,t,s){const i=`${e}:${t}`;if(!s.has(i)){s.add(i);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onDelete)continue;const[r]=o.references.split(".");if(r!==e)continue;const h=this.tables.get(i);if(!h)continue;const c=[];for(const[e,s]of h)String(s[a])===t&&c.push(e);if("RESTRICT"===o.onDelete&&c.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("SET NULL"===o.onDelete&&o.required&&c.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===o.onDelete)for(const e of c)this.checkCascadeRestrict(i,e,s)}}}async count(e,t){this.ensureTable(e);const s=this.tables.get(e);if(!t?.where||0===Object.keys(t.where).length)return s.size;let n=0;for(const e of s.values())o(e,t.where)&&n++;return n}async clear(e){this.ensureTable(e),this.tables.get(e).clear();const t=this.indexes.get(e);if(t)for(const e of t.values())e.clear()}async createIndex(e,t,s){if(this.snapshot)throw new n("CREATE INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.index||i.unique)return;const r=this.indexes.get(e);r.has(t)||r.set(t,new Map);const a=r.get(t),o=this.tables.get(e);try{for(const[i,r]of o){const o=r[t];if(null!=o){if(s&&a.has(o))throw new n(`Unique index on column "${t}" in table "${e}" cannot be created: duplicate value "${String(o)}"`,"UNIQUE_VIOLATION");a.has(o)||a.set(o,new Set),a.get(o).add(i)}}}catch(e){throw r.delete(t),e}i.index=!0,s&&(i.unique=!0,this.uniqueIndexCols.add(`${e}:${t}`))}async dropIndex(e,t,s){if(this.snapshot)throw new n("DROP INDEX is not supported inside a transaction (MemoryEngine DDL is not transactional)","NOT_SUPPORTED");this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(!i.index&&!i.unique)throw new n(`Index on column "${t}" does not exist in table "${e}"`,"INDEX_NOT_FOUND");const r=`${e}:${t}`;if(i.unique&&!this.uniqueIndexCols.has(r))throw new n(`Cannot drop index on column "${t}" in table "${e}": UNIQUE constraint defined at table creation must be removed by recreating the table`,"NOT_SUPPORTED");i.index=!1,i.unique=!1,this.uniqueIndexCols.delete(r);const a=this.indexes.get(e);a&&a.delete(t)}async beginTransaction(){if(this.snapshot)throw new n("Transaction already in progress","TX_ACTIVE");this.snapshot={tables:this.deepCloneMapMap(this.tables),schemas:new Map(this.schemas),indexes:this.deepCloneIndexes(this.indexes)}}async commitTransaction(){if(!this.snapshot)throw new n("No active transaction","TX_NONE");this.snapshot=null}async rollbackTransaction(){if(!this.snapshot)throw new n("No active transaction","TX_NONE");this.tables=this.snapshot.tables,this.schemas=this.snapshot.schemas,this.indexes=this.snapshot.indexes,this.snapshot=null}deepCloneMapMap(e){const t=new Map;for(const[s,n]of e){const e=new Map;for(const[t,s]of n)e.set(t,{...s});t.set(s,e)}return t}deepCloneIndexes(e){const t=new Map;for(const[s,n]of e){const e=new Map;for(const[t,s]of n){const n=new Map;for(const[e,t]of s)n.set(e,new Set(t));e.set(t,n)}t.set(s,e)}return t}ensureTable(e){if(!this.tables.has(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND")}getPrimaryKey(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}validateRow(e,t){const s={};for(const[i,r]of Object.entries(e.columns)){let a=t[i];if(void 0===a&&void 0!==r.default&&(a=r.default),r.required&&null==a)throw new n(`Column "${i}" is required in table "${e.name}"`,"VALIDATION_ERROR");if(r.primaryKey&&null==a)throw new n(`Primary key column "${i}" in table "${e.name}" cannot be null or undefined`,"VALIDATION_ERROR");null!=a&&this.checkType(i,r.type,a),void 0!==a&&(s[i]=a)}return s}checkType(e,t,s){const i=typeof s;switch(t){case"string":if("string"!==i)throw new n(`Column "${e}" expects string, got ${i}`,"TYPE_ERROR");break;case"number":if("number"!==i)throw new n(`Column "${e}" expects number, got ${i}`,"TYPE_ERROR");break;case"boolean":if("boolean"!==i)throw new n(`Column "${e}" expects boolean, got ${i}`,"TYPE_ERROR");break;case"date":if("string"!==i||isNaN(Date.parse(s)))throw new n(`Column "${e}" expects valid date`,"TYPE_ERROR");break;case"json":if("object"!==i)throw new n(`Column "${e}" expects object/array, got ${i}`,"TYPE_ERROR")}}tryIndexLookup(e,t,s){const n=this.indexes.get(e);if(!n||!s.where)return Array.from(t.values());const i=[],r=e=>{for(const[t,s]of Object.entries(e))if("$and"!==t)"$or"!==t&&"$not"!==t&&i.push([t,s]);else for(const e of s)r(e)};r(s.where);for(const[e,s]of i){let i;if("object"!=typeof s||null===s)i=s;else{if(!("$eq"in s)||1!==Object.keys(s).length)continue;i=s.$eq}if(null==i)continue;const r=n.get(e);if(r){const e=r.get(i);if(e){const s=[];for(const n of e){const e=t.get(n);e&&s.push(e)}return s}return[]}}return Array.from(t.values())}updateIndexes(e,t,s){const n=this.indexes.get(e);if(n)for(const[e,i]of n){const n=t[e];null!=n&&(i.has(n)||i.set(n,new Set),i.get(n).add(s))}}removeIndexEntries(e,t,s){const n=this.indexes.get(e);if(n)for(const[e,i]of n){const n=t[e];if(null!=n){const e=i.get(n);e&&(e.delete(s),0===e.size&&i.delete(n))}}}async cascadeDelete(e,t,s,i=new Set){const r=`${e}:${t}`;if(i.has(r))return 0;i.add(r);let a=0;for(const[s,r]of this.schemas)if(s!==e)for(const[o,h]of Object.entries(r.columns)){if(!h.references||!h.onDelete)continue;const[r]=h.references.split(".");if(r!==e)continue;const c=this.tables.get(s);if(!c)continue;const l=[];for(const[e,s]of c)String(s[o])===t&&l.push(e);if("RESTRICT"===h.onDelete&&l.length>0)throw new n(`Cannot delete from "${e}": foreign key "${o}" in "${s}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===h.onDelete)for(const e of l){const t=c.get(e);t&&(this.removeIndexEntries(s,t,e),a+=await this.cascadeDelete(s,e,t,i)),c.delete(e),a++}else if("SET NULL"===h.onDelete)for(const e of l){const t=c.get(e);t&&(this.removeIndexEntries(s,t,e),t[o]=null,this.updateIndexes(s,t,e))}}return a}}const g=[".crswap",".tmp"];class w{constructor(){this.root=null,this.dbDir=null,this.dbName="",this.writeQueue=Promise.resolve()}async open(e){this.dbName=e,this.root=await navigator.storage.getDirectory(),this.dbDir=await this.root.getDirectoryHandle(e,{create:!0}),await this.cleanupStaleFiles()}async close(){try{await this.writeQueue}catch{}this.dbDir=null,this.root=null}isOpen(){return null!==this.dbDir}async cleanupStaleFiles(){if(this.dbDir)try{const e=this.dbDir,t=[];for await(const[s]of e.entries())g.some(e=>s.endsWith(e))&&t.push(s);for(const e of t)try{await this.dbDir.removeEntry(e)}catch{}}catch{}}async read(e){if(!this.dbDir)return null;try{const t=await this.dbDir.getFileHandle(e),s=await t.getFile();return await s.arrayBuffer()}catch{return null}}async write(e,t){if(!this.dbDir)return;const s=this.writeQueue.then(async()=>{const s=await this.dbDir.getFileHandle(e,{create:!0}),n=await s.createWritable();await n.write(t),await n.close()});return this.writeQueue=s.then(()=>{},()=>{}),s}async append(e,t){if(!this.dbDir)return;const s=this.writeQueue.then(async()=>{const s=await this.dbDir.getFileHandle(e,{create:!0}),n=await s.getFile(),i=await s.createWritable({keepExistingData:!0});await i.write({type:"write",position:n.size,data:t}),await i.close()});return this.writeQueue=s.then(()=>{},()=>{}),s}async writeMany(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{for(const[t,s]of Object.entries(e)){const e=await this.dbDir.getFileHandle(t,{create:!0}),n=await e.createWritable();await n.write(s),await n.close()}});return this.writeQueue=t.then(()=>{},()=>{}),t}async delete(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{try{await this.dbDir.removeEntry(e)}catch{}});return this.writeQueue=t.then(()=>{},()=>{}),t}async deleteMany(e){if(!this.dbDir)return;const t=this.writeQueue.then(async()=>{for(const t of e)try{await this.dbDir.removeEntry(t)}catch{}});return this.writeQueue=t.then(()=>{},()=>{}),t}async listKeys(){if(!this.dbDir)return[];const e=[],t=this.dbDir;for await(const[s]of t.entries())e.push(s);return e}async exists(e){if(!this.dbDir)return!1;try{return await this.dbDir.getFileHandle(e),!0}catch{return!1}}async clear(){if(!this.dbDir)return;const e=this.dbDir;for await(const[t]of e.entries())try{await this.dbDir.removeEntry(t)}catch{}}}const b=new Map;class T{constructor(){this.dbName="",this.chunks=new Map,this.materialized=new Map}static clearRegistry(){b.clear()}async open(e){this.dbName=e,b.has(e)||b.set(e,new Map),this.chunks=b.get(e),this.materialized=new Map}async close(){this.chunks=new Map,this.materialized=new Map}isOpen(){return""!==this.dbName}async read(e){if(this.materialized.has(e))return this.materialized.get(e);const t=this.chunks.get(e);if(!t||0===t.length)return null;if(1===t.length)return this.materialized.set(e,t[0]),t[0];const s=t.reduce((e,t)=>e+t.byteLength,0),n=new Uint8Array(s);let i=0;for(const e of t)n.set(new Uint8Array(e),i),i+=e.byteLength;const r=n.buffer;return this.materialized.set(e,r),r}async write(e,t){this.chunks.set(e,[t]),this.materialized.set(e,t)}async append(e,t){const s=this.chunks.get(e);s?s.push(t):this.chunks.set(e,[t]),this.materialized.delete(e)}async writeMany(e){for(const[t,s]of Object.entries(e))this.chunks.set(t,[s]),this.materialized.set(t,s)}async delete(e){this.chunks.delete(e),this.materialized.delete(e)}async deleteMany(e){for(const t of e)this.chunks.delete(t),this.materialized.delete(t)}async listKeys(){return Array.from(this.chunks.keys())}async exists(e){return this.chunks.has(e)}async clear(){this.chunks.clear(),this.materialized.clear()}}const E=(()=>{const e=new Uint32Array(256);for(let t=0;t<256;t++){let s=t;for(let e=0;e<8;e++)s=1&s?3988292384^s>>>1:s>>>1;e[t]=s>>>0}return e})(),k=4294967295;function x(e,t=0){const s=function(e,t){let s=e>>>0;for(let e=0;e>>8)>>>0;return s>>>0}(t^k,e);return function(e){return(e^k)>>>0}(s)}var S;!function(e){e[e.PUT=1]="PUT",e[e.DELETE=2]="DELETE",e[e.APPEND=3]="APPEND"}(S||(S={}));const I=1263948622;function A(e,t){const s=new TextEncoder,n=Array.from(t.keys()),i=[];let r=12;for(const e of n){const n=s.encode(e),a=new Uint8Array(t.get(e));i.push({key:n,value:a}),r+=4+n.byteLength+4+a.byteLength}r+=4;const a=new Uint8Array(r),o=new DataView(a.buffer);let h=0;o.setUint32(h,I,!1),h+=4,o.setUint32(h,e,!1),h+=4,o.setUint32(h,i.length,!1),h+=4;for(const e of i)o.setUint32(h,e.key.byteLength,!1),h+=4,a.set(e.key,h),h+=e.key.byteLength,o.setUint32(h,e.value.byteLength,!1),h+=4,a.set(e.value,h),h+=e.value.byteLength;const c=x(a.subarray(0,r-4));return o.setUint32(r-4,c,!1),a}function R(e){if(e.byteLength<16)return null;const t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(t.getUint32(0,!1)!==I)return null;if(t.getUint32(e.byteLength-4,!1)!==x(e.subarray(0,e.byteLength-4)))return null;const s=new TextDecoder,n=new Map;let i=4;const r=t.getUint32(i,!1);i+=4;const a=t.getUint32(i,!1);i+=4;for(let r=0;re.byteLength-4)return null;const r=t.getUint32(i,!1);if(i+=4,i+r+4>e.byteLength-4)return null;const a=s.decode(e.subarray(i,i+r));i+=r;const o=t.getUint32(i,!1);if(i+=4,i+o>e.byteLength-4)return null;const h=e.slice(i,i+o).buffer;i+=o,n.set(a,h)}return{seq:r,entries:n}}const C="__kv_log",O="__kv_snapshot",N="__kv_meta";class L{constructor(e,t=16777216){this.dbName="",this.opened=!1,this.index=new Map,this.seq=0,this.logBytes=0,this.opQueue=Promise.resolve(),this.lastBackgroundError=null,this.medium=e??function(){const e=globalThis.navigator;return void 0!==e&&e.storage&&"function"==typeof e.storage.getDirectory?new w:new T}(),this.checkpointThreshold=t}isOpen(){return this.opened}async open(e){if(this.opened)return;this.dbName=e,this.lastBackgroundError=null,await this.medium.open(e),this.index=new Map,this.seq=0,this.logBytes=0;let t=0;const s=await this.medium.read(O);if(s){const e=R(new Uint8Array(s));e?(this.index=new Map(e.entries),this.seq=e.seq,t=e.seq):(this.index=new Map,this.seq=0,t=0)}const n=await this.medium.read(C);if(n&&n.byteLength>0){const e=new Uint8Array(n),s=t,i=[];(function(e,t,s){let n=0,i=0;const r=new DataView(e.buffer,e.byteOffset,e.byteLength),a=new TextDecoder;for(;n+4<=e.byteLength;){const o=r.getUint32(n,!1);if(o<12||n+4+o>e.byteLength){if(s&&!s(n))break;break}const h=n,c=n+4+o,l=e.subarray(h,c),u=new DataView(e.buffer,e.byteOffset+h,o+4);if(u.getUint32(o,!1)!==x(l.subarray(0,o))){if(s&&!s(h))break;break}let f=4;const d=u.getUint32(f,!1);f+=4;const p=u.getUint32(f,!1);f+=4;const y=[];let m=!0;for(let e=0;eo+4){m=!1;break}const e=u.getUint8(f);f+=1;const t=u.getUint32(f,!1);if(f+=4,f+t+4>o+4){m=!1;break}const s=a.decode(l.subarray(f,f+t));f+=t;const n=u.getUint32(f,!1);if(f+=4,f+n>o+4){m=!1;break}const i=l.slice(f,f+n).buffer;f+=n,y.push({op:e,key:s,value:i})}if(!m){if(s&&!s(h))break;break}t({seq:d,entries:y,raw:l}),i++,n=c}return i}(e,e=>{e.seq<=s||(this.applyRecord(e.entries),this.seq=e.seq)},e=>(i.push(e),!0))>0||i.length>0)&&(this.logBytes=e.byteLength),i.length>0&&await this.truncateLog()}this.opened=!0}async reload(){if(this.opened){try{await this.opQueue}catch{}this.index=new Map,this.seq=0,this.logBytes=0,this.opened=!1,await this.open(this.dbName)}}async close(){if(this.opened){try{await this.opQueue}catch{}await this.medium.close(),this.index.clear(),this.seq=0,this.logBytes=0,this.lastBackgroundError=null,this.opened=!1}}async get(e){return this.index.get(e)??null}async getAll(){return Array.from(this.index.entries())}async listKeys(){return Array.from(this.index.keys())}async exists(e){return this.index.has(e)}size(){return this.index.size}async put(e,t){await this.enqueue(async()=>{await this.appendRecord({[e]:t},[])})}async putMany(e){0!==Object.keys(e).length&&await this.enqueue(async()=>{await this.appendRecord(e,[])})}async delete(e){await this.enqueue(async()=>{await this.appendRecord({},[e])})}async deleteMany(e){0!==e.length&&await this.enqueue(async()=>{await this.appendRecord({},e)})}async writeBatch(e,t){0===Object.keys(e).length&&0===t.length||await this.enqueue(async()=>{await this.appendRecord(e,t)})}async appendValue(e,t){0!==t.byteLength&&await this.enqueue(async()=>{await this.appendRecord({},[],{[e]:t})})}async checkpoint(){await this.enqueue(async()=>{if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=A(this.seq,this.index);await this.medium.write(O,e.buffer);const t={seq:this.seq};await this.medium.write(N,(new TextEncoder).encode(JSON.stringify(t)).buffer),await this.truncateLog()})}async clear(){await this.enqueue(async()=>{await this.medium.clear(),this.index.clear(),this.seq=0,this.logBytes=0,await this.medium.write(N,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(O);t&&!R(new Uint8Array(t))&&(await this.medium.delete(O),e++);const s=await this.medium.read(C);if(s&&s.byteLength>0){const t=new Uint8Array(s),n=this.findValidLogLength(t);if(n{},()=>{}),t}async appendRecord(e,t,s={}){this.seq++;const i=function(e,t,s=[],n={}){const i=new TextEncoder,r=[];for(const[e,s]of Object.entries(t))r.push({op:S.PUT,key:e,value:s});for(const e of s)r.push({op:S.DELETE,key:e,value:new ArrayBuffer(0)});for(const[e,t]of Object.entries(n))r.push({op:S.APPEND,key:e,value:t});const a=[];let o=12;for(const e of r){const t=i.encode(e.key),s=new Uint8Array(e.value);a.push({op:e.op,key:t,value:s}),o+=5+t.byteLength+4+s.byteLength}o+=4;const h=new Uint8Array(o),c=new DataView(h.buffer);let l=0;c.setUint32(l,o-4,!1),l+=4,c.setUint32(l,e,!1),l+=4,c.setUint32(l,a.length,!1),l+=4;for(const e of a)c.setUint8(l,e.op),l+=1,c.setUint32(l,e.key.byteLength,!1),l+=4,h.set(e.key,l),l+=e.key.byteLength,c.setUint32(l,e.value.byteLength,!1),l+=4,h.set(e.value,l),l+=e.value.byteLength;const u=x(h.subarray(0,o-4));return c.setUint32(o-4,u,!1),h}(this.seq,e,t,s);try{const e=i.buffer.slice(i.byteOffset,i.byteOffset+i.byteLength);if("function"==typeof this.medium.append)await this.medium.append(C,e);else{const t=await this.medium.read(C);if(t){const s=new Uint8Array(t.byteLength+e.byteLength);s.set(new Uint8Array(t),0),s.set(new Uint8Array(e),t.byteLength),await this.medium.write(C,s.buffer)}else await this.medium.write(C,e)}}catch(e){throw this.seq--,this.lastBackgroundError=e,new n("KVStore log append failed","KV_LOG_ERROR",e)}for(const[t,s]of Object.entries(e))this.index.set(t,s);for(const e of t)this.index.delete(e);for(const[e,t]of Object.entries(s)){const s=this.index.get(e);if(s){const n=new Uint8Array(s.byteLength+t.byteLength);n.set(new Uint8Array(s),0),n.set(new Uint8Array(t),s.byteLength),this.index.set(e,n.buffer)}else this.index.set(e,t)}if(this.logBytes+=i.byteLength,this.checkpointThreshold>0&&this.logBytes>=this.checkpointThreshold){await this.medium.write(O,A(this.seq,this.index).buffer);const e={seq:this.seq};await this.medium.write(N,(new TextEncoder).encode(JSON.stringify(e)).buffer),await this.truncateLog()}}async truncateLog(){try{await this.medium.write(C,new ArrayBuffer(0))}catch{}this.logBytes=0}applyRecord(e){for(const t of e)if(t.op===S.PUT)this.index.set(t.key,t.value);else if(t.op===S.APPEND){const e=this.index.get(t.key);if(e){const s=new Uint8Array(e.byteLength+t.value.byteLength);s.set(new Uint8Array(e),0),s.set(new Uint8Array(t.value),e.byteLength),this.index.set(t.key,s.buffer)}else this.index.set(t.key,t.value)}else this.index.delete(t.key)}findValidLogLength(e){let t=0;const s=new DataView(e.buffer,e.byteOffset,e.byteLength);for(;t+4<=e.byteLength;){const n=s.getUint32(t,!1);if(n<12||t+4+n>e.byteLength)break;const i=e.subarray(t,t+4+n),r=s.getUint32(t+n,!1);if(x(i.subarray(0,n))!==r)break;t+=4+n}return t}}const v="__schema",$="t:",D=e=>(new TextEncoder).encode(e).buffer,U=e=>(new TextDecoder).decode(e);class _{constructor(e,t){this.name="kv",this.memory=new m,this.dbName="",this.version=1,this.opened=!1,this.txActive=!1,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set,this.kv=new L(e,t)}rowKey(e,t){return`${$}${e}:${t}`}rowPrefix(e){return`${$}${e}:`}async open(e,t){if(this.opened)return;this.dbName=e,this.version=t,await this.kv.open(e),await this.memory.open(e,t);const s=await this.kv.get(v);if(s)try{const e=JSON.parse(U(s));for(const t of Object.values(e))await this.memory.createTable(t)}catch{throw new n("Corrupted schema in KVStore","KV_SCHEMA_ERROR")}const i=await this.kv.getAll();for(const[e,t]of i){if(!e.startsWith($))continue;const s=e.indexOf(":",2);if(s<0)continue;const n=e.slice(2,s);if(await this.memory.hasTable(n))try{const e=JSON.parse(U(t));await this.memory.insert(n,[e])}catch{}}const r=await this.memory.getTableNames();for(const e of r){const t=await this.memory.getTableSchema(e);if(t)for(const[s,n]of Object.entries(t.columns))(n.index||n.unique)&&await this.memory.createIndex(e,s,n.unique)}this.opened=!0}async close(){if(this.opened){if(this.txActive)try{await this.rollbackTransaction()}catch{}try{await this.kv.checkpoint()}catch{}await this.kv.close(),await this.memory.close(),this.opened=!1}}isOpen(){return this.opened}async reload(){this.opened&&(await this.kv.reload(),await this.memory.close(),await this.memory.open(this.dbName,this.version),this.opened=!1,await this.open(this.dbName,this.version))}async repair(){this.ensureOpen(),await this.kv.repair(),await this.memory.close(),await this.memory.open(this.dbName,this.version),this.opened=!1,await this.open(this.dbName,this.version)}async clearAll(){this.ensureOpen(),await this.kv.clear(),await this.memory.clearAll()}async getMeta(e){const t=await this.kv.get(`__meta:${e}`);return t?U(t):null}async setMeta(e,t){await this.kv.put(`__meta:${e}`,D(t))}async createTable(e){if(this.ensureOpen(),await this.memory.createTable(e),this.txActive)return this.txDirtyTables.add(e.name),void(this.txSchemaChanged=!0);await this.persistSchema()}async dropTable(e){if(this.ensureOpen(),await this.memory.dropTable(e),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema();const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}async hasTable(e){return this.ensureOpen(),this.memory.hasTable(e)}async getTableNames(){return this.ensureOpen(),this.memory.getTableNames()}async getTableSchema(e){return this.ensureOpen(),this.memory.getTableSchema(e)}async alterTable(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("ALTER TABLE is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.alterTable(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);if(await this.persistSchema(),"DROP"===t){const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}}async insert(e,t){this.ensureOpen();const s=await this.memory.insert(e,t);if(this.txActive){if(this.txDirtyTables.add(e),this.txClearedTables.has(e))return this.txClearedTables.delete(e),this.txFullTables.add(e),s;let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of s)t.set(e,"put");return s}if(!await this.memory.getTableSchema(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const i={};for(const t of s){const s=this.memory.getRow(e,t);s&&(i[this.rowKey(e,t)]=D(JSON.stringify(s)))}return await this.kv.putMany(i),s}async find(e,t){return this.ensureOpen(),this.memory.find(e,t)}async findStream(e,t,s){return this.ensureOpen(),this.memory.findStream(e,t,s)}async update(e,t,s){this.ensureOpen();const i=await this.memory.getTableSchema(e);if(!i)throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const r=this.getPK(i),a=p(s),o=r in a,h=o?[]:await this.collectMatchingPks(e,t),c=await this.memory.update(e,t,a);if(this.txActive){if(this.txDirtyTables.add(e),o)for(const t of await this.affectedTables(e))this.txFullTables.add(t),this.txDirtyTables.add(t);else{let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of h)t.set(e,"put");for(const t of await this.affectedTables(e))t!==e&&(this.txFullTables.add(t),this.txDirtyTables.add(t))}return c}const l={},u=[];if(o)for(const t of await this.affectedTables(e)){const e=await this.collectTableDiff(t);Object.assign(l,e.puts),u.push(...e.deletes)}else{const t=new Set(h),s=await this.memory.find(e,{table:e});for(const n of s){const s=String(n[r]);t.has(s)&&(l[this.rowKey(e,s)]=D(JSON.stringify(n)),t.delete(s))}for(const s of t)u.push(this.rowKey(e,s));for(const t of await this.affectedTables(e)){if(t===e)continue;const s=await this.collectTableDiff(t);Object.assign(l,s.puts),u.push(...s.deletes)}}return await this.kv.writeBatch(l,u),c}async delete(e,t){this.ensureOpen();const s=await this.collectMatchingPks(e,t),n=await this.memory.delete(e,t);if(this.txActive){this.txDirtyTables.add(e);let t=this.txChanges.get(e);t||(t=new Map,this.txChanges.set(e,t));for(const e of s)t.set(e,"delete");for(const t of await this.affectedTables(e))t!==e&&(this.txFullTables.add(t),this.txDirtyTables.add(t));return n}const i={},r=s.map(t=>this.rowKey(e,t));for(const t of await this.affectedTables(e)){if(t===e)continue;const s=await this.collectTableDiff(t);Object.assign(i,s.puts),r.push(...s.deletes)}return await this.kv.writeBatch(i,r),n}async count(e,t){return this.ensureOpen(),this.memory.count(e,t)}async clear(e){if(this.ensureOpen(),await this.memory.clear(e),this.txActive)return this.txDirtyTables.add(e),this.txClearedTables.add(e),void this.txChanges.delete(e);const t=await this.collectTableDiff(e);await this.kv.writeBatch(t.puts,t.deletes)}async createIndex(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("CREATE INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.createIndex(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema()}async dropIndex(e,t,s){if(this.ensureOpen(),this.txActive)throw new n("DROP INDEX is not supported inside a transaction (KVStoreEngine DDL is not transactional)","NOT_SUPPORTED");if(await this.memory.dropIndex(e,t,s),this.txActive)return this.txDirtyTables.add(e),void(this.txSchemaChanged=!0);await this.persistSchema()}async beginTransaction(){this.ensureOpen(),await this.memory.beginTransaction(),this.txActive=!0,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}async commitTransaction(){if(this.ensureOpen(),!this.txActive)throw new n("No active transaction","TX_NONE");const e={},t=[];for(const s of this.txDirtyTables){if(!await this.memory.hasTable(s)){const e=await this.kv.getAll(),n=this.rowPrefix(s);for(const[s]of e)s.startsWith(n)&&t.push(s);continue}if(this.txClearedTables.has(s)){const e=await this.kv.getAll(),n=this.rowPrefix(s);for(const[s]of e)s.startsWith(n)&&t.push(s);continue}if(this.txFullTables.has(s)){const n=await this.collectTableDiff(s);Object.assign(e,n.puts),t.push(...n.deletes);continue}const n=this.txChanges.get(s);if(n&&n.size>0){const i=await this.memory.getTableSchema(s);if(!i)continue;const r=this.getPK(i),a=new Map(n),o=await this.memory.find(s,{table:s});for(const n of o){const i=String(n[r]),o=a.get(i);void 0!==o&&("put"===o?e[this.rowKey(s,i)]=D(JSON.stringify(n)):t.push(this.rowKey(s,i)),a.delete(i))}for(const[e,n]of a)"delete"===n&&t.push(this.rowKey(s,e))}}await this.kv.writeBatch(e,t),this.txSchemaChanged&&await this.persistSchema(),await this.memory.commitTransaction(),this.txActive=!1,this.txDirtyTables=new Set,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}async rollbackTransaction(){if(this.ensureOpen(),!this.txActive)throw new n("No active transaction","TX_NONE");await this.memory.rollbackTransaction(),this.txActive=!1,this.txDirtyTables=new Set,this.txSchemaChanged=!1,this.txChanges=new Map,this.txFullTables=new Set,this.txClearedTables=new Set}ensureOpen(){if(!this.opened)throw new n("Database not opened","DB_NOT_OPEN")}getPK(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}async collectMatchingPks(e,t){const s=await this.memory.getTableSchema(e);if(!s)throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND");const i=this.getPK(s);return(await this.memory.find(e,t)).map(e=>String(e[i]))}async affectedTables(e){const t=new Set([e]);let s=!0;for(;s;){s=!1;for(const e of await this.memory.getTableNames()){if(t.has(e))continue;const n=await this.memory.getTableSchema(e);if(n)for(const i of Object.values(n.columns))if(i.references){const n=i.references.split(".")[0];if(t.has(n)){t.add(e),s=!0;break}}}}return t}async persistSchema(){const e={};for(const t of await this.memory.getTableNames()){const s=await this.memory.getTableSchema(t);s&&(e[t]=s)}await this.kv.put(v,D(JSON.stringify(e)))}async collectTableDiff(e){const t=this.rowPrefix(e),s={},n=[],i=await this.memory.getTableSchema(e);if(i){const r=this.getPK(i),a=await this.memory.find(e,{table:e}),o=new Set;for(const t of a){const n=this.rowKey(e,String(t[r]));o.add(n),s[n]=D(JSON.stringify(t))}const h=await this.kv.getAll();for(const[e]of h)e.startsWith(t)&&!o.has(e)&&n.push(e)}else{const e=await this.kv.getAll();for(const[s]of e)s.startsWith(t)&&n.push(s)}return{puts:s,deletes:n}}}const M=4096;var P;!function(e){e[e.DATA=1]="DATA",e[e.INDEX=2]="INDEX",e[e.OVERFLOW=3]="OVERFLOW",e[e.META=4]="META"}(P||(P={}));const B=4194304;var K,q;!function(e){e[e.INSERT=1]="INSERT",e[e.UPDATE=2]="UPDATE",e[e.DELETE=3]="DELETE",e[e.BEGIN=4]="BEGIN",e[e.COMMIT=5]="COMMIT",e[e.ROLLBACK=6]="ROLLBACK",e[e.CREATE_TABLE=7]="CREATE_TABLE",e[e.DROP_TABLE=8]="DROP_TABLE"}(K||(K={})),function(e){e[e.ACTIVE=1]="ACTIVE",e[e.COMMITTED=2]="COMMITTED",e[e.ABORTED=3]="ABORTED"}(q||(q={}));const z={pageSize:M,bufferPoolPages:256,memtableSizeThreshold:B,levelSizeMultiplier:10,bloomFilterBitsPerKey:10,walEnabled:!0,walSyncMode:"full",checkpointInterval:1e3,compression:!1,storageBackend:"opfs",walSizeThreshold:16777216,maxMemoryMB:64,encryption:void 0,pageStorage:void 0};var F;!function(e){e[e.RED=0]="RED",e[e.BLACK=1]="BLACK"}(F||(F={}));class j{constructor(e,t){this.color=F.RED,this.left=null,this.right=null,this.parent=null,this.key=e,this.value=t}}class V{constructor(){this.root=null,this._size=0}get size(){return this._size}insert(e,t){const s=new j(e,t);if(!this.root)return this.root=s,s.color=F.BLACK,void this._size++;let n=null,i=this.root;for(;i;)if(n=i,ei.key))return void(i.value=t);i=i.right}s.parent=n,et.key))return t.value;t=t.right}return null}delete(e){const t=this.findNode(e);return!!t&&(this.deleteNode(t),this._size--,!0)}inorder(e){this._inorder(this.root,e)}rangeScan(e,t,s){this._rangeScan(this.root,e,t,s)}*scanLazy(e,t){const s=[];let n=this.root;for(;n;)n.key>=e?(s.push(n),n=n.left):n=n.right;for(;s.length>0;){const i=s.pop();if(i.key>t)break;for(i.key>=e&&(yield[i.key,i.value]),n=i.right;n;)s.push(n),n=n.left}}getAllEntries(){const e=[];return this.inorder((t,s)=>e.push([t,s])),e}clear(){this.root=null,this._size=0}findNode(e){let t=this.root;for(;t;)if(et.key))return t;t=t.right}return null}deleteNode(e){if(e.left||e.right)if(e.left)if(e.right){const t=this.minimum(e.right),s=t.right,n=t.parent;n!==e&&(this.transplant(t,s),t.right=e.right,t.right.parent=t),this.transplant(e,t),t.left=e.left,t.left.parent=t;const i=t.color;if(t.color=e.color,i===F.BLACK){const i=s,r=i?i.parent:n===e?t:n;this.fixDelete(i,r)}}else this.transplant(e,e.left),e.color===F.BLACK&&this.fixDelete(e.left,e.left.parent);else this.transplant(e,e.right),e.color===F.BLACK&&this.fixDelete(e.right,e.right.parent);else this.transplant(e,null),e.color===F.BLACK&&this.fixDelete(null,e.parent)}transplant(e,t){e.parent?e===e.parent.left?e.parent.left=t:e.parent.right=t:this.root=t,t&&(t.parent=e.parent)}minimum(e){for(;e.left;)e=e.left;return e}fixInsert(e){for(;e.parent&&e.parent.color===F.RED;){const t=e.parent,s=t.parent;if(!s)break;if(t===s.left){const n=s.right;n&&n.color===F.RED?(t.color=F.BLACK,n.color=F.BLACK,s.color=F.RED,e=s):(e===t.right&&(e=t,this.rotateLeft(e)),e.parent&&(e.parent.color=F.BLACK),e.parent?.parent&&(e.parent.parent.color=F.RED),e.parent?.parent&&this.rotateRight(e.parent.parent))}else{const n=s.left;n&&n.color===F.RED?(t.color=F.BLACK,n.color=F.BLACK,s.color=F.RED,e=s):(e===t.left&&(e=t,this.rotateRight(e)),e.parent&&(e.parent.color=F.BLACK),e.parent?.parent&&(e.parent.parent.color=F.RED),e.parent?.parent&&this.rotateLeft(e.parent.parent))}}this.root&&(this.root.color=F.BLACK)}fixDelete(e,t){let s=e,n=t;for(;(!s||s.color===F.BLACK)&&s!==this.root&&n;)if(s===n.left){let e=n.right;if(!e)break;if(e.color===F.RED&&(e.color=F.BLACK,n.color=F.RED,this.rotateLeft(n),e=n.right,!e))break;const t=e.left,i=e.right;if(t&&t.color!==F.BLACK||i&&i.color!==F.BLACK){if(!(i&&i.color!==F.BLACK||(t&&(t.color=F.BLACK),e.color=F.RED,this.rotateRight(e),e=n.right,e)))break;e.color=n.color,n.color=F.BLACK,e.right&&(e.right.color=F.BLACK),this.rotateLeft(n),s=this.root}else e.color=F.RED,s=n,n=s.parent}else{let e=n.left;if(!e)break;if(e.color===F.RED&&(e.color=F.BLACK,n.color=F.RED,this.rotateRight(n),e=n.left,!e))break;const t=e.left,i=e.right;if(t&&t.color!==F.BLACK||i&&i.color!==F.BLACK){if(!(t&&t.color!==F.BLACK||(i&&(i.color=F.BLACK),e.color=F.RED,this.rotateLeft(e),e=n.left,e)))break;e.color=n.color,n.color=F.BLACK,e.left&&(e.left.color=F.BLACK),this.rotateRight(n),s=this.root}else e.color=F.RED,s=n,n=s.parent}s&&(s.color=F.BLACK)}rotateLeft(e){const t=e.right;t&&(e.right=t.left,t.left&&(t.left.parent=e),t.parent=e.parent,e.parent?e===e.parent.left?e.parent.left=t:e.parent.right=t:this.root=t,t.left=e,e.parent=t)}rotateRight(e){const t=e.left;t&&(e.left=t.right,t.right&&(t.right.parent=e),t.parent=e.parent,e.parent?e===e.parent.right?e.parent.right=t:e.parent.left=t:this.root=t,t.right=e,e.parent=t)}_inorder(e,t){e&&(this._inorder(e.left,t),t(e.key,e.value),this._inorder(e.right,t))}_rangeScan(e,t,s,n){e&&(e.key>t&&this._rangeScan(e.left,t,s,n),e.key>=t&&e.key<=s&&n(e.key,e.value),e.key=this.maxSize}getAllEntries(){return this.tree.getAllEntries()}rangeScan(e,t){const s=[];return this.tree.rangeScan(e,t,(e,t)=>s.push([e,t])),s}scanLazy(e,t){return this.tree.scanLazy(e,t)}getEntryCount(){return this.tree.size}getEstimatedSize(){return this._estimatedSize}clear(){this.tree.clear(),this._estimatedSize=0}estimateEntrySize(e,t){if(!t)return 0;let s=2*e.length;for(const e of Object.entries(t)){s+=2*e[0].length;const t=e[1];s+="string"==typeof t?2*t.length:"number"==typeof t?8:"boolean"==typeof t||null==t?1:16}return s}}class H{constructor(e,t=10){this._inserted=0;const s=Math.max(64,e*t),n=Math.ceil(s/8);this.bits=new Uint8Array(n),this.numHashes=Math.max(1,Math.floor(.69*t))}static fromData(e,t){const s=new H(1);return s.bits=e,s.numHashes=t,s}insert(e){const t=this.getHashes(e);for(const e of t){const t=Math.floor(e/8),s=e%8;this.bits[t]|=1<>>0;return t}murmurSimple(e){let t=0;for(let s=0;s>>16)>>>0;return Math.abs(t)}}const G=1397969987;class X{constructor(e=4096){this.entries=[],this.blockSizeLimit=e}add(e,t){this.entries.push([e,t])}build(){const e=new TextEncoder,t=this.entries.map(([t,s])=>({key:t,keyBytes:e.encode(t),valueBytes:e.encode(JSON.stringify(s))})),s=this.splitIntoBlocks(t),n=new H(this.entries.length);let i=0;const r=[];for(const e of s)r.push(i),i+=this.computeBlockSize(e);const a=[];for(let e=0;e=this.blockSizeLimit&&s.length>1&&(t.push(s.slice(0,-1)),s=[n]);return s.length>0&&t.push(s),t}computeBlockSize(e){let t=4;for(const s of e)t+=4+s.keyBytes.length+4+s.valueBytes.length;return t}writeDataBlock(e,t,s,n){e.setUint32(t,s.length,!1),t+=4;for(const i of s){if(i.keyBytes.length>4294967295||i.valueBytes.length>4294967295)throw new Error("SSTable entry too large (exceeds u32 length field)");e.setUint32(t,i.keyBytes.length,!1),t+=4,new Uint8Array(e.buffer).set(i.keyBytes,t),t+=i.keyBytes.length,e.setUint32(t,i.valueBytes.length,!1),t+=4,new Uint8Array(e.buffer).set(i.valueBytes,t),t+=i.valueBytes.length,n.insert(i.key)}return t}estimateIndexBlockSize(e){let t=4;const s=new TextEncoder;for(const n of e)t+=4+s.encode(n.key).byteLength+8;return t}writeIndexBlock(e,t,s){e.setUint32(t,s.length,!1),t+=4;for(const n of s){const s=(new TextEncoder).encode(n.key);e.setUint32(t,s.length,!1),t+=4,new Uint8Array(e.buffer).set(s,t),t+=s.length,e.setUint32(t,n.blockOffset,!1),t+=4,e.setUint32(t,n.blockSize,!1),t+=4}return t}}class J{constructor(e,t){this.indexEntries=[],this.entryCount=0,this.bloomFilter=null,this.format=2,this.storedChecksum=0,this.data=e,this.view=new DataView(e.buffer,e.byteOffset,e.byteLength),this.meta=t,this.parseFooter()}verifyChecksum(){return 0===this.storedChecksum||!(this.data.byteLength<4)&&x(this.data.subarray(0,this.data.byteLength-4))===this.storedChecksum}lenFieldSize(){return 2===this.format?4:2}readLen(e){return 2===this.format?this.view.getUint32(e,!1):this.view.getUint16(e,!1)}get(e){if(this.bloomFilter&&!this.bloomFilter.mayContain(e))return null;const t=this.locateBlock(e);if(t<0)return null;const s=this.indexEntries[t],n=this.getBlockData(s);if(!n)return null;const i=new DataView(n.buffer,n.byteOffset,n.byteLength),r=this.lenFieldSize(),a=i.getUint32(0,!1);let o=4;for(let t=0;tn.byteLength);t++){const t=2===this.format?i.getUint32(o,!1):i.getUint16(o,!1);if(o+=r,o+t+r>n.byteLength)break;const s=(new TextDecoder).decode(n.slice(o,o+t));o+=t;const a=2===this.format?i.getUint32(o,!1):i.getUint16(o,!1);if(o+=r,o+a>n.byteLength)break;const h=n.slice(o,o+a);if(o+=a,s===e)try{return JSON.parse((new TextDecoder).decode(h))}catch{return null}}return null}rangeScan(e,t,s){for(const[n,i]of this.scanLazy(e,t))s(n,i)}*scanLazy(e,t){if(0===this.indexEntries.length)return;const s=Math.max(0,this.locateBlockGE(e)),n=Math.min(this.indexEntries.length-1,this.locateBlockLE(t)+1);if(s<0||n<0||s>n)return;const i=this.lenFieldSize();for(let r=s;r<=n&&r>=0;r++){const s=this.indexEntries[r],n=this.getBlockData(s);if(!n)continue;const a=new DataView(n.buffer,n.byteOffset,n.byteLength),o=a.getUint32(0,!1);let h=4;for(let s=0;sn.byteLength);s++){const s=2===this.format?a.getUint32(h,!1):a.getUint16(h,!1);if(h+=i,h+s+i>n.byteLength)break;const r=(new TextDecoder).decode(n.slice(h,h+s));h+=s;const o=2===this.format?a.getUint32(h,!1):a.getUint16(h,!1);if(h+=i,h+o>n.byteLength)break;const c=n.slice(h,h+o);if(h+=o,r>=e&&r<=t)try{const e=JSON.parse((new TextDecoder).decode(c));yield[r,e]}catch{}}}}scanAll(e){const t=this.lenFieldSize();for(const s of this.indexEntries){const n=this.getBlockData(s);if(!n)continue;const i=new DataView(n.buffer,n.byteOffset,n.byteLength),r=i.getUint32(0,!1);let a=4;for(let s=0;sn.byteLength);s++){const s=2===this.format?i.getUint32(a,!1):i.getUint16(a,!1);if(a+=t,a+s+t>n.byteLength)break;const r=(new TextDecoder).decode(n.slice(a,a+s));a+=s;const o=2===this.format?i.getUint32(a,!1):i.getUint16(a,!1);if(a+=t,a+o>n.byteLength)break;const h=n.slice(a,a+o);a+=o;try{e(r,JSON.parse((new TextDecoder).decode(h)))}catch{}}}}parseFooter(){if(this.data.byteLength<32)throw new Error("SSTable too small: missing footer");const e=this.data.byteLength-32,t=this.view.getUint32(e+24,!1);if(1397969986===t)this.format=1;else{if(t!==G)throw new Error(`Invalid SSTable magic: expected 1397969986 or 1397969987, got ${t}`);this.format=2}const s=this.view.getUint32(e,!1),n=this.view.getUint32(e+4,!1),i=this.view.getUint32(e+8,!1),r=this.view.getUint32(e+12,!1),a=this.view.getUint32(e+16,!1);if(this.entryCount=this.view.getUint32(e+20,!1),this.storedChecksum=this.view.getUint32(e+28,!1),!(s+4>this.data.byteLength||s+n>this.data.byteLength)&&(this.parseIndexBlock(s,n),i>0&&r>0&&i+r<=this.data.byteLength))try{const e=this.data.slice(i,i+r);this.bloomFilter=H.fromData(e,a||10)}catch{}}parseIndexBlock(e,t){const s=this.view.getUint32(e,!1);e+=4;const n=this.lenFieldSize();for(let t=0;tthis.data.byteLength);t++){const t=this.readLen(e);if((e+=n)+t+8>this.data.byteLength)break;const s=(new TextDecoder).decode(this.data.slice(e,e+t));e+=t;const i=this.view.getUint32(e,!1);e+=4;const r=this.view.getUint32(e,!1);e+=4,0===r||i+r>this.data.byteLength||this.indexEntries.push({key:s,blockOffset:i,blockSize:r})}}getBlockData(e){return e.blockSize<=0||e.blockOffset<0||e.blockOffset+e.blockSize>this.data.byteLength?null:new Uint8Array(this.data.buffer,this.data.byteOffset+e.blockOffset,e.blockSize)}locateBlock(e){let t=0,s=this.indexEntries.length-1;for(;t<=s;){const n=Math.floor((t+s)/2);if(e<=this.indexEntries[n].key){if(e>(0===n?"":this.indexEntries[n-1].key))return n;s=n-1}else t=n+1}return-1}locateBlockGE(e){let t=0,s=this.indexEntries.length;for(;t>1;this.indexEntries[n].key>1;this.indexEntries[n].key<=e?t=n+1:s=n}return t>0?t-1:0}}class Q{constructor(e){this.index=0,this.entries=e}next(){return this.index>=this.entries.length?null:this.entries[this.index++]}reset(){this.index=0}}class Y{constructor(e){this.iter=e}next(){const e=this.iter.next();return e.done?null:e.value}reset(){}}class Z{constructor(){this.heap=[]}push(e){this.heap.push(e),this.bubbleUp(this.heap.length-1)}pop(){if(0===this.heap.length)return null;if(1===this.heap.length)return this.heap.pop();const e=this.heap[0];return this.heap[0]=this.heap.pop(),this.bubbleDown(0),e}peek(){return this.heap.length>0?this.heap[0]:null}get size(){return this.heap.length}bubbleUp(e){for(;e>0;){const t=Math.floor((e-1)/2);if(this.heap[e].key>=this.heap[t].key)break;[this.heap[e],this.heap[t]]=[this.heap[t],this.heap[e]],e=t}}bubbleDown(e){const t=this.heap.length;for(;;){let s=e;const n=2*e+1,i=2*e+2;if(n=0&&e.level<7&&this.levels[e.level].push(e);for(let e=0;e<7;e++)this.levels[e].sort((e,t)=>t.id-e.id);this.initialized=!0}put(e,t){this.levels[0].length>=8&&this.enqueueCompact(0),this.memtable.put(e,t),this.operationCount++,this.memtable.shouldFlush()&&this.freezeMemtable()}delete(e){this.levels[0].length>=8&&this.enqueueCompact(0),this.memtable.put(e,{__tombstone:!0}),this.operationCount++,this.memtable.shouldFlush()&&this.freezeMemtable()}getEstimatedMemory(){let e=this.memtable.getEstimatedSize();for(const t of this.frozenMemtables)e+=t.getEstimatedSize();return e+=this.cacheSize,e}freezeMemtable(){if(this.immutableMemtable){const e=this.immutableMemtable;this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e))}this.immutableMemtable=this.memtable,this.frozenMemtables.push(this.immutableMemtable),this.memtable=new W(this.memtableSizeThreshold)}enqueueOnChain(e){return this.flushChain.then(e).catch(e=>{this.lastBackgroundError=e})}async drainChain(){for(;;){const e=this.flushChain;if(await e,this.flushChain===e)return}}async flushImmutableAsync(e){const t=e.getAllEntries();if(0===t.length)return void(this.immutableMemtable===e&&(this.immutableMemtable=null));const s=await this.sstableStore.allocateId(),n=new X(this.blockSize);for(const[e,s]of t)n.add(e,s);const{sstableData:i,indexEntries:r}=n.build(),a={id:s,level:0,minKey:t[0][0],maxKey:t[t.length-1][0],blockCount:r.length,totalSize:i.byteLength,bloomData:null};this.cacheSSTable(s,i),this.trimCache(),await this.sstableStore.save(s,i),await this.sstableStore.saveMeta(a),this.immutableMemtable===e&&(this.immutableMemtable=null),this.levels[0].unshift(a),this.frozenMemtables=this.frozenMemtables.filter(t=>t!==e),this.levels[0].length>=4&&!this.compacting&&this.scheduleCompact(0)}scheduleCompact(e){e>=6||this.compacting||(this.compacting=!0,this.flushChain=this.enqueueOnChain(()=>this.compactLevelAsync(e).finally(()=>{this.compacting=!1,this.levels[e].length>=4&&this.scheduleCompact(e),e+1<6&&this.levels[e+1].length>=4&&this.scheduleCompact(e+1)})))}enqueueCompact(e){e>=6||this.compacting||(this.compacting=!0,this.flushChain=this.flushChain.then(async()=>{try{await this.compactLevelAsync(e)}finally{this.compacting=!1}}).catch(e=>{this.compacting=!1,this.lastBackgroundError=e}))}async prefetchRange(e,t){await this.drainChain(),this.trimCache();const s=[];for(let n=0;n<7;n++)for(const i of this.levels[n])ti.maxKey||this.sstableCache.has(i.id)||s.push(i.id);for(const e of s){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}async prefetchKeys(e){if(0===e.length)return;await this.drainChain(),this.trimCache();const t=new Set;for(let s=0;s<7;s++)for(const n of this.levels[s])if(!this.sstableCache.has(n.id))for(const s of e)if(s>=n.minKey&&s<=n.maxKey){t.add(n.id);break}for(const e of t){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}async prefetchPrefixRanges(e){if(0===e.length)return;const t=new Set,s=[];for(const n of e){const e=`${n[0]}\0${n[1]}`;t.has(e)||(t.add(e),s.push(n))}if(0===s.length)return;await this.drainChain(),this.trimCache();const n=new Set;for(let e=0;e<7;e++)for(const t of this.levels[e])if(!this.sstableCache.has(t.id))for(const[e,i]of s)if(!(it.maxKey)){n.add(t.id);break}for(const e of n){const t=this.findMetaById(e);await this.preloadSSTable(e,t)}}findMetaById(e){for(let t=0;t<7;t++)for(const s of this.levels[t])if(s.id===e)return s}get(e){let t=this.memtable.get(e);if(null!==t)return this.unwrapTombstone(t);for(let s=this.frozenMemtables.length-1;s>=0;s--)if(t=this.frozenMemtables[s].get(e),null!==t)return this.unwrapTombstone(t);for(let t=0;t<7;t++)for(const s of this.levels[t]){if(es.maxKey)continue;const t=this.loadSSTableReader(s);if(!t)continue;const n=t.get(e);if(null!==n)return this.unwrapTombstone(n)}return null}rangeScan(e,t){const s=[];return this.rangeScanLazy(e,t,(e,t)=>{s.push([e,t])}),s}rangeScanLazy(e,t,s){const n=new ee;n.addSource(new Y(this.memtable.scanLazy(e,t)));for(let s=this.frozenMemtables.length-1;s>=0;s--)n.addSource(new Y(this.frozenMemtables[s].scanLazy(e,t)));for(let s=0;s<7;s++)for(const i of this.levels[s]){if(ti.maxKey)continue;const s=this.loadSSTableReader(i);s&&n.addSource(new Y(s.scanLazy(e,t)))}let i=n.next();for(;i;){const[e,t]=i;if(!t.__tombstone&&!1===s(e,t))return;i=n.next()}}async compactLevel(e){await this.compactLevelAsync(e,2)}async compactLevelAsync(e,t=4){if(e>=6)return;if(this.levels[e].lengthr.push([e,t])),n.addSource(new Q(r)),i.push(e)}const r=n.drain();if(0===r.length){for(const t of i)this.levels[e].push(t);return void this.levels[e].sort((e,t)=>t.id-e.id)}const a=await this.sstableStore.allocateId(),o=new X(this.blockSize);for(const[e,t]of r)o.add(e,t);const{sstableData:h,indexEntries:c}=o.build(),l={id:a,level:e+1,minKey:r[0][0],maxKey:r[r.length-1][0],blockCount:c.length,totalSize:h.byteLength,bloomData:null};this.cacheSSTable(a,h),this.trimCache(),await this.sstableStore.save(a,h),await this.sstableStore.saveMeta(l),this.levels[e+1].unshift(l);for(const e of s)this.sstableCache.delete(e.id),await this.sstableStore.delete(e.id),await this.sstableStore.deleteMeta(e.id)}async flush(){if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("AriaEngine background flush/compaction failed (data may be inconsistent)","ARIA_BACKGROUND_ERROR",e)}if(await this.drainChain(),this.immutableMemtable){const e=this.immutableMemtable;this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e)),this.immutableMemtable=null}if(this.memtable.getEntryCount()>0){this.freezeMemtable();const e=this.immutableMemtable;e&&(this.flushChain=this.enqueueOnChain(()=>this.flushImmutableAsync(e)),this.immutableMemtable=null)}await this.drainChain(),this.frozenMemtables=this.frozenMemtables.filter(e=>e.getEntryCount()>0)}async clear(){await this.drainChain(),this.memtable.clear(),this.immutableMemtable=null,this.frozenMemtables=[];for(const e of this.levels)for(const t of e)this.sstableCache.delete(t.id),this.sstableStore.delete(t.id).catch(()=>{}),this.sstableStore.deleteMeta(t.id).catch(()=>{});this.levels=[];for(let e=0;e<7;e++)this.levels.push([]);this.sstableCache.clear(),this.cacheSize=0}getStats(){return{memtableSize:this.memtable.getEntryCount(),sstableCount:this.levels.reduce((e,t)=>e+t.length,0),levelCounts:this.levels.map(e=>e.length)}}async validateAll(){let e=0;for(let t=0;t<7;t++){const s=[];for(const n of this.levels[t])await this.validateSSTable(n)?s.push(n):(this.sstableCache.delete(n.id),e++);this.levels[t]=s}return e}async validateSSTable(e){try{const t=await this.sstableStore.load(e.id);if(!t)return this.dropInvalidSSTable(e),!1;if(t.byteLength<32)return this.dropInvalidSSTable(e),!1;try{if(!new J(t,e).verifyChecksum())return this.dropInvalidSSTable(e),!1}catch{return this.dropInvalidSSTable(e),!1}return!0}catch{return this.dropInvalidSSTable(e),!1}}async dropInvalidSSTable(e){try{await this.sstableStore.delete(e.id)}catch{}try{await this.sstableStore.deleteMeta(e.id)}catch{}}unwrapTombstone(e){return e?e.__tombstone?null:e:null}loadSSTableReader(e){const t=this.sstableCache.get(e.id);if(!t)return null;this.sstableCache.delete(e.id),this.sstableCache.set(e.id,t);try{return new J(t,e)}catch{return null}}async preloadSSTable(e,t){if(this.sstableCache.has(e))return;const s=await this.sstableStore.load(e);if(s){if(t)try{if(!new J(s,t).verifyChecksum())return void await this.dropInvalidSSTable(t)}catch{return void await this.dropInvalidSSTable(t)}this.cacheSSTable(e,s)}}cacheSSTable(e,t){this.sstableCache.has(e)&&(this.cacheSize-=this.sstableCache.get(e).byteLength,this.sstableCache.delete(e)),this.sstableCache.set(e,t),this.cacheSize+=t.byteLength}trimCache(){for(;this.cacheSize>this.cacheLimitBytes&&this.sstableCache.size>0;){const e=this.sstableCache.keys().next().value,t=this.sstableCache.get(e);this.cacheSize-=t.byteLength,this.sstableCache.delete(e)}}}class se{constructor(e,t=!0,s="batch"){this.lsn=0,this.buffer=[],this.bufferedBytes=0,this.store=e,this.enabled=t,this.syncMode=s}async append(e){if(!this.enabled)return;this.lsn++;const t={...e,lsn:this.lsn,checksum:0},s=this.encodeRecord(t);"full"===this.syncMode?(await this.store.append(s),this.bufferedBytes+=s.byteLength):"batch"===this.syncMode&&(this.buffer.push(s),this.bufferedBytes+=s.byteLength)}async appendBatch(e){if(!this.enabled||0===e.length)return;const t=[];for(const s of e)this.lsn++,t.push(this.encodeRecord({...s,lsn:this.lsn,checksum:0}));const s=this.mergeChunks(t);"full"===this.syncMode?(await this.store.append(s),this.bufferedBytes+=s.byteLength):"batch"===this.syncMode&&(this.buffer.push(s),this.bufferedBytes+=s.byteLength)}async flush(){if(!this.enabled||0===this.buffer.length)return;const e=this.mergeChunks(this.buffer);await this.store.append(e),this.buffer=[]}mergeChunks(e){if(1===e.length)return e[0];const t=e.reduce((e,t)=>e+t.byteLength,0),s=new Uint8Array(t);let n=0;for(const t of e)s.set(t,n),n+=t.byteLength;return s}async recover(e){if(!this.enabled)return 0;if(!await this.store.exists())return 0;const t=await this.store.readAll();if(0===t.byteLength)return 0;const s=this.decodeAllRecords(t);for(const t of s)e(t);return this.lsn=s.length>0?s[s.length-1].lsn:0,s.length}async checkpoint(){this.enabled&&(await this.flush(),await this.store.truncate(),this.lsn=0,this.bufferedBytes=0)}getBufferedCount(){return this.buffer.length}getBufferedBytes(){return this.bufferedBytes}legacyChecksum(e){let t=0;for(let s=0;s>>0}encodeRecord(e){const t=new TextEncoder,s=t.encode(e.tableName),n=t.encode(e.key),i=e.data?JSON.stringify(e.data):"",r=t.encode(i),a=11+s.length+2+n.length+4+r.length+4,o=new ArrayBuffer(a),h=new DataView(o);let c=0;h.setUint32(c,e.lsn,!1),c+=4,h.setUint8(c,e.type),c+=1,h.setUint32(c,e.txnId,!1),c+=4,h.setUint16(c,s.length,!1),c+=2,new Uint8Array(o).set(s,c),c+=s.length,h.setUint16(c,n.length,!1),c+=2,new Uint8Array(o).set(n,c),c+=n.length,h.setUint32(c,r.length,!1),c+=4,new Uint8Array(o).set(r,c),c+=r.length;const l=x(new Uint8Array(o,0,c));return h.setUint32(c,l,!1),new Uint8Array(o)}decodeAllRecords(e){const t=[],s=new DataView(e.buffer,e.byteOffset,e.byteLength);let n=0;for(;n+15<=e.byteLength;)try{const i=n,r=s.getUint32(n,!1);n+=4;const a=s.getUint8(n);n+=1;const o=s.getUint32(n,!1);n+=4;const h=s.getUint16(n,!1);if(n+=2,n+h>e.byteLength)break;const c=(new TextDecoder).decode(e.slice(n,n+h));n+=h;const l=s.getUint16(n,!1);if(n+=2,n+l>e.byteLength)break;const u=(new TextDecoder).decode(e.slice(n,n+l));n+=l;const f=s.getUint32(n,!1);if(n+=4,n+f>e.byteLength)break;let d;if(f>0){const t=(new TextDecoder).decode(e.slice(n,n+f));try{d=JSON.parse(t)}catch{}}n+=f;const p=s.getUint32(n,!1);n+=4;const y=e.slice(i,n-4),m=x(y),g=this.legacyChecksum(y);if(m>>>0!==p&&g>>>0!==p)continue;t.push({lsn:r,type:a,txnId:o,tableName:c,key:u,data:d,checksum:p})}catch{break}return t}}const ne="__wal_",ie=/^__wal_(\d{6})\.bin$/,re=/^__wal_(\d+)$/,ae="__wal_count";class oe{constructor(e,t=4194304){this.backend=e,this.currentSegment=0,this.currentSize=0,this.segmentSize=t}segmentKey(e){return`${ne}${String(e).padStart(6,"0")}.bin`}async append(e){if(0===e.byteLength)return;this.currentSize+e.byteLength>this.segmentSize&&(this.currentSegment++,this.currentSize=0);const t=this.segmentKey(this.currentSegment),s=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);if("function"==typeof this.backend.append)await this.backend.append(t,s);else{const e=await this.backend.read(t);if(e){const n=new ArrayBuffer(e.byteLength+s.byteLength);new Uint8Array(n).set(new Uint8Array(e),0),new Uint8Array(n).set(new Uint8Array(s),e.byteLength),await this.backend.write(t,n)}else await this.backend.write(t,s)}this.currentSize+=e.byteLength}async readAll(){const e=await this.backend.listKeys(),t=e.filter(e=>ie.test(e)).map(e=>({seq:Number(e.match(ie)[1]),key:e})).sort((e,t)=>e.seq-t.seq);let s=0;for(let e=0;ere.test(e)).map(e=>({seq:Number(e.match(re)[1]),key:e})).sort((e,t)=>e.seq-t.seq),r=[];for(const{key:e}of i){const t=await this.backend.read(e);t&&r.push(new Uint8Array(t))}for(const{key:e}of n){const t=await this.backend.read(e);t&&r.push(new Uint8Array(t))}if(n.length>0){this.currentSegment=n[n.length-1].seq;const e=await this.backend.read(n[n.length-1].key);this.currentSize=e?e.byteLength:0,i.length>0&&(this.currentSegment++,this.currentSize=0)}else i.length>0&&(this.currentSegment=0,this.currentSize=0);const a=r.reduce((e,t)=>e+t.byteLength,0),o=new Uint8Array(a);let h=0;for(const e of r)o.set(e,h),h+=e.byteLength;return o}async truncate(){const e=(await this.backend.listKeys()).filter(e=>e.startsWith(ne)||e===ae);e.length>0&&await this.backend.deleteMany(e),this.currentSegment=0,this.currentSize=0}async exists(){return(await this.backend.listKeys()).some(e=>e.startsWith(ne)||e===ae)}}class he{constructor(){this.acquired=!1,this.supported=!1,this.releaseResolve=null,this.releasePromise=null}async acquire(e){const t=globalThis.navigator,s=t?.locks;return s&&"function"==typeof s.request?(this.supported=!0,await new Promise((t,i)=>{s.request.bind(s)(function(e){return`metona-sqlark:${e}`}(e),{ifAvailable:!0,mode:"exclusive"},async s=>{if(!s)return void i(new n(`Database "${e}" is already open in another tab (locked)`,"ARIA_LOCKED"));this.acquired=!0;const r=new Promise(e=>{this.releaseResolve=e});this.releasePromise=r,t(),await r})}),!0):(this.supported=!1,!1)}async release(){if(this.acquired){if(this.releaseResolve){const e=this.releaseResolve,t=this.releasePromise;this.releaseResolve=null,this.releasePromise=null,e();try{await t}catch{}}this.acquired=!1}}isAcquired(){return this.acquired}isSupported(){return this.supported}}class ce{constructor(e,t,s=null,n=1e3,i=16777216){this.opCount=0,this.lsm=e,this.wal=t,this.flushable=s,this.interval=n,this.walSizeThreshold=i}async tick(){this.opCount++,(this.opCount>=this.interval||this.getWALEstimatedSize()>=this.walSizeThreshold)&&await this.checkpoint()}getWALEstimatedSize(){const e=this.wal;if("function"==typeof e.getBufferedBytes){const t=e.getBufferedBytes();if(t>0)return t}return 200*("function"==typeof e.getBufferedCount?e.getBufferedCount():0)}async checkpoint(){await this.lsm.flush(),this.flushable&&await this.flushable.flushAll(),await this.wal.checkpoint(),this.opCount=0}}class le{constructor(){this.store=new Map,this.opened=!1}async open(e){this.opened=!0}async close(){this.store.clear(),this.opened=!1}isOpen(){return this.opened}async read(e){return this.store.get(e)??null}async write(e,t){this.store.set(e,t)}async writeMany(e){for(const[t,s]of Object.entries(e))this.store.set(t,s)}async delete(e){this.store.delete(e)}async deleteMany(e){for(const t of e)this.store.delete(t)}async listKeys(){return Array.from(this.store.keys())}async exists(e){return this.store.has(e)}async clear(){this.store.clear()}}class ue{constructor(e,t){this.kv=new L(e,t)}getKV(){return this.kv}async open(e){await this.kv.open(e)}async close(){await this.kv.close()}isOpen(){return this.kv.isOpen()}async read(e){return this.kv.get(e)}async write(e,t){await this.kv.put(e,t)}async append(e,t){await this.kv.appendValue(e,t)}async writeMany(e){await this.kv.putMany(e)}async delete(e){await this.kv.delete(e)}async deleteMany(e){await this.kv.deleteMany(e)}async listKeys(){return this.kv.listKeys()}async exists(e){return this.kv.exists(e)}async clear(){await this.kv.clear()}}const fe="AES-GCM";class de{constructor(){this.cryptoKey=null,this._enabled=!1}get enabled(){return this._enabled}async init(e,t){const s=new TextEncoder,n=await crypto.subtle.importKey("raw",s.encode(e),"PBKDF2",!1,["deriveKey"]),i=t||crypto.getRandomValues(new Uint8Array(16));return this.cryptoKey=await crypto.subtle.deriveKey({name:"PBKDF2",salt:i,iterations:1e5,hash:"SHA-256"},n,{name:fe,length:256},!1,["encrypt","decrypt"]),this._enabled=!0,i}async encryptPage(e){if(!this.cryptoKey)throw new Error("Crypto not initialized");const t=crypto.getRandomValues(new Uint8Array(12)),s=e instanceof Uint8Array?e:new Uint8Array(e);return{iv:t,data:await crypto.subtle.encrypt({name:fe,iv:t},this.cryptoKey,s)}}async decryptPage(e,t){if(!this.cryptoKey)throw new Error("Crypto not initialized");const s=t instanceof Uint8Array?t:new Uint8Array(t);return crypto.subtle.decrypt({name:fe,iv:e},this.cryptoKey,s)}close(){this.cryptoKey=null,this._enabled=!1}}const pe=12,ye="__aria_keymeta";function me(e){if("undefined"!=typeof Buffer)return Buffer.from(e).toString("base64");let t="";for(let s=0;se!==ye))throw new n("Cannot open with encryption: existing database has no key metadata (database was created without encryption, or key metadata was lost)","ARIA_ENCRYPT_CONFIG_ERROR");const e=await this.crypto.init(this.password),t=await this.crypto.encryptPage((new TextEncoder).encode("metona-sqlark-encryption-verifier").buffer),s=new Uint8Array(pe+t.data.byteLength);s.set(t.iv,0),s.set(new Uint8Array(t.data),pe);const i=JSON.stringify({salt:me(e),verifier:me(s)});await this.inner.write(ye,(new TextEncoder).encode(i).buffer)}this.opened=!0}async close(){await this.inner.close(),this.crypto.close(),this.opened=!1}isOpen(){return this.opened}async read(e){this.ensureReady();const t=await this.inner.read(e);return null===t?null:this.decrypt(t)}async write(e,t){this.ensureReady(),await this.inner.write(e,await this.encrypt(t))}async writeMany(e){this.ensureReady();const t={};for(const[s,n]of Object.entries(e))t[s]=await this.encrypt(n);await this.inner.writeMany(t)}async delete(e){this.ensureReady(),await this.inner.delete(e)}async deleteMany(e){this.ensureReady(),await this.inner.deleteMany(e)}async listKeys(){return this.ensureReady(),this.inner.listKeys()}async exists(e){return this.ensureReady(),this.inner.exists(e)}async clear(){const e=(await this.inner.listKeys()).filter(e=>e!==ye);e.length>0&&await this.inner.deleteMany(e)}ensureReady(){if(!this.opened)throw new n("EncryptedBackend not opened","ARIA_DB_NOT_OPEN");if(!this.crypto.enabled)throw new n("EncryptedBackend key not initialized","ARIA_DECRYPT_ERROR")}async encrypt(e){const t=await this.crypto.encryptPage(e),s=new ArrayBuffer(pe+t.data.byteLength),n=new Uint8Array(s);return n.set(t.iv,0),n.set(new Uint8Array(t.data),pe),s}async decrypt(e){const t=new Uint8Array(e);if(t.byteLength<=pe)throw new n("Corrupted encrypted data block (too short)","ARIA_DECRYPT_ERROR");const s=t.subarray(0,pe),i=t.subarray(pe);try{return await this.crypto.decryptPage(s,i)}catch(e){if(e instanceof n)throw e;throw new n("Decryption failed (data corrupted or wrong key)","ARIA_DECRYPT_ERROR",e)}}}class be{constructor(e,t){this.fileManager=e,this.bufferPool=t,this.pageIds=new Map}async save(e,t){const s=Math.max(1,Math.ceil(t.byteLength/M)),n=await this.bufferPool.newPages(s,P.DATA),i=[];for(let e=0;ee+t.byteLength,0),r=new Uint8Array(Math.min(i,s));let a=0;for(const e of n){const t=Math.min(e.byteLength,r.byteLength-a);if(t<=0)break;r.set(e.subarray(0,t),a),a+=t}return this.pageIds.delete(e),r}async delete(e,t){for(const e of t){this.bufferPool.removePage(e);try{await this.fileManager.freePageId(e)}catch{}}this.pageIds.delete(e)}}class Te{constructor(e){this.nextPageId=0,this.metaLoaded=!1,this.dbName="",this.backend=e}async init(e){this.dbName=e;const t=await this.backend.read("__aria_meta");let s=1;t&&t instanceof ArrayBuffer&&t.byteLength>=4&&(s=new DataView(t).getUint32(0,!1));const n=await this.backend.listKeys();for(const e of n)if(e.startsWith("pg_")){const t=Number.parseInt(e.slice(3),10);!Number.isNaN(t)&&t+1>s&&(s=t+1)}this.nextPageId=s,t&&t instanceof ArrayBuffer&&!(t.byteLength<4)&&s===new DataView(t).getUint32(0,!1)||await this.saveMeta(),this.metaLoaded=!0}async readPage(e){const t=`pg_${e}`,s=await this.backend.read(t);if(!s)return null;if(s.byteLengtht.txnId!==e);0===n.length?this.versionStore.delete(t):this.versionStore.set(t,n)}this.activeTxns.delete(e),this.txnWriteKeys.delete(e)}rollbackTransaction(e){const t=this.activeTxns.get(e);if(!t)throw new Error(`Transaction ${e} not found`);t.state=q.ABORTED;const s=this.txnWriteKeys.get(e);if(s)for(const t of s){const s=this.versionStore.get(t);if(!s)continue;const n=s.filter(t=>t.txnId!==e);0===n.length?this.versionStore.delete(t):this.versionStore.set(t,n)}this.activeTxns.delete(e),this.txnWriteKeys.delete(e)}writeVersion(e,t,s,n){const i=`${e}.${t}`,r=this.versionStore.get(i)??[],a={txnId:n,data:s,prevVersion:r.length>0?r[r.length-1]:null,committed:!1};r.push(a),this.versionStore.set(i,r),this.txnWriteKeys.get(n)?.add(i)}deleteVersion(e,t,s){this.writeVersion(e,t,{__mvcc_tombstone:!0},s)}discardVersions(e){const t=this.txnWriteKeys.get(e);if(t)for(const s of t){const t=this.versionStore.get(s);if(!t)continue;const n=t.filter(t=>t.txnId!==e);0===n.length?this.versionStore.delete(s):this.versionStore.set(s,n)}}gc(e=100){for(const[t,s]of this.versionStore){if(s.length<=e)continue;const n=s.slice(s.length-e);this.versionStore.set(t,n)}}getGlobalLSN(){return this.globalCommitLsn}}function ke(e,t){const s=new ArrayBuffer(M);return function(e,t,s){const n=new DataView(e);n.setUint32(0,t,!1),n.setUint8(4,s),n.setUint16(5,16,!1),n.setUint16(7,e.byteLength,!1),n.setUint16(9,0,!1),n.setUint32(11,0,!1),n.setUint8(15,0)}(s,e,t),{pageId:e,type:t,data:s,dirty:!0,pins:0,prev:null,next:null,lastAccess:Date.now()}}class xe{constructor(){this.head=null,this.tail=null,this._size=0}get size(){return this._size}moveToHead(e){this.head!==e&&(null!==e.prev||null!==e.next||this.head===e||this.tail===e?this.detach(e):this._size++,e.prev=null,e.next=this.head,this.head&&(this.head.prev=e),this.head=e,this.tail||(this.tail=e))}remove(e){(null!==e.prev||null!==e.next||this.head===e||this.tail===e)&&(this.detach(e),this._size=Math.max(0,this._size-1))}detach(e){e.prev?e.prev.next=e.next:this.head===e&&(this.head=e.next),e.next?e.next.prev=e.prev:this.tail===e&&(this.tail=e.prev),e.prev=null,e.next=null}clear(){this.head=null,this.tail=null,this._size=0}getLRU(){return this.tail}}class Se{constructor(e,t,s){this.lru=new xe,this.capacity=e,this.onEvict=t,this.onRemove=s}access(e){e.lastAccess=Date.now(),this.lru.moveToHead(e)}add(e){this.access(e)}remove(e){this.lru.remove(e)}async evictIfNeeded(e){let t=0;for(;this.lru.size+e>this.capacity&&this.lru.size>0;){const e=this.findEvictionCandidate();if(!e)break;e.dirty&&(await this.onEvict(e),e.dirty=!1),this.lru.remove(e),this.onRemove?.(e),t++}return t}findEvictionCandidate(){let e=this.lru.getLRU();for(;e;){if(0===e.pins&&!e.dirty)return e;e=e.prev}for(e=this.lru.getLRU();e;){if(0===e.pins)return e;e=e.prev}return null}clear(){this.lru.clear()}}class Ie{constructor(e,t=256){this.pages=new Map,this.nextPageId=0,this.pageIO=e,this.eviction=new Se(t,async e=>{e.dirty&&(await this.pageIO.writePage(e.pageId,e.data),e.dirty=!1)},e=>{this.pages.delete(e.pageId)})}async getPage(e){let t=this.pages.get(e);if(t)return this.eviction.access(t),t.pins++,t;const s=await this.pageIO.readPage(e);return s?(await this.eviction.evictIfNeeded(1),t={pageId:e,type:new DataView(s).getUint8(4),data:s,dirty:!1,pins:1,prev:null,next:null,lastAccess:Date.now()},this.pages.set(e,t),this.eviction.add(t),t):null}async newPages(e,t=P.DATA){if(e<=0)return[];let s;if("function"==typeof this.pageIO.allocatePageIds)s=await this.pageIO.allocatePageIds(e);else{s=[];for(let t=0;t0&&e.pins--}markDirty(e){e.dirty=!0}async flushPage(e){const t=this.pages.get(e);t&&t.dirty&&(await this.pageIO.writePage(e,t.data),t.dirty=!1)}async flushAll(){for(const[,e]of this.pages)e.dirty&&(await this.pageIO.writePage(e.pageId,e.data),e.dirty=!1)}removePage(e){const t=this.pages.get(e);t&&(this.eviction.remove(t),this.pages.delete(e))}async clear(){await this.flushAll(),this.pages.clear(),this.eviction.clear()}}class Ae{constructor(e={}){this.name="aria",this.opened=!1,this.dbName="",this.dbLock=null,this.schemas=new Map,this.tablePKs=new Map,this.opCounter=0,this.secondaryIndexes=new Map,this.uniqueIndexCols=new Set,this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.gcCounter=0,this.savepoints=new Map,this.config={...z,...e}}async open(e,t){if(!this.opened)try{await this.openInternal(e)}catch(t){if(this.dbLock){try{await this.dbLock.release()}catch{}this.dbLock=null}if(t instanceof n)throw t;throw new n(`Failed to open AriaEngine database "${e}"`,"ARIA_OPEN_ERROR",t)}}async openInternal(e){this.dbName=e;const t=new he;let s;if(this.dbLock=t,await t.acquire(e),s="opfs"===this.config.storageBackend?new w:"kv"===this.config.storageBackend?new ue:new le,await s.open(e),this.config.encryption?.password)this.backend=new we(s,this.config.encryption.password);else if(this.backend=s,await s.exists("__aria_keymeta"))throw await s.close(),new n("Database is encrypted: provide encryption.password to open it","ARIA_ENCRYPT_REQUIRED");await this.backend.open(e),this.fileManager=new Te(this.backend),await this.fileManager.init(e),this.bufferPool=new Ie(this.fileManager,this.config.bufferPoolPages);const i=this.createSSTableStore("main");this.lsm=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:i}),this.wal=new se(new oe(this.backend),this.config.walEnabled,this.config.walSyncMode),await this.loadSchemas();for(const[e,t]of this.schemas){const s=this.tablePKs.get(e);for(const[n,i]of Object.entries(t.columns))if((i.index||i.unique)&&n!==s){const t=`${e}:idx:${n}`;if(!this.secondaryIndexes.has(t)){const s=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e}_${n}`)});await s.init(),this.secondaryIndexes.set(t,s)}}}await this.lsm.init();const r=new Set,a=[];await this.wal.recover(e=>a.push(e));for(const e of a)e.type===K.COMMIT&&r.add(e.txnId),e.type===K.ROLLBACK&&r.delete(e.txnId);for(const e of a)(0===e.txnId||r.has(e.txnId))&&(e.type===K.DROP_TABLE?await this.applyDropTableRecovery(e.tableName):this.applyWALRecord(e));if(a.length>0){await this.lsm.flush(),await this.wal.checkpoint();for(const e of this.schemas.keys())await this.reindexTableInternal(e)}this.checkpointManager=new ce(this.lsm,{checkpoint:async()=>{this.currentTxnId||await this.wal.checkpoint()},flush:async()=>{this.currentTxnId||await this.wal.flush()},getBufferedBytes:()=>this.wal.getBufferedBytes(),getBufferedCount:()=>this.wal.getBufferedCount()},{flushAll:async()=>{await this.lsm.flush();for(const e of this.secondaryIndexes.values())await e.flush()}},this.config.checkpointInterval,this.config.walSizeThreshold),this.opened=!0}async close(){if(this.opened){await this.persistSchemas(),await this.lsm.flush();for(const e of this.secondaryIndexes.values())await e.flush();await this.bufferPool.flushAll(),await this.wal.flush(),await this.wal.checkpoint(),await this.backend.close(),this.dbLock&&(await this.dbLock.release(),this.dbLock=null),this.schemas.clear(),this.tablePKs.clear(),this.secondaryIndexes.clear(),this.uniqueIndexCols.clear(),this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.savepoints.clear(),this.opCounter=0,this.opened=!1}}async repair(){this.ensureOpen(),await this.bufferPool.clear(),await this.lsm.validateAll(),await this.lsm.flush(),await this.wal.checkpoint();for(const e of this.schemas.keys())await this.reindexTable(e);const e=this.backend;if("function"==typeof e.cleanupStaleFiles)try{await e.cleanupStaleFiles()}catch{}await this.cleanupOrphanPages()}async cleanupOrphanPages(){const e=(await this.backend.listKeys()).filter(e=>/^pg_\d+$/.test(e));if(0===e.length)return;const t=new Set,s=async e=>{const s="main"===e?"__aria_lsm_meta":`__aria_lsm_meta_${e}`,n=await this.backend.read(s);if(n)try{const e=JSON.parse((new TextDecoder).decode(n));for(const s of e)if(s.pageIds)for(const e of s.pageIds)t.add(e)}catch{}};await s("main");for(const[e,t]of this.schemas)for(const[n,i]of Object.entries(t.columns))(i.index||i.unique)&&await s(`idx_${e}_${n}`);const n=e.map(e=>Number(e.slice(3))).filter(e=>!t.has(e));n.length>0&&await this.backend.deleteMany(n.map(e=>`pg_${e}`))}async clearAll(){this.ensureOpen(),await this.backend.clear(),await this.bufferPool.clear(),await this.fileManager.clearAll(),this.schemas.clear(),this.tablePKs.clear(),this.secondaryIndexes.clear(),this.uniqueIndexCols.clear(),this.lsm.clear(),this.mvcc=new Ee,this.currentTxnId=null,this.txnSnapshot=null,this.savepoints.clear(),this.opCounter=0,await this.persistSchemas(),await this.wal.checkpoint()}isOpen(){return this.opened}async getMeta(e){const t=await this.backend.read(`__meta_${e}`);return t?(new TextDecoder).decode(t):null}async setMeta(e,t){await this.backend.write(`__meta_${e}`,(new TextEncoder).encode(t).buffer)}async createTable(e){if(this.ensureOpen(),this.ensureNoDDLInTransaction("CREATE TABLE"),this.schemas.has(e.name))throw new n(`Table "${e.name}" already exists`,"TABLE_EXISTS");this.schemas.set(e.name,e),this.tablePKs.set(e.name,this.getPK(e));for(const[t,s]of Object.entries(e.columns))if(s.index||s.unique){const s=`${e.name}:idx:${t}`;if(!this.secondaryIndexes.has(s)){const n=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e.name}_${t}`)});await n.init(),this.secondaryIndexes.set(s,n)}}await this.persistSchemas(),await this.wal.append({type:K.CREATE_TABLE,txnId:0,tableName:e.name,key:"",data:{schema:JSON.stringify(e)}})}async dropTable(e){this.ensureOpen(),this.ensureNoDDLInTransaction("DROP TABLE"),this.ensureTable(e);const t=await this.getAllRows(e);for(const s of t){const t=this.tablePKs.get(e);this.lsm.delete(`${e}:${s[t]}`)}await this.cleanupTableIndexes(e);const s=`${e}:`;for(const e of this.uniqueIndexCols)e.startsWith(s)&&this.uniqueIndexCols.delete(e);this.schemas.delete(e),this.tablePKs.delete(e),await this.persistSchemas(),await this.wal.append({type:K.DROP_TABLE,txnId:0,tableName:e,key:""})}async cleanupTableIndexes(e){const t=`${e}:idx:`,s=[];for(const[e,n]of this.secondaryIndexes)if(e.startsWith(t)){s.push(e);try{await n.clear()}catch{}}for(const e of s)this.secondaryIndexes.delete(e)}async hasTable(e){return this.ensureOpen(),this.schemas.has(e)}async getTableNames(){return this.ensureOpen(),Array.from(this.schemas.keys())}async getTableSchema(e){return this.ensureOpen(),this.schemas.get(e)??null}async insert(e,t){this.ensureOpen(),this.ensureTable(e);const s=this.schemas.get(e),i=this.tablePKs.get(e),r=[],a=[],o=this.uniqueColumns(e,s),h=[];for(const n of t){const t=this.validateRow(s,n),r=String(t[i]);h.push({row:t,pkValue:r,key:`${e}:${r}`})}await this.lsm.prefetchKeys(h.map(e=>e.key));const c=new Set;for(const{pkValue:t,key:s}of h){if(c.has(t))throw new n(`Duplicate primary key "${t}" in table "${e}"`,"DUPLICATE_KEY");c.add(t);const i=this.currentTxnId?this.txnSnapshot?.get(s)??this.lsm.get(s):this.lsm.get(s);if(i&&!i.__txn_deleted)throw new n(`Duplicate primary key "${t}" in table "${e}"`,"DUPLICATE_KEY")}for(const t of o){const s=this.secondaryIndexes.get(`${e}:idx:${t}`);await s.prefetchPrefixRanges(h.map(e=>{const s=e.row[t];if(null==s)return null;const n=`${String(s)}:`;return[n,`${n}￿`]}).filter(e=>null!==e))}const l=new Map;for(const{row:t,pkValue:s}of h)for(const i of o){const r=t[i];if(null==r)continue;const a=String(r);let o=l.get(i);if(o||(o=new Set,l.set(i,o)),o.has(a))throw new n(`Unique constraint violation on column "${i}" in table "${e}"`,"UNIQUE_VIOLATION");o.add(a),this.checkUniqueSync(e,[i],t,s)}for(const{row:t,pkValue:s,key:n}of h)this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(n,t),this.mvcc.writeVersion(e,s,t,this.currentTxnId)):this.lsm.put(n,t),this.updateSecondaryIndexes(e,s,t,null),r.push(s),a.push({type:K.INSERT,txnId:this.currentTxnId??0,tableName:e,key:s,data:t});return await this.wal.appendBatch(a),this.opCounter+=t.length,this.checkMemoryBudget(),await this.checkpointManager.tick(),this.tryGC(),r}async find(e,t){let s;this.ensureOpen(),this.ensureTable(e);const n=await this.tryIndexLookup(e,t);s=null!==n?n:await this.getAllRows(e),s=this.mergeTxnSnapshot(e,s),t.where&&Object.keys(t.where).length>0&&(s=s.filter(e=>o(e,t.where))),t.orderBy&&t.orderBy.length>0&&(s=l(s,t.orderBy));const i=t.offset??0,r=t.limit??s.length;return s=s.slice(i,i+r),t.columns&&t.columns.length>0&&"*"!==t.columns[0]&&(s=s.map(e=>f(e,t.columns))),this.trimAllCaches(),s}async update(e,t,s){if(this.ensureOpen(),this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const i=this.schemas.get(e),r=await this.getAllRows(e);let h=0;const c=[],l=new Set,u=p(s);for(const t of Object.keys(u))if(!i.columns[t])throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const f=this.uniqueColumns(e,i);for(const t of f){const s=this.secondaryIndexes.get(`${e}:idx:${t}`),n=[];if(void 0!==u[t]&&null!==u[t]){const e=`${String(u[t])}:`;n.push([e,`${e}￿`])}else if(!(t in u))for(const e of r){const s=e[t];if(null==s)continue;const i=`${String(s)}:`;n.push([i,`${i}￿`])}await s.prefetchPrefixRanges(n)}const d=[],y=new Map;for(const s of r){const r=this.tablePKs.get(e),a=`${e}:${s[r]}`;if(t.where&&Object.keys(t.where).length>0&&!o(s,t.where))continue;const h={...s,...u};this.validateRow(i,h),this.checkBatchUnique(e,f,h,y),this.checkUniqueSync(e,f,h,String(s[r]));const c=String(h[r]),l=c!==String(s[r]);if(l){const t=`${e}:${c}`,s=this.currentTxnId?this.txnSnapshot?.get(t)??this.lsm.get(t):this.lsm.get(t);if(s&&!s.__txn_deleted)throw new n(`Duplicate primary key "${c}" in table "${e}" (cannot update key to existing value)`,"DUPLICATE_KEY")}d.push({row:s,pk:String(s[r]),key:a,updated:h,newPk:c,pkChanged:l})}for(const t of d)t.pkChanged&&await this.checkForeignKeyUpdateRestrict(e,t.pk,t.newPk);for(const{row:t,pk:s,key:n,updated:i,newPk:r,pkChanged:a}of d)a&&await this.applyForeignKeyUpdateRules(e,s,r,c,l),this.currentTxnId&&this.txnSnapshot?(a&&(this.txnSnapshot.set(n,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,s,this.currentTxnId)),this.txnSnapshot.set(`${e}:${r}`,i),this.mvcc.writeVersion(e,r,i,this.currentTxnId)):(a&&this.lsm.delete(n),this.lsm.put(`${e}:${r}`,i)),h++,a&&c.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:s}),c.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:e,key:r,data:i}),this.updateSecondaryIndexes(e,r,i,t);return await this.wal.appendBatch(c),this.opCounter+=h,await this.checkpointManager.tick(),this.trimAllCaches(),h}checkBatchUnique(e,t,s,i){for(const r of t){const t=s[r];if(null==t)continue;let a=i.get(r);if(a||(a=new Set,i.set(r,a)),a.has(t))throw new n(`Unique constraint violation on column "${r}" in table "${e}"`,"UNIQUE_VIOLATION");a.add(t)}}async checkForeignKeyUpdateRestrict(e,t,s){for(const[s,i]of this.schemas)if(s!==e)for(const[r,a]of Object.entries(i.columns)){if(!a.references||!a.onUpdate)continue;const[i]=a.references.split(".");if(i===e&&("RESTRICT"===a.onUpdate||"SET NULL"===a.onUpdate&&a.required)){const i=await this.getAllRows(s);for(const o of i)if(String(o[r])===t){const i="RESTRICT"===a.onUpdate?`foreign key "${r}" in "${s}" has dependent rows`:`foreign key "${r}" in "${s}" is required (SET NULL violates constraint)`;throw new n(`Cannot update "${e}" key "${t}": ${i}`,"FOREIGN_KEY_VIOLATION")}}}}async applyForeignKeyUpdateRules(e,t,s,n,i){const r=`${e}:${t}`;if(!i.has(r)){i.add(r);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onUpdate)continue;const[r]=o.references.split(".");if(r!==e)continue;if("CASCADE"!==o.onUpdate&&"SET NULL"!==o.onUpdate)continue;const h=await this.getAllRows(i);for(const e of h){if(String(e[a])!==t)continue;const r=this.tablePKs.get(i),h=String(e[r]),c={...e,[a]:"CASCADE"===o.onUpdate?s:null},l=`${i}:${h}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(l,c),this.mvcc.writeVersion(i,h,c,this.currentTxnId)):this.lsm.put(l,c),this.updateSecondaryIndexes(i,h,c,e),n.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:i,key:h,data:c})}}}}async delete(e,t){if(this.ensureOpen(),this.ensureTable(e),a(t.where))throw new n("Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)","NOT_SUPPORTED");const s=await this.getAllRows(e);let i=0;const r=[],h=new Set,c=[];for(const n of s)t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||c.push(String(n[this.tablePKs.get(e)]));const l=new Set;for(const t of c)await this.checkCascadeRestrict(e,t,l);for(const n of s){const s=this.tablePKs.get(e),a=`${e}:${n[s]}`;t.where&&0!==Object.keys(t.where).length&&!o(n,t.where)||(i+=await this.applyForeignKeyRules(e,String(n[s]),r,h),this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(a,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,String(n[s]),this.currentTxnId)):this.lsm.delete(a),i++,r.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:String(n[s])}),this.updateSecondaryIndexes(e,String(n[s]),null,n))}return await this.wal.appendBatch(r),this.opCounter+=i,await this.checkpointManager.tick(),this.trimAllCaches(),i}async checkCascadeRestrict(e,t,s){const i=`${e}:${t}`;if(!s.has(i)){s.add(i);for(const[i,r]of this.schemas)if(i!==e)for(const[a,o]of Object.entries(r.columns)){if(!o.references||!o.onDelete)continue;const[r]=o.references.split(".");if(r!==e)continue;const h=(await this.getAllRows(i)).filter(e=>String(e[a])===t);if("RESTRICT"===o.onDelete&&h.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("SET NULL"===o.onDelete&&o.required&&h.length>0)throw new n(`Cannot delete from "${e}": foreign key "${a}" in "${i}" is required (SET NULL violates constraint)`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===o.onDelete){const e=this.tablePKs.get(i);for(const t of h)await this.checkCascadeRestrict(i,String(t[e]),s)}}}}async applyForeignKeyRules(e,t,s,i){let r=0;const a=`${e}:${t}`;if(i.has(a))return 0;i.add(a);for(const[a,o]of this.schemas)if(a!==e)for(const[h,c]of Object.entries(o.columns)){if(!c.references||!c.onDelete)continue;const[o]=c.references.split(".");if(o!==e)continue;const l=(await this.getAllRows(a)).filter(e=>String(e[h])===t);if("RESTRICT"===c.onDelete&&l.length>0)throw new n(`Cannot delete from "${e}": foreign key "${h}" in "${a}" has dependent rows`,"FOREIGN_KEY_VIOLATION");if("CASCADE"===c.onDelete){const e=this.tablePKs.get(a);for(const t of l){const n=String(t[e]);r+=await this.applyForeignKeyRules(a,n,s,i);const o=`${a}:${n}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(o,{__txn_deleted:!0}),this.mvcc.deleteVersion(a,n,this.currentTxnId)):this.lsm.delete(o),this.updateSecondaryIndexes(a,n,null,t),s.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:a,key:n}),r++}}else if("SET NULL"===c.onDelete){const e=this.tablePKs.get(a);for(const t of l){const n=String(t[e]),i={...t,[h]:null},r=`${a}:${n}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(r,i),this.mvcc.writeVersion(a,n,i,this.currentTxnId)):this.lsm.put(r,i),this.updateSecondaryIndexes(a,n,i,t),s.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:a,key:n,data:i})}}}return r}async findStream(e,t,s){this.ensureOpen(),this.ensureTable(e);const n=!!(t.where&&Object.keys(t.where).length>0),i=t.columns&&t.columns.length>0&&"*"!==t.columns[0]?e=>f(e,t.columns):null,r=t.limit??1/0,a=t.offset??0,h=this.tablePKs.get(e),c=`${e}:`;let l=0,u=0;const d=e=>!(!n||o(e,t.where))||(u{if(l>=r)return!1;const s={...t};return s[h]=e.slice(c.length),d(s)}),l}async count(e,t){this.ensureOpen(),this.ensureTable(e);const s=await this.getAllRows(e);return this.trimAllCaches(),t?.where&&0!==Object.keys(t.where).length?s.filter(e=>o(e,t.where)).length:s.length}async clear(e){this.ensureOpen(),this.ensureTable(e);const t=await this.getAllRows(e),s=[];for(const n of t){const t=this.tablePKs.get(e),i=`${e}:${n[t]}`;this.currentTxnId&&this.txnSnapshot?(this.txnSnapshot.set(i,{__txn_deleted:!0}),this.mvcc.deleteVersion(e,String(n[t]),this.currentTxnId)):this.lsm.delete(i),s.push({type:K.DELETE,txnId:this.currentTxnId??0,tableName:e,key:String(n[t])}),this.updateSecondaryIndexes(e,String(n[t]),null,n)}await this.wal.appendBatch(s),this.opCounter+=t.length,await this.checkpointManager.tick(),this.tryGC()}async alterTable(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("ALTER TABLE"),this.ensureTable(e);const i=this.schemas.get(e);if("ADD"===t){if(i.columns[s.name])throw new n(`Column "${s.name}" already exists in table "${e}"`,"COLUMN_EXISTS");return i.columns[s.name]=s,void await this.persistSchemas()}if(!i.columns[s.name])throw new n(`Column "${s.name}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.columns[s.name].index||i.columns[s.name].unique){const t=`${e}:idx:${s.name}`,n=this.secondaryIndexes.get(t);if(n){try{await n.clear()}catch{}this.secondaryIndexes.delete(t)}}delete i.columns[s.name],await this.persistSchemas();const r=`${e}:`,a=`${r}￿`;await this.lsm.prefetchRange(r,a);const o=this.lsm.rangeScan(r,a),h=[];for(const[t,n]of o){if(!(s.name in n))continue;const i={...n};delete i[s.name],this.lsm.put(t,i);const a=t.slice(r.length);this.updateSecondaryIndexes(e,a,i,n),h.push({type:K.UPDATE,txnId:this.currentTxnId??0,tableName:e,key:a,data:i})}await this.wal.appendBatch(h),this.opCounter+=h.length,await this.checkpointManager.tick(),this.trimAllCaches()}async createIndex(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("CREATE INDEX"),this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");const r=`${e}:idx:${t}`;if(this.secondaryIndexes.has(r))return;const a=new te({memtableSizeThreshold:this.config.memtableSizeThreshold,levelSizeMultiplier:this.config.levelSizeMultiplier,blockSize:this.config.pageSize,bloomBitsPerKey:this.config.bloomFilterBitsPerKey,cacheLimitBytes:this.config.bufferPoolPages*this.config.pageSize,sstableStore:this.createSSTableStore(`idx_${e}_${t}`)});await a.init(),this.secondaryIndexes.set(r,a);try{const i=this.tablePKs.get(e),r=await this.getAllRows(e),o=new Set;for(const h of r){const r=h[t];if(null!=r){const c=String(r);if(s&&o.has(c))throw new n(`Unique index on column "${t}" in table "${e}" cannot be created: duplicate value "${c}"`,"UNIQUE_VIOLATION");o.add(c),a.put(`${c}:${h[i]}`,{pk:h[i]})}}await a.flush()}catch(e){this.secondaryIndexes.delete(r);try{await a.clear()}catch{}throw e}i.index=!0,s&&(i.unique=!0,this.uniqueIndexCols.add(`${e}:${t}`)),await this.persistSchemas()}async dropIndex(e,t,s){this.ensureOpen(),this.ensureNoDDLInTransaction("DROP INDEX"),this.ensureTable(e);const i=this.schemas.get(e).columns[t];if(!i)throw new n(`Column "${t}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");if(i.primaryKey)throw new n(`Cannot drop primary key index on column "${t}"`,"NOT_SUPPORTED");if(!i.index&&!i.unique&&!this.secondaryIndexes.has(`${e}:idx:${t}`))throw new n(`Index on column "${t}" does not exist in table "${e}"`,"INDEX_NOT_FOUND");const r=`${e}:${t}`;if(i.unique&&!this.uniqueIndexCols.has(r))throw new n(`Cannot drop index on column "${t}" in table "${e}": UNIQUE constraint defined at table creation must be removed by recreating the table`,"NOT_SUPPORTED");i.index=!1,i.unique=!1,this.uniqueIndexCols.delete(r);const a=`${e}:idx:${t}`,o=this.secondaryIndexes.get(a);o&&(await o.clear(),this.secondaryIndexes.delete(a)),await this.persistSchemas()}async beginTransaction(){if(this.currentTxnId)throw new n("Transaction already in progress","TX_ACTIVE");this.currentTxnId=this.mvcc.beginTransaction(),this.txnSnapshot=new Map;try{await this.wal.append({type:K.BEGIN,txnId:this.currentTxnId,tableName:"",key:""})}catch(e){throw this.mvcc.rollbackTransaction(this.currentTxnId),this.currentTxnId=null,this.txnSnapshot=null,e}}async commitTransaction(){if(!this.currentTxnId)throw new n("No active transaction","TX_NONE");if(await this.wal.append({type:K.COMMIT,txnId:this.currentTxnId,tableName:"",key:""}),await this.wal.flush(),this.txnSnapshot)for(const[e,t]of this.txnSnapshot)t.__txn_deleted?this.lsm.delete(e):this.lsm.put(e,t);this.mvcc.commitTransaction(this.currentTxnId),this.currentTxnId=null,this.txnSnapshot=null}async rollbackTransaction(){if(!this.currentTxnId)throw new n("No active transaction","TX_NONE");const e=this.currentTxnId;await this.wal.append({type:K.ROLLBACK,txnId:e,tableName:"",key:""});const t=new Set;if(this.txnSnapshot)for(const e of this.txnSnapshot.keys()){const s=e.indexOf(":");s>0&&t.add(e.slice(0,s))}this.mvcc.rollbackTransaction(e),this.txnSnapshot=null,this.currentTxnId=null;for(const e of t)this.schemas.has(e)&&await this.reindexTable(e)}async savepoint(e){if(!this.currentTxnId)throw new n("No active transaction for savepoint","TX_NONE");if(this.savepoints.has(e))throw new n(`Savepoint "${e}" already exists`,"SAVEPOINT_EXISTS");this.savepoints.set(e,{txnId:this.currentTxnId,snapshot:this.txnSnapshot?new Map(this.txnSnapshot):null})}async rollbackToSavepoint(e){const t=this.savepoints.get(e);if(!t)throw new n(`Savepoint "${e}" not found`,"SAVEPOINT_NOT_FOUND");const s=new Set;if(this.txnSnapshot)for(const e of this.txnSnapshot.keys()){const t=e.indexOf(":");t>0&&s.add(e.slice(0,t))}this.txnSnapshot=t.snapshot?new Map(t.snapshot):null,this.mvcc.discardVersions(this.currentTxnId);let i=!1;for(const[t]of this.savepoints)t!==e?i&&this.savepoints.delete(t):i=!0;for(const e of s)this.schemas.has(e)&&await this.reindexTable(e)}async releaseSavepoint(e){if(!this.savepoints.has(e))throw new n(`Savepoint "${e}" not found`,"SAVEPOINT_NOT_FOUND");this.savepoints.delete(e)}async backup(){this.ensureOpen();const e={};for(const t of this.schemas.keys())e[t]=await this.getAllRows(t);return e}async getAllRows(e){const t=this.tablePKs.get(e),s=`${e}:`;await this.lsm.prefetchRange(s,`${s}￿`);const n=this.lsm.rangeScan(s,`${s}￿`).map(([e,n])=>{const i={...n};return i[t]=e.slice(s.length),i});return this.mergeTxnSnapshot(e,n)}mergeTxnSnapshot(e,t){if(!this.currentTxnId||!this.txnSnapshot)return t;const s=this.tablePKs.get(e),n=`${e}:`;for(const[e,i]of this.txnSnapshot){if(!e.startsWith(n))continue;const r=e.slice(n.length),a=i.__txn_deleted,o=t.findIndex(e=>e[s]===r);if(a)o>=0&&t.splice(o,1);else{const e={...i,[s]:r};o>=0?t[o]=e:t.push(e)}}return t}getPK(e){for(const[t,s]of Object.entries(e.columns))if(s.primaryKey)return t;return Object.keys(e.columns)[0]}validateRow(e,t){const s={};for(const[i,r]of Object.entries(e.columns)){let a=t[i];if(void 0===a&&void 0!==r.default&&(a=r.default),r.required&&null==a)throw new n(`Column "${i}" is required in table "${e.name}"`,"VALIDATION_ERROR");if(r.primaryKey&&null==a)throw new n(`Primary key column "${i}" in table "${e.name}" cannot be null or undefined`,"VALIDATION_ERROR");null!=a&&this.checkType(i,r.type,a,r),void 0!==a&&(s[i]=a)}return s}checkType(e,t,s,i){!function(e,t,s,i,r){const a=typeof i;switch(s){case"string":if("string"!==a)throw new n(`Column "${t}" in table "" expects string, got ${a}`,"TYPE_ERROR");if(void 0!==r?.maxLength&&i.length>r.maxLength)throw new n(`Column "${t}" in table "" exceeds max length ${r.maxLength}`,"VALIDATION_ERROR");break;case"number":if("number"!==a)throw new n(`Column "${t}" in table "" expects number, got ${a}`,"TYPE_ERROR");if(void 0!==r?.min&&ir.max)throw new n(`Column "${t}" in table "" value ${i} above maximum ${r.max}`,"VALIDATION_ERROR");break;case"boolean":if("boolean"!==a)throw new n(`Column "${t}" in table "" expects boolean, got ${a}`,"TYPE_ERROR");break;case"date":if("string"!==a||isNaN(Date.parse(i)))throw new n(`Column "${t}" in table "" expects valid date string, got ${typeof i}`,"TYPE_ERROR");break;case"json":if("object"!==a)throw new n(`Column "${t}" in table "" expects object/array, got ${a}`,"TYPE_ERROR")}}(0,e,t,s,i)}async persistSchemas(){const e={};for(const[t,s]of this.schemas)e[t]=s.columns;const t=JSON.stringify(e),s=(new TextEncoder).encode(t).buffer;await this.backend.write("__aria_schemas",s)}async loadSchemas(){const e=await this.backend.read("__aria_schemas");if(e)try{const t=(new TextDecoder).decode(e),s=JSON.parse(t);for(const[e,t]of Object.entries(s)){const s={name:e,columns:t};this.schemas.set(e,s),this.tablePKs.set(e,this.getPK(s))}}catch{}}createSSTableStore(e){const t="main"===e?"sst_":`sst_${e}_`,s="main"===e?"__aria_lsm_meta":`__aria_lsm_meta_${e}`;let n=0,i=!1;const r=this.isPageStorage()?new be(this.fileManager,this.bufferPool):null,a=e=>(new TextEncoder).encode(e).buffer,o=async()=>{const e=await this.backend.read(s);if(!e)return[];try{return JSON.parse((new TextDecoder).decode(e))}catch{return[]}};return{save:async(e,s)=>{if(r)return void await r.save(e,s);let n=s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength);if(this.config.compression){const e=function(e){if(0===e.byteLength){const e=new Uint8Array(4);return new DataView(e.buffer).setUint32(0,0,!0),e}const t=e.byteLength+Math.ceil(e.byteLength/15)+8,s=new Uint8Array(t);let n=0,i=0,r=0;for(;n=4&&i>t&&(t=i,a=n-s)}if(t>4&&n-r<=15){const o=n-r,h=t-4;s[i++]=(15&o)<<4|15&h;for(let t=0;t>8&255,n+=t,r=n}else if(n++,n-r>=15){s[i++]=240;for(let t=0;t<15;t++)s[i++]=e[r+t];r=n}}let a=n-r;for(;a>0;){const t=Math.min(a,15);s[i++]=(15&t)<<4;for(let n=0;n{if(r){const t=(await o()).find(t=>t.id===e);if(t&&t.pageIds&&t.pageIds.length>0)return r.load(e,t.pageIds,t.totalSize)}const s=await this.backend.read(`${t}${e}`);if(!s)return null;let n=new Uint8Array(s);return this.config.compression&&(n=function(e){if(e.byteLength<4)throw new Error("LZ4 stream too short: missing header");const t=new DataView(e.buffer,e.byteOffset,e.byteLength).getUint32(0,!0);if(0===t&&4===e.byteLength)return new Uint8Array(0);if(t<=0||t>1073741823)throw new Error("Invalid LZ4 header: bad original size");const s=e.subarray(4),n=new Uint8Array(t);let i=0,r=0;for(;i>4&15,o=15&e;for(let e=0;e=s.byteLength)break;const h=s[i++]|s[i++]<<8,c=o+4;for(let e=0;e{if(r){const t=(await o()).find(t=>t.id===e);t&&t.pageIds&&t.pageIds.length>0&&await r.delete(e,t.pageIds)}await this.backend.delete(`${t}${e}`)},allocateId:async()=>{if(!i){const e=await o();n=e.reduce((e,t)=>Math.max(e,t.id),0),i=!0}return++n},listMeta:o,saveMeta:async e=>{const t=await o(),n=r?.getPageIds(e.id),i=n&&n.length>0?{...e,pageIds:n}:e,h=t.findIndex(t=>t.id===e.id);h>=0?t[h]=i:t.push(i),await this.backend.write(s,a(JSON.stringify(t)))},deleteMeta:async e=>{const t=(await o()).filter(t=>t.id!==e);await this.backend.write(s,a(JSON.stringify(t)))}}}isPageStorage(){return!0===this.config.pageStorage||!1!==this.config.pageStorage&&("opfs"===this.config.storageBackend||"kv"===this.config.storageBackend)}applyWALRecord(e){switch(e.type){case K.INSERT:case K.UPDATE:e.data&&this.lsm.put(`${e.tableName}:${e.key}`,e.data);break;case K.DELETE:this.lsm.delete(`${e.tableName}:${e.key}`);break;case K.CREATE_TABLE:if(e.data?.schema)try{const t=JSON.parse(e.data.schema);this.schemas.has(t.name)||(this.schemas.set(t.name,t),this.tablePKs.set(t.name,this.getPK(t)))}catch{}case K.COMMIT:case K.ROLLBACK:case K.BEGIN:}}async applyDropTableRecovery(e){if(!e)return;await this.cleanupTableIndexes(e),this.schemas.delete(e),this.tablePKs.delete(e);const t=`${e}:`,s=`${t}￿`;await this.lsm.prefetchRange(t,s);const n=this.lsm.rangeScan(t,s);for(const[e]of n)this.lsm.delete(e)}uniqueColumns(e,t){const s=[];for(const[n,i]of Object.entries(t.columns))i.unique&&this.secondaryIndexes.has(`${e}:idx:${n}`)&&s.push(n);return s}checkUniqueSync(e,t,s,i){for(const r of t){const t=s[r];if(null==t)continue;const a=this.secondaryIndexes.get(`${e}:idx:${r}`);if(!a)continue;const o=`${String(t)}:`,h=a.rangeScan(o,`${o}￿`);for(const[,t]of h){const s=t.pk;if(void 0!==s&&s!==i)throw new n(`Unique constraint violation on column "${r}" in table "${e}"`,"UNIQUE_VIOLATION")}}}updateSecondaryIndexes(e,t,s,n){const i=this.schemas.get(e);if(i)for(const[r,a]of Object.entries(i.columns)){if(!a.index&&!a.unique)continue;const i=`${e}:idx:${r}`,o=this.secondaryIndexes.get(i);if(o){if(n){const e=n[r];null!=e&&o.delete(`${String(e)}:${t}`)}if(s){const e=s[r];null!=e&&o.put(`${String(e)}:${t}`,{pk:t})}}}}async tryIndexLookup(e,t){if(!t.where)return null;const s=this.schemas.get(e);if(!s)return null;const n=this.tablePKs.get(e),i=[],r=e=>{for(const[t,s]of Object.entries(e))if("$and"!==t)"$or"!==t&&"$not"!==t&&"$exists"!==t&&i.push([t,s]);else for(const e of s)r(e)};r(t.where);for(const[t,r]of i){const i=s.columns[t];if(!(i&&(i.index||i.unique||i.primaryKey)||t===n))continue;if(t===n){if("object"!=typeof r||null===r){const t=`${e}:${r}`;await this.lsm.prefetchKeys([t]);const s=this.lsm.get(t);return s?[{...s,[n]:r}]:[]}const t=r;if("$eq"in t){const s=`${e}:${t.$eq}`;await this.lsm.prefetchKeys([s]);const i=this.lsm.get(s);return i?[{...i,[n]:t.$eq}]:[]}if("$in"in t&&Array.isArray(t.$in)){const s=t.$in.map(t=>`${e}:${t}`);await this.lsm.prefetchKeys(s);const i=[],r=new Set;for(const s of t.$in){const t=String(s);if(r.has(t))continue;const a=this.lsm.get(`${e}:${t}`);a&&(r.add(t),i.push({...a,[n]:t}))}return i}if("$gt"in t||"$gte"in t||"$lt"in t||"$lte"in t){const t=`${e}:`;await this.lsm.prefetchRange(t,`${t}￿`);const s=this.lsm.rangeScan(t,`${t}￿`),i=[];for(const[e,a]of s){const s={...a,[n]:e.slice(t.length)};o(s,{[n]:r})&&i.push(s)}return i}}const a=`${e}:idx:${t}`,h=this.secondaryIndexes.get(a);if(!h)continue;if("object"!=typeof r||null===r){if(null===r)continue;return this.indexScanToRows(e,n,h,String(r),String(r))}const c=r;if("$eq"in c){if(null===c.$eq)continue;const t=String(c.$eq);return this.indexScanToRows(e,n,h,t,t)}if("$in"in c&&Array.isArray(c.$in)){if(c.$in.some(e=>null===e))continue;const t=c.$in.map(e=>String(e));await h.prefetchPrefixRanges(t.map(e=>[e,`${e}￿`]));const s=[],i=new Set,r=[];for(const e of t){const t=h.rangeScan(e,`${e}￿`);for(const[,e]of t){const t=e.pk;t&&!i.has(t)&&(i.add(t),r.push(t))}}await this.lsm.prefetchKeys(r.map(t=>`${e}:${t}`));for(const t of r){const i=this.lsm.get(`${e}:${t}`);i&&s.push({...i,[n]:t})}return s}if("$gt"in c||"$gte"in c||"$lt"in c||"$lte"in c)return(await this.indexScanToRows(e,n,h,"","￿")).filter(e=>o(e,{[t]:r}))}return null}async indexScanToRows(e,t,s,n,i){const r=i.includes("￿")?i:`${i}￿`;await s.prefetchRange(n,r);const a=s.rangeScan(n,r),o=[];for(const[,e]of a){const t=e.pk;t&&o.push(t)}await this.lsm.prefetchKeys(o.map(t=>`${e}:${t}`));const h=[];for(const s of o){const n=this.lsm.get(`${e}:${s}`);n&&h.push({...n,[t]:s})}return h}tryGC(){this.gcCounter++,this.gcCounter>=10&&(this.mvcc.gc(100),this.gcCounter=0)}trimAllCaches(){this.lsm.trimCache();for(const e of this.secondaryIndexes.values())e.trimCache()}checkMemoryBudget(){const e=1024*this.config.maxMemoryMB*1024;this.lsm.getEstimatedMemory()>e&&(this.lsm.flush().catch(()=>{}),this.mvcc.gc(50))}async analyzeTable(e){this.ensureOpen(),this.ensureTable(e);const t=await this.getAllRows(e);let s=this.lsm.getStats().sstableCount,n=this.lsm.getStats().memtableSize,i=this.lsm.getStats().levelCounts.filter(e=>e>0).length;for(const[t,r]of this.secondaryIndexes){if(!t.startsWith(`${e}:idx:`))continue;const a=r.getStats();s+=a.sstableCount,n+=a.memtableSize,i=Math.max(i,a.levelCounts.filter(e=>e>0).length)}const r={table:e,rowCount:t.length,avgRowSize:t.length>0?Math.round(t.reduce((e,t)=>e+JSON.stringify(t).length,0)/t.length):0,indexDepth:i,sstableCount:s,memtableSize:n,estimatedMemory:this.lsm.getEstimatedMemory()},a=this.schemas.get(e);if(a&&t.length>0){const e={};for(const s of Object.keys(a.columns)){const n=new Set(t.map(e=>String(e[s])));e[s]={distinctValues:n.size}}r.columnStats=e}return r}async reindexTable(e){return this.ensureOpen(),this.ensureTable(e),this.reindexTableInternal(e)}async reindexTableInternal(e){const t=this.schemas.get(e);if(!t)return 0;let s=0;const n=this.tablePKs.get(e),i=Object.entries(t.columns).filter(([,e])=>e.index||e.unique);if(0===i.length)return 0;const r=await this.getAllRows(e);for(const[t]of i){const i=`${e}:idx:${t}`,a=this.secondaryIndexes.get(i);if(a){await a.clear(),s++;for(const e of r){const s=e[t];null!=s&&a.put(`${String(s)}:${e[n]}`,{pk:e[n]})}}}return s}async vacuum(){this.ensureOpen(),await this.lsm.flush();for(let e=0;e<6;e++)this.lsm.getStats().levelCounts[e]>=2&&await this.lsm.compactLevel(e);const e=this.mvcc.getGlobalLSN();return this.mvcc.gc(10),{compactedLevels:6,gcVersions:e}}ensureOpen(){if(!this.opened)throw new n("AriaEngine not opened","DB_NOT_OPEN")}ensureNoDDLInTransaction(e){if(this.currentTxnId)throw new n(`${e} is not supported inside a transaction (AriaEngine DDL is not transactional)`,"NOT_SUPPORTED")}ensureTable(e){if(!this.schemas.has(e))throw new n(`Table "${e}" does not exist`,"TABLE_NOT_FOUND")}}class Re{constructor(e="opfs"){this.name="hybrid",this.dbName="",this.version=1,this.memoryEngine=new m,this.diskEngineType=e,this.diskEngine=new _}async open(e,t){this.dbName=e,this.version=t,await this.diskEngine.open(e,t),await this.memoryEngine.open(e,t),await this.reloadMemoryFromDisk()}async reloadMemoryFromDisk(){await this.memoryEngine.close(),await this.memoryEngine.open(this.dbName,this.version);const e=this.diskEngine;"function"==typeof e.reload&&await e.reload();const t=await this.diskEngine.getTableNames();for(const e of t){const t=await this.diskEngine.getTableSchema(e);if(!t)continue;await this.memoryEngine.createTable(t);const s=await this.diskEngine.find(e,{table:e});if(s.length>0)try{await this.memoryEngine.insert(e,s)}catch(e){}}}async close(){await this.memoryEngine.close(),await this.diskEngine.close()}isOpen(){return this.memoryEngine.isOpen()&&this.diskEngine.isOpen()}async repair(){"function"==typeof this.diskEngine.repair&&await this.diskEngine.repair(),await this.reloadMemoryFromDisk()}async clearAll(){if("function"==typeof this.diskEngine.clearAll)await this.diskEngine.clearAll();else{const e=await this.diskEngine.getTableNames();for(const t of e)await this.diskEngine.dropTable(t)}if("function"==typeof this.memoryEngine.clearAll)await this.memoryEngine.clearAll();else{const e=await this.memoryEngine.getTableNames();for(const t of e)await this.memoryEngine.dropTable(t)}}async getMeta(e){return"function"==typeof this.diskEngine.getMeta?this.diskEngine.getMeta(e):null}async setMeta(e,t){"function"==typeof this.diskEngine.setMeta&&await this.diskEngine.setMeta(e,t)}async createTable(e){await this.memoryEngine.createTable(e);try{await this.diskEngine.createTable(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async dropTable(e){await this.memoryEngine.dropTable(e);try{await this.diskEngine.dropTable(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async hasTable(e){return this.memoryEngine.hasTable(e)}async getTableNames(){return this.memoryEngine.getTableNames()}async getTableSchema(e){return this.memoryEngine.getTableSchema(e)}async alterTable(e,t,s){await this.memoryEngine.alterTable(e,t,s);try{if("function"==typeof this.diskEngine.alterTable)await this.diskEngine.alterTable(e,t,s);else{const n=await this.diskEngine.getTableSchema(e);n&&"DROP"===t&&delete n.columns[s.name]}}catch(e){await this.recoverMemoryAfterDiskError(e)}}async recoverMemoryAfterDiskError(e){try{await this.reloadMemoryFromDisk()}catch{}throw e}async insert(e,t){const s=await this.memoryEngine.insert(e,t);try{await this.diskEngine.insert(e,t)}catch(e){await this.recoverMemoryAfterDiskError(e)}return s}async find(e,t){return this.memoryEngine.find(e,t)}async findStream(e,t,s){return this.memoryEngine.findStream(e,t,s)}async update(e,t,s){const n=await this.memoryEngine.update(e,t,s);try{await this.diskEngine.update(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}return n}async delete(e,t){const s=await this.memoryEngine.delete(e,t);try{await this.diskEngine.delete(e,t)}catch(e){await this.recoverMemoryAfterDiskError(e)}return s}async count(e,t){return this.memoryEngine.count(e,t)}async clear(e){await this.memoryEngine.clear(e);try{await this.diskEngine.clear(e)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async createIndex(e,t,s){await this.memoryEngine.createIndex(e,t,s);try{"function"==typeof this.diskEngine.createIndex&&await this.diskEngine.createIndex(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async dropIndex(e,t,s){await this.memoryEngine.dropIndex(e,t,s);try{"function"==typeof this.diskEngine.dropIndex&&await this.diskEngine.dropIndex(e,t,s)}catch(e){await this.recoverMemoryAfterDiskError(e)}}async beginTransaction(){await this.memoryEngine.beginTransaction();try{await this.diskEngine.beginTransaction()}catch(e){throw await this.memoryEngine.rollbackTransaction(),e}}async commitTransaction(){await this.diskEngine.commitTransaction();try{await this.memoryEngine.commitTransaction()}catch(e){throw new n("Hybrid commit failed: memory engine error after disk commit (disk data is committed)","TX_COMMIT_ERROR",e)}}async rollbackTransaction(){await this.memoryEngine.rollbackTransaction(),await this.diskEngine.rollbackTransaction()}getDiskEngineType(){return this.diskEngineType}getMemoryEngine(){return this.memoryEngine}}class Ce{constructor(e,t,s=["*"],n){this.engine=e,this.tableName=t,this._columns=s,this._where={},this._orderBy=[],this._joins=[],this._executor=n}as(e){return this._alias=e,this}innerJoin(e,t,s){return this._addJoin("INNER",e,t,s)}leftJoin(e,t,s){return this._addJoin("LEFT",e,t,s)}rightJoin(e,t,s){return this._addJoin("RIGHT",e,t,s)}crossJoin(e,t){return this._addJoin("CROSS",e,{},t)}join(e,t,s){return this._addJoin("INNER",e,t,s)}_addJoin(e,t,s,n){return this._joins.push({type:e,table:t,on:s,alias:n}),this}where(e){return this._where={...this._where,...e},this}orderBy(e,t="asc"){return this._orderBy.push({column:e,direction:t}),this}limit(e){return this._limit=e,this}offset(e){return this._offset=e,this}async execute(){if(this._joins.length>0&&this._executor){const e=this.toAST();return this._executor.execute(e)}return this.engine.find(this.tableName,{table:this.tableName,columns:this._columns,where:this._where,orderBy:this._orderBy.length>0?this._orderBy:void 0,limit:this._limit,offset:this._offset})}toAST(){return{type:"SELECT",columns:this._columns,from:this.tableName,alias:this._alias,joins:this._joins.length>0?[...this._joins]:void 0,where:this._where,orderBy:this._orderBy.length>0?this._orderBy:void 0,limit:this._limit,offset:this._offset}}}class Oe{constructor(e,t,s,n,i){this.engine=e,this.tableName=t,this._updates=s,this._where={},this.onWrite=n,this.onHooks=i}where(e){return this._where={...this._where,...e},this}async execute(){const e={table:this.tableName,where:this._where};await(this.onHooks?.("beforeUpdate",[e,this._updates]));const t=await this.engine.update(this.tableName,e,this._updates);return this.onWrite?.(this.tableName),await(this.onHooks?.("afterUpdate",[e,this._updates,t])),t}toAST(){return{type:"UPDATE",table:this.tableName,sets:this._updates,where:this._where}}}class Ne{constructor(e,t,s,n){this.engine=e,this.tableName=t,this._where={},this.onWrite=s,this.onHooks=n}where(e){return this._where={...this._where,...e},this}async execute(){const e={table:this.tableName,where:this._where};await(this.onHooks?.("beforeDelete",[e]));const t=await this.engine.delete(this.tableName,e);return this.onWrite?.(this.tableName),await(this.onHooks?.("afterDelete",[e,t])),t}toAST(){return{type:"DELETE",from:this.tableName,where:this._where}}}class Le{constructor(e,t,s,n,i){this.schema=null,this.engine=e,this.name=t,this.executor=s,this.onWrite=n,this.onHooks=i}async getSchema(){if(!this.schema){const e=await this.engine.getTableSchema(this.name);if(!e)throw new n(`Table "${this.name}" does not exist`,"TABLE_NOT_FOUND");this.schema=e}return this.schema}async insert(e){await(this.onHooks?.("beforeInsert",[[e]]));const t=await this.engine.insert(this.name,[e]);return this.onWrite?.(this.name),await(this.onHooks?.("afterInsert",[[e],t])),t[0]}async insertMany(e){await(this.onHooks?.("beforeInsert",[e]));const t=await this.engine.insert(this.name,e);return this.onWrite?.(this.name),await(this.onHooks?.("afterInsert",[e,t])),t}select(e=["*"]){return new Ce(this.engine,this.name,e,this.executor)}async stream(e,t={}){if("function"!=typeof this.engine.findStream){const s=await this.engine.find(this.name,{table:this.name,where:t.where,limit:t.limit,offset:t.offset,columns:t.columns});for(const t of s)e(t);return s.length}return this.engine.findStream(this.name,{table:this.name,where:t.where,limit:t.limit,offset:t.offset,columns:t.columns},e)}update(e){return new Oe(this.engine,this.name,e,this.onWrite,this.onHooks)}delete(){return new Ne(this.engine,this.name,this.onWrite,this.onHooks)}async count(e){return this.engine.count(this.name,e?{table:this.name,where:e}:void 0)}async clear(){await this.engine.clear(this.name),this.onWrite?.(this.name)}async drop(){await this.engine.dropTable(this.name),this.onWrite?.(this.name)}}function ve(e){switch(e.type){case"SELECT":return function(e){return{table:e.from,columns:e.columns,where:e.where,orderBy:e.orderBy?.length?e.orderBy:void 0,limit:e.limit,offset:e.offset}}(e);case"DELETE":return function(e){return{table:e.from,where:e.where}}(e);case"UPDATE":return function(e){return{table:e.table,where:e.where}}(e);default:throw new n(`Cannot compile statement type "${e.type}" to QueryPlan`,"COMPILE_ERROR")}}var $e;!function(e){e.SELECT="SELECT",e.FROM="FROM",e.WHERE="WHERE",e.INSERT="INSERT",e.INTO="INTO",e.VALUES="VALUES",e.UPDATE="UPDATE",e.SET="SET",e.DELETE="DELETE",e.CREATE="CREATE",e.TABLE="TABLE",e.DROP="DROP",e.ORDER="ORDER",e.BY="BY",e.ASC="ASC",e.DESC="DESC",e.LIMIT="LIMIT",e.OFFSET="OFFSET",e.AND="AND",e.OR="OR",e.NOT="NOT",e.LIKE="LIKE",e.IN="IN",e.PRIMARY="PRIMARY",e.KEY="KEY",e.UNIQUE="UNIQUE",e.DEFAULT="DEFAULT",e.NULL="NULL",e.TRUE="TRUE",e.REFERENCES="REFERENCES",e.CASCADE="CASCADE",e.BETWEEN="BETWEEN",e.IF="IF",e.EXISTS="EXISTS",e.FALSE="FALSE",e.ALTER="ALTER",e.ADD="ADD",e.TRUNCATE="TRUNCATE",e.INNER="INNER",e.LEFT="LEFT",e.RIGHT="RIGHT",e.CROSS="CROSS",e.JOIN="JOIN",e.ON="ON",e.AS="AS",e.OUTER="OUTER",e.GROUP="GROUP",e.HAVING="HAVING",e.COUNT="COUNT",e.SUM="SUM",e.AVG="AVG",e.MIN="MIN",e.MAX="MAX",e.DISTINCT="DISTINCT",e.BEGIN="BEGIN",e.COMMIT="COMMIT",e.ROLLBACK="ROLLBACK",e.UNION="UNION",e.ALL="ALL",e.INDEX="INDEX",e.CASE="CASE",e.WHEN="WHEN",e.THEN="THEN",e.ELSE="ELSE",e.END="END",e.EXPLAIN="EXPLAIN",e.ANALYZE="ANALYZE",e.REINDEX="REINDEX",e.VACUUM="VACUUM",e.SAVEPOINT="SAVEPOINT",e.RELEASE="RELEASE",e.TO="TO",e.IDENTIFIER="IDENTIFIER",e.STRING="STRING",e.NUMBER="NUMBER",e.COMMA="COMMA",e.LPAREN="LPAREN",e.RPAREN="RPAREN",e.SEMICOLON="SEMICOLON",e.EQ="EQ",e.NEQ="NEQ",e.GT="GT",e.GTE="GTE",e.LT="LT",e.LTE="LTE",e.STAR="STAR",e.DOT="DOT",e.EOF="EOF",e.ILLEGAL="ILLEGAL"}($e||($e={}));const De={SELECT:$e.SELECT,FROM:$e.FROM,WHERE:$e.WHERE,INSERT:$e.INSERT,INTO:$e.INTO,VALUES:$e.VALUES,UPDATE:$e.UPDATE,SET:$e.SET,DELETE:$e.DELETE,CREATE:$e.CREATE,TABLE:$e.TABLE,DROP:$e.DROP,ORDER:$e.ORDER,BY:$e.BY,ASC:$e.ASC,DESC:$e.DESC,LIMIT:$e.LIMIT,OFFSET:$e.OFFSET,AND:$e.AND,OR:$e.OR,NOT:$e.NOT,LIKE:$e.LIKE,IN:$e.IN,PRIMARY:$e.PRIMARY,KEY:$e.KEY,UNIQUE:$e.UNIQUE,DEFAULT:$e.DEFAULT,NULL:$e.NULL,TRUE:$e.TRUE,FALSE:$e.FALSE,REFERENCES:$e.REFERENCES,CASCADE:$e.CASCADE,BETWEEN:$e.BETWEEN,IF:$e.IF,EXISTS:$e.EXISTS,ALTER:$e.ALTER,ADD:$e.ADD,TRUNCATE:$e.TRUNCATE,INNER:$e.INNER,LEFT:$e.LEFT,RIGHT:$e.RIGHT,CROSS:$e.CROSS,JOIN:$e.JOIN,ON:$e.ON,AS:$e.AS,OUTER:$e.OUTER,GROUP:$e.GROUP,HAVING:$e.HAVING,COUNT:$e.COUNT,SUM:$e.SUM,AVG:$e.AVG,MIN:$e.MIN,MAX:$e.MAX,DISTINCT:$e.DISTINCT,BEGIN:$e.BEGIN,COMMIT:$e.COMMIT,ROLLBACK:$e.ROLLBACK,UNION:$e.UNION,ALL:$e.ALL,INDEX:$e.INDEX,CASE:$e.CASE,WHEN:$e.WHEN,THEN:$e.THEN,ELSE:$e.ELSE,END:$e.END,EXPLAIN:$e.EXPLAIN,ANALYZE:$e.ANALYZE,REINDEX:$e.REINDEX,VACUUM:$e.VACUUM,SAVEPOINT:$e.SAVEPOINT,RELEASE:$e.RELEASE,TO:$e.TO};class Ue{constructor(e){this.position=0,this.readPosition=0,this.ch="",this.input=e,this.readChar()}nextToken(){let e;switch(this.skipWhitespace(),this.ch){case",":e=this.makeToken($e.COMMA,",");break;case"(":e=this.makeToken($e.LPAREN,"(");break;case")":e=this.makeToken($e.RPAREN,")");break;case";":e=this.makeToken($e.SEMICOLON,";");break;case"*":e=this.makeToken($e.STAR,"*");break;case".":e=this.makeToken($e.DOT,".");break;case"=":e=this.makeToken($e.EQ,"=");break;case"!":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.NEQ,"!=")):e=this.makeToken($e.ILLEGAL,"!");break;case">":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.GTE,">=")):e=this.makeToken($e.GT,">");break;case"<":"="===this.peekChar()?(this.readChar(),e=this.makeToken($e.LTE,"<=")):">"===this.peekChar()?(this.readChar(),e=this.makeToken($e.NEQ,"<>")):e=this.makeToken($e.LT,"<");break;case"'":case'"':e=this.readString(this.ch);break;case"":e={type:$e.EOF,value:"",position:this.position};break;default:if("-"===this.ch&&"-"===this.peekChar())return this.skipLineComment(),this.nextToken();if("/"===this.ch&&"*"===this.peekChar())return this.skipBlockComment(),this.nextToken();if(this.isLetter(this.ch)){const t=this.readIdentifier();return e={type:De[t.toUpperCase()]??$e.IDENTIFIER,value:t,position:this.position-t.length},e}if(this.isDigit(this.ch)||"-"===this.ch&&this.isDigit(this.peekChar())){const t=this.readNumber();return e={type:$e.NUMBER,value:t,position:this.position-t.length},e}e=this.makeToken($e.ILLEGAL,this.ch)}return this.readChar(),e}readChar(){this.readPosition>=this.input.length?this.ch="":this.ch=this.input[this.readPosition],this.position=this.readPosition,this.readPosition++}peekChar(){return this.readPosition>=this.input.length?"":this.input[this.readPosition]}skipWhitespace(){for(;" "===this.ch||"\t"===this.ch||"\n"===this.ch||"\r"===this.ch;)this.readChar()}skipLineComment(){for(;"\n"!==this.ch&&"\r"!==this.ch&&""!==this.ch;)this.readChar()}skipBlockComment(){for(this.readChar(),this.readChar();""!==this.ch&&("*"!==this.ch||"/"!==this.peekChar());)this.readChar();""!==this.ch&&(this.readChar(),this.readChar())}readIdentifier(){const e=this.position;for(;this.isLetter(this.ch)||this.isDigit(this.ch)||"_"===this.ch;)this.readChar();return this.input.slice(e,this.position)}readNumber(){const e=this.position;for("-"===this.ch&&this.readChar();this.isDigit(this.ch);)this.readChar();if("."===this.ch&&this.isDigit(this.peekChar()))for(this.readChar();this.isDigit(this.ch);)this.readChar();return this.input.slice(e,this.position)}readString(e){const t=this.position+1;this.readChar();let s="";for(;""!==this.ch;){if(this.ch===e){if(this.peekChar()===e){s+=e,this.readChar(),this.readChar();continue}break}"\\"!==this.ch||this.peekChar()!==e?(s+=this.ch,this.readChar()):(this.readChar(),s+=e,this.readChar())}if(""===this.ch)throw new n(`Unterminated string literal at position ${t}`,"PARSE_ERROR");return{type:$e.STRING,value:s,position:t}}isLetter(e){return/[a-zA-Z_]/.test(e)}isDigit(e){return/[0-9]/.test(e)}makeToken(e,t){return{type:e,value:t,position:this.position}}}class _e{constructor(e){this.sql=e,this.lexer=new Ue(e),this.nextToken(),this.nextToken()}parseStatement(){switch(this.curToken.type){case $e.SELECT:return this.parseSelect();case $e.INSERT:return this.parseInsert();case $e.UPDATE:return this.parseUpdate();case $e.DELETE:return this.parseDelete();case $e.CREATE:return this.parseCreateStatement();case $e.DROP:return this.parseDropStatement();case $e.ALTER:return this.parseAlterTable();case $e.TRUNCATE:return this.parseTruncateTable();case $e.BEGIN:return this.parseBegin();case $e.COMMIT:return this.parseCommit();case $e.ROLLBACK:return this.parseRollback();case $e.EXPLAIN:return this.parseExplain();case $e.ANALYZE:return this.parseAnalyze();case $e.REINDEX:return this.parseReindex();case $e.VACUUM:return this.parseVacuum();case $e.SAVEPOINT:case $e.RELEASE:return this.parseSavepoint();default:throw this.error(`Unexpected token "${this.curToken.value}"`)}}parseAllStatements(){const e=[];for(;!this.curTokenIs($e.EOF);){for(;this.curTokenIs($e.SEMICOLON);)this.nextToken();if(this.curTokenIs($e.EOF))break;if(e.push(this.parseStatement()),this.curTokenIs($e.SEMICOLON))this.nextToken();else if(!this.curTokenIs($e.EOF))throw this.error(`Expected ';' after statement, got "${this.curToken.value}"`)}return e}parseExplain(){if(this.expect($e.EXPLAIN),this.curTokenIs($e.EXPLAIN))throw this.error("Nested EXPLAIN is not allowed");return{type:"EXPLAIN",query:this.parseStatement()}}parseAnalyze(){return this.expect($e.ANALYZE),this._isKeywordAsIdent()&&"TABLE"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ANALYZE",table:this.expectIdentifier("table name")}}parseReindex(){return this.expect($e.REINDEX),this._isKeywordAsIdent()&&"TABLE"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"REINDEX",table:this.expectIdentifier("table name")}}parseVacuum(){return this.expect($e.VACUUM),{type:"VACUUM"}}parseSavepoint(){let e;return this.curTokenIs($e.RELEASE)?(e="RELEASE",this.nextToken()):(e="SAVE",this.expect($e.SAVEPOINT)),this.curTokenIs($e.SAVEPOINT)&&this.nextToken(),{type:"SAVEPOINT",name:this.expectIdentifier("savepoint name"),action:e}}parseBegin(){return this.expect($e.BEGIN),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"BEGIN"}}parseCommit(){return this.expect($e.COMMIT),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"COMMIT"}}parseRollback(){return this.expect($e.ROLLBACK),this._isKeywordAsIdent()&&"TRANSACTION"===this.curToken.value.toUpperCase()?(this.nextToken(),{type:"ROLLBACK"}):this.curTokenIs($e.TO)||this._isKeywordAsIdent()&&"TO"===this.curToken.value.toUpperCase()?(this.nextToken(),(this.curTokenIs($e.SAVEPOINT)||this._isKeywordAsIdent()&&"SAVEPOINT"===this.curToken.value.toUpperCase())&&this.nextToken(),{type:"SAVEPOINT",name:this.expectIdentifier("savepoint name"),action:"ROLLBACK"}):{type:"ROLLBACK"}}parseCreateStatement(){if(this.expect($e.CREATE),this.curTokenIs($e.TABLE))return this.parseCreateTable();if(this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())return this.parseCreateIndex();if(this.curTokenIs($e.UNIQUE)&&(this.nextToken(),this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())){const e=this.parseCreateIndex();return e.unique=!0,e}throw this.error(`Expected TABLE or INDEX after CREATE, got "${this.curToken.value}"`)}parseCreateIndex(){this.expect($e.INDEX);const e=this.expectIdentifier("index name");this.expect($e.ON);const t=this.expectIdentifier("table name");this.expect($e.LPAREN);const s=this.expectIdentifier("column name");return this.expect($e.RPAREN),{type:"CREATE_INDEX",name:e,table:t,column:s}}parseDropStatement(){if(this.expect($e.DROP),this.curTokenIs($e.TABLE))return this.parseDropTable();if(this.curTokenIs($e.INDEX)||this._isKeywordAsIdent()&&"INDEX"===this.curToken.value.toUpperCase())return this.parseDropIndex();throw this.error(`Expected TABLE or INDEX after DROP, got "${this.curToken.value}"`)}parseDropIndex(){this.expect($e.INDEX);const e=this.expectIdentifier("index name");let t="",s="";return this.curTokenIs($e.ON)&&(this.nextToken(),t=this.expectIdentifier("table name"),this.curTokenIs($e.LPAREN)&&(this.nextToken(),s=this.expectIdentifier("column name"),this.expect($e.RPAREN))),{type:"DROP_INDEX",name:e,table:t,column:s}}parseSelect(){this.expect($e.SELECT);let e=!1;this.curTokenIs($e.DISTINCT)&&(e=!0,this.nextToken());const t=[];if(this.curTokenIs($e.STAR))for(t.push("*"),this.nextToken();this.curTokenIs($e.COMMA);)this.nextToken(),t.push(this.parseColumnWithAlias());else t.push(...this.parseColumnList());let s,n,i="";this.curTokenIs($e.FROM)&&(this.nextToken(),this.curTokenIs($e.LPAREN)?(this.nextToken(),s=this.parseSelect(),this.expect($e.RPAREN),this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isReservedAfterFrom()||(n=this.curToken.value,this.nextToken())):(i=this.expectIdentifier("table name"),this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isReservedAfterFrom()||(n=this.curToken.value,this.nextToken())));const r={type:"SELECT",columns:t,distinct:e||void 0,from:i,alias:n,where:{}};s&&(r.fromSubquery=s);const a=this.parseJoinClauses();return a.length>0&&(r.joins=a),this.curTokenIs($e.WHERE)&&(this.nextToken(),r.where=this.parseCondition()),this.curTokenIs($e.GROUP)&&(this.nextToken(),this.expect($e.BY),r.groupBy=this.parseIdentifierList()),this.curTokenIs($e.HAVING)&&(this.nextToken(),r.having=this.parseCondition()),this.curTokenIs($e.ORDER)&&(this.nextToken(),this.expect($e.BY),r.orderBy=this.parseOrderByList()),this.curTokenIs($e.LIMIT)&&(this.nextToken(),r.limit=this.expectNumber("LIMIT value")),this.curTokenIs($e.OFFSET)&&(this.nextToken(),r.offset=this.expectNumber("OFFSET value")),this.curTokenIs($e.UNION)?this.parseUnion(r):r}parseUnion(e){this.expect($e.UNION);let t=!1;this.curTokenIs($e.ALL)&&(t=!0,this.nextToken());const s={type:"SELECT_UNION",left:e,right:this.parseSelect(),all:t||void 0};return this.curTokenIs($e.UNION)?this.parseUnionChain(s):s}parseUnionChain(e){this.expect($e.UNION);let t=!1;this.curTokenIs($e.ALL)&&(t=!0,this.nextToken());const s={type:"SELECT_UNION",left:e,right:this.parseSelect(),all:t||void 0};return this.curTokenIs($e.UNION)?this.parseUnionChain(s):s}parseJoinClauses(){const e=[];for(;this._isJoinKeyword();)e.push(this.parseJoinClause());return e}_isJoinKeyword(){return this.curTokenIs($e.INNER)||this.curTokenIs($e.LEFT)||this.curTokenIs($e.RIGHT)||this.curTokenIs($e.CROSS)||this.curTokenIs($e.JOIN)}parseJoinClause(){let e="INNER";this.curTokenIs($e.INNER)?(e="INNER",this.nextToken()):this.curTokenIs($e.LEFT)?(e="LEFT",this.nextToken(),this.curTokenIs($e.OUTER)&&this.nextToken()):this.curTokenIs($e.RIGHT)?(e="RIGHT",this.nextToken(),this.curTokenIs($e.OUTER)&&this.nextToken()):this.curTokenIs($e.CROSS)&&(e="CROSS",this.nextToken()),this.expect($e.JOIN);const t=this.expectIdentifier("table name");let s;this.curTokenIs($e.AS)?(this.nextToken(),s=this.expectIdentifier("alias")):this.curToken.type!==$e.IDENTIFIER||this._isJoinReserved()||(s=this.curToken.value,this.nextToken());let n={};return"CROSS"!==e&&this.curTokenIs($e.ON)&&(this.nextToken(),n=this.parseCondition()),{type:e,table:t,alias:s,on:n}}_isReservedAfterFrom(){return this.curTokenIs($e.WHERE)||this.curTokenIs($e.ORDER)||this.curTokenIs($e.LIMIT)||this.curTokenIs($e.OFFSET)||this.curTokenIs($e.GROUP)||this._isJoinKeyword()}_isJoinReserved(){return this.curTokenIs($e.ON)||this.curTokenIs($e.WHERE)||this.curTokenIs($e.ORDER)||this.curTokenIs($e.LIMIT)||this._isJoinKeyword()}parseInsert(){this.expect($e.INSERT),this.expect($e.INTO);const e=this.expectIdentifier("table name");let t;if(this.curTokenIs($e.LPAREN)&&(this.nextToken(),t=this.parseIdentifierList(),this.expect($e.RPAREN)),this.curTokenIs($e.SELECT))return{type:"INSERT",into:e,columns:t,select:this.parseSelect()};this.expect($e.VALUES);const s=[];do{this.curTokenIs($e.COMMA)&&this.nextToken(),this.expect($e.LPAREN);const e=this.parseValueList();this.expect($e.RPAREN),s.push(e)}while(this.curTokenIs($e.COMMA));return{type:"INSERT",into:e,columns:t,values:s}}parseUpdate(){this.expect($e.UPDATE);const e=this.expectIdentifier("table name");this.expect($e.SET);const t={};do{this.curTokenIs($e.COMMA)&&this.nextToken();const e=this.expectIdentifier("column name");this.expect($e.EQ),t[e]=this.parseValue()}while(this.curTokenIs($e.COMMA));let s={};return this.curTokenIs($e.WHERE)&&(this.nextToken(),s=this.parseCondition()),{type:"UPDATE",table:e,sets:t,where:s}}parseDelete(){this.expect($e.DELETE),this.expect($e.FROM);const e=this.expectIdentifier("table name");let t={};return this.curTokenIs($e.WHERE)&&(this.nextToken(),t=this.parseCondition()),{type:"DELETE",from:e,where:t}}parseCreateTable(){this.expect($e.TABLE);let e=!1;this.curTokenIs($e.IF)&&(this.nextToken(),this.expect($e.NOT),this.expect($e.EXISTS),e=!0);const t=this.expectIdentifier("table name");this.expect($e.LPAREN);const s=[];do{this.curTokenIs($e.COMMA)&&this.nextToken(),s.push(this.parseColumnDef())}while(this.curTokenIs($e.COMMA));return this.expect($e.RPAREN),{type:"CREATE_TABLE",name:t,columns:s,ifNotExists:e||void 0}}parseColumnDef(){const e={name:this.expectIdentifier("column name"),type:this.expectIdentifier("column type").toLowerCase()};for(;this.curTokenIs($e.PRIMARY)||this.curTokenIs($e.UNIQUE)||this.curTokenIs($e.NOT)||this.curTokenIs($e.DEFAULT)||this.curTokenIs($e.REFERENCES);)if(this.curTokenIs($e.PRIMARY))this.nextToken(),this.expect($e.KEY),e.primaryKey=!0;else if(this.curTokenIs($e.UNIQUE))this.nextToken(),e.unique=!0;else if(this.curTokenIs($e.NOT))this.nextToken(),this.expect($e.NULL),e.required=!0;else if(this.curTokenIs($e.DEFAULT))this.nextToken(),e.default=this.parseValue();else{if(!this.curTokenIs($e.REFERENCES))break;{this.nextToken();const t=this.expectIdentifier("referenced table");this.expect($e.LPAREN);const s=this.expectIdentifier("referenced column");for(this.expect($e.RPAREN),e.references=`${t}.${s}`;this.curTokenIs($e.ON);)if(this.nextToken(),this.curTokenIs($e.DELETE))this.nextToken(),e.onDelete=this.parseCascadeAction();else{if(!this.curTokenIs($e.UPDATE))break;this.nextToken(),e.onUpdate=this.parseCascadeAction()}}}return e}parseCascadeAction(){return this.curTokenIs($e.CASCADE)?(this.nextToken(),"CASCADE"):this.curTokenIs($e.SET)?(this.nextToken(),this.expect($e.NULL),"SET NULL"):this.curToken.type===$e.IDENTIFIER&&"RESTRICT"===this.curToken.value.toUpperCase()?(this.nextToken(),"RESTRICT"):"RESTRICT"}parseAlterTable(){this.expect($e.ALTER),this.expect($e.TABLE);const e=this.expectIdentifier("table name");let t;if(this.curTokenIs($e.ADD))return t="ADD",this.nextToken(),this.curToken.type===$e.IDENTIFIER&&"COLUMN"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ALTER_TABLE",name:e,action:t,column:this.parseColumnDef()};if(this.curTokenIs($e.DROP)||this._isKeywordAsIdent()&&"DROP"===this.curToken.value.toUpperCase())return t="DROP",this.nextToken(),this.curToken.type===$e.IDENTIFIER&&"COLUMN"===this.curToken.value.toUpperCase()&&this.nextToken(),{type:"ALTER_TABLE",name:e,action:t,column:{name:this.expectIdentifier("column name"),type:"string"}};throw this.error("Expected ADD or DROP in ALTER TABLE")}parseTruncateTable(){return this.expect($e.TRUNCATE),this.expect($e.TABLE),{type:"TRUNCATE_TABLE",name:this.expectIdentifier("table name")}}parseDropTable(){this.expect($e.TABLE);let e=!1;return this.curTokenIs($e.IF)&&(this.nextToken(),this.expect($e.EXISTS),e=!0),{type:"DROP_TABLE",name:this.expectIdentifier("table name"),ifExists:e||void 0}}parseCondition(){let e=this.parseSimpleCondition();for(;this.curTokenIs($e.AND)||this.curTokenIs($e.OR);){const t=this.curTokenIs($e.AND);this.nextToken();const s=this.parseSimpleCondition();e=t?{$and:[e,s]}:{$or:[e,s]}}return e}parseWhere(){return this.parseCondition()}parseSimpleCondition(){if(this.curTokenIs($e.EXISTS)||this._isKeywordAsIdent()&&"EXISTS"===this.curToken.value.toUpperCase())return this.nextToken(),this.parseExistsCondition(!1);if(this.curTokenIs($e.NOT)&&this._peekIsExists())return this.nextToken(),this.nextToken(),this.parseExistsCondition(!0);if(this.curTokenIs($e.NOT)&&!this._isNotInOrLike())return this.nextToken(),{$not:this.parseSimpleCondition()};if(this.curTokenIs($e.LPAREN)){this.nextToken();const e=this.parseCondition();return this.expect($e.RPAREN),e}const e=this.parseColumnRef();if(this.curTokenIs($e.IDENTIFIER)&&"IS"===this.curToken.value.toUpperCase()){this.nextToken();const t=this.curTokenIs($e.NOT);t&&this.nextToken(),this.expect($e.NULL);const s={};return s[e]=t?{$ne:null}:{$eq:null},s}if(this.curTokenIs($e.BETWEEN)){this.nextToken();const t=this.parseValue();this.expect($e.AND);const s=this.parseValue(),n={};return n[e]={$gte:t,$lte:s},n}if(this.curTokenIs($e.NOT)&&this.peekTokenIs($e.BETWEEN)){this.nextToken(),this.nextToken();const t=this.parseValue();this.expect($e.AND);const s=this.parseValue(),n={};return n[e]={$not:{$gte:t,$lte:s}},n}if(this.curTokenIs($e.NOT)){if(this.peekTokenIs($e.IN)){if(this.nextToken(),this.nextToken(),this.expect($e.LPAREN),this.curTokenIs($e.SELECT)){const t=this.parseSelect();this.expect($e.RPAREN);const s={};return s[e]={$nin:{$subquery:t}},s}const t=this.parseValueList();this.expect($e.RPAREN);const s={};return s[e]={$nin:t},s}if(this.peekTokenIs($e.LIKE)){this.nextToken(),this.nextToken();const t=this.parseValue(),s={};return s[e]={$not:{$like:t}},s}}if(this.curTokenIs($e.LIKE)){this.nextToken();const t=this.parseValue(),s={};return s[e]={$like:t},s}if(this.curTokenIs($e.IN)){if(this.nextToken(),this.expect($e.LPAREN),this.curTokenIs($e.SELECT)){const t=this.parseSelect();this.expect($e.RPAREN);const s={};return s[e]={$in:{$subquery:t}},s}const t=this.parseValueList();this.expect($e.RPAREN);const s={};return s[e]={$in:t},s}if(this.curTokenIs($e.AND)||this.curTokenIs($e.OR)||this.curTokenIs($e.RPAREN)||this.curTokenIs($e.EOF)||this.curToken.type===$e.IDENTIFIER&&["THEN","END","ELSE","NULLS","LIMIT","OFFSET","ORDER","GROUP","HAVING","UNION","WHERE"].includes(this.curToken.value.toUpperCase())){const t={};return t[e]={$eq:!0},t}const t=this.parseComparisonOp();if(this.curTokenIs($e.LPAREN)&&this.peekTokenIs($e.SELECT)){this.nextToken();const s=this.parseSelect();this.expect($e.RPAREN);const n={};return n[e]={[t]:{$subquery:s}},n}let s;s=(this.curToken.type===$e.IDENTIFIER||this._isKeywordAsIdent())&&this.peekTokenIs($e.DOT)?{$col:this.parseColumnRef()}:this.parseValue();const n={};return n[e]={[t]:s},n}parseExistsCondition(e){this.expect($e.LPAREN);const t=this.parseSelect();return this.expect($e.RPAREN),{$exists:{$subquery:t,$negate:e||void 0}}}_peekIsExists(){return this.peekToken.type===$e.EXISTS||this.peekToken.type===$e.IDENTIFIER&&"EXISTS"===this.peekToken.value.toUpperCase()}_isNotInOrLike(){return this.peekTokenIs($e.IN)||this.peekTokenIs($e.LIKE)}peekTokenIs(e){return this.peekToken.type===e}parseComparisonOp(){switch(this.curToken.type){case $e.EQ:return this.nextToken(),"$eq";case $e.NEQ:return this.nextToken(),"$ne";case $e.GT:return this.nextToken(),"$gt";case $e.GTE:return this.nextToken(),"$gte";case $e.LT:return this.nextToken(),"$lt";case $e.LTE:return this.nextToken(),"$lte";default:throw this.error(`Expected comparison operator, got "${this.curToken.value}"`)}}parseColumnList(){const e=[];for(e.push(this.parseColumnWithAlias());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseColumnWithAlias());return e}parseColumnWithAlias(){let e=this.parseColumnRef();if(this.curTokenIs($e.AS))this.nextToken(),e=`${e} AS ${this.expectIdentifier("alias")}`;else if(this.curToken.type===$e.IDENTIFIER&&!this._isReservedAfterFrom()&&!this._isJoinKeyword()){const t=this.curToken.value;this.nextToken(),e=`${e} AS ${t}`}return e}parseColumnRef(){if(this.curTokenIs($e.CASE))return this.parseCaseExpressionText();if(this.curTokenIs($e.NUMBER)){const e=this.curToken.value;return this.nextToken(),e}if(this.curTokenIs($e.STRING)){const e=this.curToken.value;return this.nextToken(),`'${e}'`}if(this.curTokenIs($e.COUNT)||this.curTokenIs($e.SUM)||this.curTokenIs($e.AVG)||this.curTokenIs($e.MIN)||this.curTokenIs($e.MAX))return this.parseAggregateCall();const e=this.expectIdentifier("column name");return this.curTokenIs($e.DOT)?(this.nextToken(),`${e}.${this.expectIdentifier("column name")}`):e}parseCaseExpressionText(){const e=this.curToken.position;this.nextToken();let t=1,s=e+4;for(;!this.curTokenIs($e.EOF)&&t>0&&(this.curTokenIs($e.CASE)&&t++,!this.curTokenIs($e.END)||(t--,s=this.curToken.position+3,this.nextToken(),0!==t));)s=this.curToken.position+this.curToken.value.length,this.nextToken();let n=this.sql.slice(e,s);return this.curTokenIs($e.AS)?(this.nextToken(),n+=` AS ${this.expectIdentifier("alias")}`):this.curToken.type!==$e.IDENTIFIER||this.curTokenIs($e.COMMA)||this._isReservedAfterFrom()||(n+=` AS ${this.curToken.value}`,this.nextToken()),n}parseAggregateCall(){const e=this.curToken.value.toUpperCase();this.nextToken(),this.expect($e.LPAREN);let t,s=!1;this.curTokenIs($e.DISTINCT)&&(s=!0,this.nextToken()),this.curTokenIs($e.STAR)?(t="*",this.nextToken()):t=this.parseColumnRef(),this.expect($e.RPAREN);let n="";this.curTokenIs($e.AS)?(this.nextToken(),n=this.expectIdentifier("alias")):this.curToken.type===$e.IDENTIFIER&&this._isAggregateAlias()&&(n=this.curToken.value,this.nextToken());const i=s?`DISTINCT ${t}`:t;return n?`${e}(${i}) AS ${n}`:`${e}(${i})`}_isAggregateAlias(){return!this._isReservedAfterFrom()&&!this._isJoinKeyword()}parseIdentifierList(){const e=[];for(e.push(this.parseIdentifierWithDot());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseIdentifierWithDot());return e}parseValueList(){const e=[];for(e.push(this.parseValue());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseValue());return e}parseOrderByList(){const e=[];for(e.push(this.parseOrderBy());this.curTokenIs($e.COMMA);)this.nextToken(),e.push(this.parseOrderBy());return e}parseOrderBy(){const e=this.parseIdentifierWithDot();let t,s="asc";return this.curTokenIs($e.ASC)?this.nextToken():this.curTokenIs($e.DESC)&&(s="desc",this.nextToken()),this.curTokenIs($e.IDENTIFIER)&&"NULLS"===this.curToken.value.toUpperCase()&&(this.nextToken(),this.curTokenIs($e.IDENTIFIER)&&"FIRST"===this.curToken.value.toUpperCase()?(t="first",this.nextToken()):this.curTokenIs($e.IDENTIFIER)&&"LAST"===this.curToken.value.toUpperCase()&&(t="last",this.nextToken())),{column:e,direction:s,...t?{nulls:t}:{}}}parseIdentifierWithDot(){const e=this.expectIdentifier("identifier");return this.curTokenIs($e.DOT)?(this.nextToken(),`${e}.${this.expectIdentifier("identifier")}`):e}parseValue(){switch(this.curToken.type){case $e.STRING:{const e=this.curToken.value;return this.nextToken(),e}case $e.NUMBER:{const e=Number(this.curToken.value);return this.nextToken(),e}case $e.TRUE:return this.nextToken(),!0;case $e.FALSE:return this.nextToken(),!1;case $e.NULL:return this.nextToken(),null;default:throw this.error(`Expected value, got "${this.curToken.value}"`)}}nextToken(){this.curToken=this.peekToken,this.peekToken=this.lexer.nextToken()}curTokenIs(e){return this.curToken.type===e}expect(e){if(!this.curTokenIs(e))throw this.error(`Expected ${e}, got "${this.curToken.value}"`);this.nextToken()}expectIdentifier(e){if(this.curToken.type===$e.IDENTIFIER||this._isKeywordAsIdent()){const e=this.curToken.value;return this.nextToken(),e}throw this.error(`Expected ${e}, got "${this.curToken.value}"`)}_isKeywordAsIdent(){return this.curToken.type!==$e.EOF&&this.curToken.type!==$e.ILLEGAL&&this.curToken.type!==$e.STRING&&this.curToken.type!==$e.NUMBER&&this.curToken.type!==$e.COMMA&&this.curToken.type!==$e.LPAREN&&this.curToken.type!==$e.RPAREN&&this.curToken.type!==$e.SEMICOLON&&this.curToken.type!==$e.EQ&&this.curToken.type!==$e.NEQ&&this.curToken.type!==$e.GT&&this.curToken.type!==$e.GTE&&this.curToken.type!==$e.LT&&this.curToken.type!==$e.LTE&&this.curToken.type!==$e.DOT&&this.curToken.type!==$e.STAR}expectNumber(e){if(this.curToken.type===$e.NUMBER){const e=Number(this.curToken.value);return this.nextToken(),e}throw this.error(`Expected ${e}, got "${this.curToken.value}"`)}error(e){return new n(`Parse error at position ${this.curToken.position}: ${e}`,"PARSE_ERROR")}}function Me(e){return new _e(e).parseAllStatements()}function Pe(e){return new _e(e).parseWhere()}function Be(e){return null===e?"n":void 0===e?"u":"string"==typeof e?`s${e}`:"number"==typeof e?`d${e}`:"boolean"==typeof e?`b${e}`:"object"==typeof e?`o${JSON.stringify(e)}`:`x${String(e)}`}function Ke(e){const t=e.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);if(!t)return null;const s=t[1],n=t[2]??null,i=[],r=/WHEN\s+([\s\S]*?)\s+THEN\s+([\s\S]*?)(?=\s+WHEN\s+|\s+ELSE\s+|\s*$)/gi;let a;for(;null!==(a=r.exec(s));){let e=null;try{e=Pe(a[1].trim())}catch{}i.push({cond:e,value:a[2].trim()})}let o=null;const h=s.match(/\sELSE\s+([\s\S]*)$/i);return h&&(o=h[1].trim()),{whens:i,elseValue:o,alias:n}}function qe(e,t){const s=e.trim();if("null"===s)return null;if("true"===s)return!0;if("false"===s)return!1;const n=Number(s);if(""!==s&&!isNaN(n))return n;const i=s.match(/^'(.*)'$/s)||s.match(/^"(.*)"$/s);return i?i[1]:/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(s)?t[s]??null:s}function ze(e,t){for(const{cond:s,value:n}of e.whens)if(s&&o(t,s))return qe(n,t);return null!==e.elseValue?qe(e.elseValue,t):null}class Fe{constructor(e,t=0){this.engine=e,this.maxRowsPerQuery=t}async execute(e){switch(e.type){case"SELECT":return this.executeSelect(e);case"SELECT_UNION":return this.executeSelectUnion(e);case"EXPLAIN":return this.executeExplain(e);case"INSERT":return this.executeInsert(e);case"UPDATE":return this.executeUpdate(e);case"DELETE":return this.executeDelete(e);case"CREATE_TABLE":return this.executeCreateTable(e);case"DROP_TABLE":return this.executeDropTable(e);case"ALTER_TABLE":return this.executeAlterTable(e);case"TRUNCATE_TABLE":return this.executeTruncateTable(e);case"CREATE_INDEX":return this.executeCreateIndex(e);case"DROP_INDEX":return this.executeDropIndex(e);case"BEGIN":return this.executeBegin();case"COMMIT":return this.executeCommit();case"ROLLBACK":return this.executeRollback();case"SAVEPOINT":return this.executeSavepoint(e);case"ANALYZE":return this.executeAnalyze(e);case"REINDEX":return this.executeReindex(e);case"VACUUM":return this.executeVacuum();default:throw new n("Unknown statement type","UNKNOWN_STATEMENT")}}async executeSelectUnion(e){const t=await this.executeSelectPart(e.left),s=await this.executeSelectPart(e.right),n=t.length>0?Object.keys(t[0]):[],i=t.map(e=>e);if(e.all){for(const e of s)i.push(this.projectUnionRow(e,n));return i}const r=new Set,a=[];for(const e of i){const t=Object.values(e).map(Be).join("");r.has(t)||(r.add(t),a.push(e))}for(const e of s){const t=this.projectUnionRow(e,n),s=Object.values(t).map(Be).join("");r.has(s)||(r.add(s),a.push(t))}return a}async executeSelectPart(e){return"SELECT_UNION"===e.type?this.executeSelectUnion(e):this.executeSelect(e)}projectUnionRow(e,t){if(0===t.length)return e;const s=Object.values(e),n={};for(let e=0;e0)try{const e=await this.engine.getTableSchema(r.table);if(e){const t=s=>{for(const[n,i]of Object.entries(s)){if("$and"===n){for(const e of i){const s=t(e);if(s)return s}continue}if("$or"===n||"$not"===n)continue;const s=e.columns[n];if(s){if(s.primaryKey)return"pk";if(s.index||s.unique)return`index:${n}`}}return null};a=t(r.where)??"none"}}catch{}return{type:e.query.type,table:r?.table,columns:r?.columns,where:r?.where||{},orderBy:r?.orderBy||[],limit:r?.limit,offset:r?.offset,usingIndex:a,estimatedRows:n,actualTimeMs:i}}async executeSelect(e){const t=!!(e.groupBy&&e.groupBy.length>0),s=!t&&this._hasAggregateColumn(e.columns);let n;const i=!!(e.joins&&e.joins.length>0),r=this.hasCaseColumn(e.columns)||!!e.where&&this.whereHasCase(e.where),a=this.orderByUsesSelectAlias(e),h=e.columns.some(e=>/\s+AS\s+\w+$/i.test(e));if(e.fromSubquery){const t=await this.executeSelectPart(e.fromSubquery);n=i?await this.executeJoinSelect(e,t.map(t=>this.prefixRow(t,e.alias??""))):t,!i&&e.where&&Object.keys(e.where).length>0&&(e.where=await this.resolveSubqueries(e.where),n=n.filter(t=>o(t,e.where)))}else if(e.from||i)if(i)n=await this.executeJoinSelect(e);else{e.where&&Object.keys(e.where).length>0&&(e.where=this.normalizeWhereColumns(e.where,[e.alias??e.from]));const i=[e.alias??e.from].filter(Boolean);if(e.orderBy&&e.orderBy.length>0&&(e.orderBy=e.orderBy.map(e=>({...e,column:this.stripAlias(e.column,i)}))),e.groupBy&&e.groupBy.length>0&&(e.groupBy=e.groupBy.map(e=>this.stripAlias(e,i))),e.columns=e.columns.map(e=>{if("*"===e||/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e)||/^\s*CASE\b/i.test(e)||/^'/.test(e))return e;const t=e.match(/^(.+?)\s+AS\s+(\w+)$/i);if(t){const s=this.stripAlias(t[1].trim(),i);return s===t[1].trim()?e:`${s} AS ${t[2]}`}return this.stripAlias(e,i)}),e.where&&this.hasCorrelatedRefs(e.where)){const i=ve(t||s?{...e,columns:["*"]}:e);i.columns=["*"],a&&(i.orderBy=void 0,i.limit=void 0,i.offset=void 0),n=await this.engine.find(i.table,{...i,where:this.stripCorrelatedExists(e.where)}),n=await this.filterCorrelated(n,e.where)}else{e.where&&Object.keys(e.where).length>0&&(e.where=await this.resolveSubqueries(e.where));const i=ve(t||s?{...e,columns:["*"]}:e);(r||h)&&(i.columns=["*"]),a&&(i.orderBy=void 0,i.limit=void 0,i.offset=void 0),n=await this.engine.find(i.table,i)}}else n=[{}];if(s&&(n=[this.computeSingleAggregate(n,e)]),t&&(n=this.executeGroupBy(n,e)),e.distinct&&(n=this.executeDistinct(n)),e.having&&Object.keys(e.having).length>0){e.having=await this.resolveSubqueries(e.having);const t=e._aggAliasMap;if(t&&t.size>0){const s={};for(const[n,i]of Object.entries(e.having))s[t.get(n)??n]=i;e.having=s}n=n.filter(t=>o(t,e.having))}e.orderBy&&e.orderBy.length>0&&(n=l(n,e.orderBy)),t||s||!(e.columns.length>0)||1===e.columns.length&&"*"===e.columns[0]||(n=n.map(t=>this.projectRow(t,e.columns))),a&&e.orderBy&&e.orderBy.length>0&&(n=l(n,e.orderBy));const c=e.offset??0,u=e.limit??n.length;return n=n.slice(c,c+u),this.maxRowsPerQuery>0&&n.length>this.maxRowsPerQuery&&(n=n.slice(0,this.maxRowsPerQuery)),n}async executeJoinSelect(e,t){const s=e.alias??e.from;let n;if(t)n=t;else{const{pushable:t}=this.extractPushableWhere(e.where??{},s);n=(await this.engine.find(e.from,{table:e.from,where:Object.keys(t).length>0?t:void 0})).map(e=>this.prefixRow(e,s))}let i=n;for(const t of e.joins){const e=t.alias??t.table,n=await this.tryHashJoin(i,t,e,s);if(n){i=n;continue}const r=(await this.engine.find(t.table,{table:t.table})).map(t=>this.prefixRow(t,e));i=this.joinRows(i,r,t)}return e.where&&Object.keys(e.where).length>0&&(this.hasCorrelatedRefs(e.where)?i=await this.filterCorrelated(i,e.where):(e.where=await this.resolveSubqueries(e.where),i=i.filter(t=>o(t,e.where)))),i}prefixRow(e,t){const s={};for(const[n,i]of Object.entries(e))s[`${t}.${n}`]=i;return s}extractPushableWhere(e,t){const s={};if(!t)return{pushable:s};const n=`${t}.`;for(const[t,i]of Object.entries(e))t.startsWith(n)&&("object"==typeof i&&null!==i&&("$col"in i||"$subquery"in i||"$and"in i||"$or"in i||"$not"in i)||(s[t.slice(n.length)]=i));return{pushable:s}}async tryHashJoin(e,t,s,n){if("CROSS"===t.type||"RIGHT"===t.type)return null;const i=[],r=e=>{for(const[t,s]of Object.entries(e)){if("$and"===t){if(!s.every(r))return!1;continue}if("$or"===t||"$not"===t)return!1;let e=null;if("object"==typeof s&&null!==s){const t=s;"$eq"in t&&"object"==typeof t.$eq&&null!==t.$eq&&"$col"in t.$eq?e=String(t.$eq.$col):"$col"in t&&1===Object.keys(t).length&&(e=String(t.$col))}if(!e)return!1;const a=!!n&&t.startsWith(`${n}.`);i.push({leftCol:a?t:e,rightCol:a?e:t})}return!0};if(!r(t.on))return null;if(0===i.length)return null;const a=await this.engine.getTableSchema(t.table);if(!a)return null;const o=i.find(e=>{const t=e.rightCol.split(".").pop(),s=a.columns[t];return s&&(s.primaryKey||s.index||s.unique)});if(!o)return null;const h=o.rightCol.split(".").pop(),c=Array.from(new Set(e.map(e=>e[o.leftCol]).filter(e=>null!=e)));if(0===c.length)return null;const l=await this.engine.find(t.table,{table:t.table,where:{[h]:{$in:c}}}),u=new Map;for(const e of l){const t=i.map(t=>String(e[t.rightCol.split(".").pop()]??"\0")).join("");u.has(t)||u.set(t,[]),u.get(t).push(e)}const f={};for(const e of Object.keys(a.columns))f[e]=null;const d=[];for(const n of e){const e=i.map(e=>String(n[e.leftCol]??"\0")).join(""),r=u.get(e);if(r&&r.length>0)for(const e of r)d.push({...n,...this.prefixRow(e,s)});else"LEFT"===t.type&&d.push({...n,...this.prefixRow(f,s)})}return d}joinRows(e,t,s){if("CROSS"===s.type){const s=[];for(const n of e)for(const e of t)s.push({...n,...e});return s}const n=[];for(const i of e){let e=!1;for(const r of t){const t={...i,...r};o(t,s.on,{$col:!0})&&(n.push(t),e=!0)}if(!e&&"LEFT"===s.type){const e={};for(const s of Object.keys(t[0]??{}))e[s]=null;n.push({...i,...e})}}if("RIGHT"===s.type)for(const i of t)if(!e.some(e=>o({...e,...i},s.on,{$col:!0}))){const t={};for(const s of Object.keys(e[0]??{}))t[s]=null;n.push({...t,...i})}return n}executeGroupBy(e,t){const s=new Map;for(const n of e){const e=t.groupBy.map(e=>Be(n[e])).join("");s.has(e)||s.set(e,[]),s.get(e).push(n)}const n=[],i=new Map;for(const e of s.values()){const s={};for(const n of t.groupBy)s[n]=e[0][n];for(const n of t.columns){if("*"===n)continue;const r=n.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);if(r){const[,t,a,o]=r,h=this.computeAggregate(t.toUpperCase(),e,a.trim()),c=`${t.toUpperCase()}(${a.trim()})`,l=o||n;l!==c&&i.set(c,l),s[l]=h}else if(/^\s*CASE\b/i.test(n)){const t=Ke(n);s[t?.alias??n]=t?ze(t,e[0]):null}else t.groupBy.includes(n)||(s[n]=e[0][n])}n.push(s)}return t._aggAliasMap=i,n}computeAggregate(e,t,s){const n=/^\s*CASE\b/i.test(s)?Ke(s):null,i=!n&&/^\s*DISTINCT\s+/i.test(s),r=i?s.replace(/^\s*DISTINCT\s+/i,"").trim():s,a=t.map(e=>n?ze(n,e):e[r]).filter(e=>null!=e);if("COUNT"===e)return"*"===r?t.length:i?new Set(a.map(e=>"object"==typeof e?JSON.stringify(e):String(e))).size:a.length;const o=a.map(Number),h=i?Array.from(new Set(o)):o;switch(e){case"SUM":return h.reduce((e,t)=>e+t,0);case"AVG":return 0===h.length?0:h.reduce((e,t)=>e+t,0)/h.length;case"MIN":return 0===h.length?0:Math.min(...h);case"MAX":return 0===h.length?0:Math.max(...h);default:return 0}}executeDistinct(e){const t=new Set;return e.filter(e=>{const s=Object.values(e).map(Be).join("");return!t.has(s)&&(t.add(s),!0)})}async executeInsert(e){const t=await this.engine.getTableSchema(e.into);if(!t)throw new n(`Table "${e.into}" does not exist`,"TABLE_NOT_FOUND");const s=e.columns??Object.keys(t.columns);if(e.select){const t=await this.executeSelectPart(e.select);let n=[];const i=e.select;if("SELECT"===i.type)if(i.columns&&i.columns.length>0&&"*"!==i.columns[0])n=i.columns.map(e=>e.split(".").pop());else if(i.from){const e=await this.engine.getTableSchema(i.from);n=e?Object.keys(e.columns):[]}0===n.length&&t.length>0&&(n=Object.keys(t[0]));const r=t.map(e=>{const t={};for(let i=0;i{const t={};for(let n=0;ne.primaryKey))throw new n(`Composite primary keys are not supported yet: table "${e.name}" already has a primary key column`,"SCHEMA_ERROR");if("function"==typeof this.engine.alterTable)return this.engine.alterTable(e.name,e.action,{...y(e.column),name:e.column.name});if("ADD"===e.action){if(t.columns[e.column.name])throw new n(`Column "${e.column.name}" already exists in table "${e.name}"`,"COLUMN_EXISTS");t.columns[e.column.name]=y(e.column)}else if("DROP"===e.action){if(!t.columns[e.column.name])throw new n(`Column "${e.column.name}" does not exist in table "${e.name}"`,"COLUMN_NOT_FOUND");delete t.columns[e.column.name];const s=await this.engine.find(e.name,{table:e.name}),i=e.column.name;for(const e of s)i in e&&delete e[i]}}}async executeTruncateTable(e){if(!await this.engine.hasTable(e.name))throw new n(`Table "${e.name}" does not exist`,"TABLE_NOT_FOUND");return this.engine.clear(e.name)}async executeCreateIndex(e){if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");const t=await this.engine.getTableSchema(e.table);if(t&&!t.columns[e.column])throw new n(`Column "${e.column}" does not exist in table "${e.table}"`,"COLUMN_NOT_FOUND");if("function"!=typeof this.engine.createIndex)throw new n(`Engine "${this.engine.name}" does not support CREATE INDEX`,"NOT_SUPPORTED");return this.engine.createIndex(e.table,e.column,e.unique)}async executeDropIndex(e){if("function"!=typeof this.engine.dropIndex)throw new n(`Engine "${this.engine.name}" does not support DROP INDEX`,"NOT_SUPPORTED");return this.engine.dropIndex(e.table,e.column,e.name)}async executeBegin(){return this.engine.beginTransaction()}async executeCommit(){return this.engine.commitTransaction()}async executeRollback(){return this.engine.rollbackTransaction()}async executeSavepoint(e){const t=this.engine;if("SAVE"===e.action){if("function"!=typeof t.savepoint)throw new n(`Engine "${this.engine.name}" does not support SAVEPOINT`,"NOT_SUPPORTED");return t.savepoint(e.name)}if("ROLLBACK"===e.action){if("function"!=typeof t.rollbackToSavepoint)throw new n(`Engine "${this.engine.name}" does not support ROLLBACK TO SAVEPOINT`,"NOT_SUPPORTED");return t.rollbackToSavepoint(e.name)}if("function"!=typeof t.releaseSavepoint)throw new n(`Engine "${this.engine.name}" does not support RELEASE SAVEPOINT`,"NOT_SUPPORTED");return t.releaseSavepoint(e.name)}async executeAnalyze(e){const t=this.engine;if("function"!=typeof t.analyzeTable)throw new n(`Engine "${this.engine.name}" does not support ANALYZE`,"NOT_SUPPORTED");if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");return t.analyzeTable(e.table)}async executeReindex(e){const t=this.engine;if("function"!=typeof t.reindexTable)throw new n(`Engine "${this.engine.name}" does not support REINDEX`,"NOT_SUPPORTED");if(!await this.engine.hasTable(e.table))throw new n(`Table "${e.table}" does not exist`,"TABLE_NOT_FOUND");return t.reindexTable(e.table)}async executeVacuum(){const e=this.engine;if("function"!=typeof e.vacuum)throw new n(`Engine "${this.engine.name}" does not support VACUUM`,"NOT_SUPPORTED");return e.vacuum()}hasCaseColumn(e){return e.some(e=>/^\s*CASE\b/i.test(e))}orderByUsesSelectAlias(e){if(!e.orderBy||0===e.orderBy.length)return!1;const t=new Set;for(const s of e.columns){const e=s.match(/\s+AS\s+(\w+)$/i);if(e)t.add(e[1]);else if(/^\s*CASE\b/i.test(s)){const e=Ke(s);e?.alias&&t.add(e.alias)}}return 0!==t.size&&e.orderBy.some(e=>t.has(e.column))}whereHasCase(e){for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if(/^\s*CASE\b/i.test(t))return!0}else if(this.whereHasCase(s))return!0}else if(s.some(e=>this.whereHasCase(e)))return!0;return!1}getEngine(){return this.engine}projectRow(e,t){const s=[],n=[],i=[],r=[];let a=!1;for(const e of t){if("*"===e){a=!0;continue}const t=Ke(e);if(t){i.push({alias:t.alias??e,expr:t});continue}const o=e.match(/^(.+?)\s+AS\s+(\w+)$/i);if(o){n.push({alias:o[2],source:o[1].trim()});continue}const h=e.match(/^'(.*)'$/s);if(h){const t=h[1].replace(/''/g,"'");r.push({key:e,value:t});continue}s.push(e)}const o=a?{...e}:s.length>0?f(e,s):{};for(const{alias:t,source:s}of n)if("*"===s)Object.assign(o,e);else{const n=s.match(/^'(.*)'$/s);o[t]=n?n[1].replace(/''/g,"'"):e[s]}for(const{key:e,value:t}of r)o[e]=t;for(const{alias:t,expr:s}of i)o[t]=ze(s,e);return o}_hasAggregateColumn(e){return e.some(e=>/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e))}computeSingleAggregate(e,t){const s={};for(const n of t.columns){if("*"===n)continue;const t=n.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);if(t){const[,i,r,a]=t;s[a||n]=this.computeAggregate(i.toUpperCase(),e,r.trim())}else s[n]=e.length>0?e[0][n]:null}return s}normalizeWhereColumns(e,t){const s={};for(const[n,i]of Object.entries(e))"$and"!==n&&"$or"!==n?"$not"!==n?"$exists"!==n?s[this.stripAlias(n,t)]=this.normalizeFieldValue(i,t):s[n]=this.normalizeExistsValue(i,t):s.$not=this.normalizeWhereColumns(i,t):s[n]=i.map(e=>this.normalizeWhereColumns(e,t));return s}normalizeExistsValue(e,t){if("object"!=typeof e||null===e)return e;const s=e;if(s.$subquery){const e=s.$subquery,n=[e.alias??e.from,...t].filter(Boolean);return{...s,$subquery:{...e,where:this.normalizeWhereColumns(e.where,n)}}}return e}normalizeFieldValue(e,t){if("object"!=typeof e||null===e||Array.isArray(e))return e;const s={};for(const[n,i]of Object.entries(e))"$and"===n||"$or"===n?s[n]=i.map(e=>this.normalizeWhereColumns(e,t)):"$not"===n&&"object"==typeof i&&null!==i?s[n]=this.normalizeFieldValue(i,t):"$col"===n?s[n]=this.stripAlias(String(i),t):"object"==typeof i&&null!==i&&!Array.isArray(i)&&"$col"in i?s[n]={$col:this.stripAlias(String(i.$col),t)}:s[n]=i;return s}stripAlias(e,t){for(const s of t){if(!s)continue;const t=`${s}.`;if(e.startsWith(t))return e.slice(t.length)}return e}hasCorrelatedRefs(e){for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"!==t){if(/^\s*CASE\b/i.test(t))return!0;if(this.fieldHasColRef(s))return!0}else if("object"==typeof s&&null!==s&&"$subquery"in s)return!0}else if(this.hasCorrelatedRefs(s))return!0}else if(s.some(e=>this.hasCorrelatedRefs(e)))return!0;return!1}fieldHasColRef(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;const t=e;if("$col"in t)return!0;if("$and"in t||"$or"in t)return(t.$and??t.$or).some(e=>this.hasCorrelatedRefs(e));if("$not"in t&&"object"==typeof t.$not&&null!==t.$not)return this.fieldHasColRef(t.$not);for(const[,e]of Object.entries(t))if("object"==typeof e&&null!==e&&!Array.isArray(e)){if("$col"in e)return!0;if(this.fieldHasColRef(e))return!0}return!1}stripCorrelatedExists(e){const t={};for(const[s,n]of Object.entries(e))if("$and"!==s&&"$or"!==s){if("$not"===s){const e=this.stripCorrelatedExists(n);Object.keys(e).length>0&&(t.$not=e);continue}"$exists"!==s&&(/^\s*CASE\b/i.test(s)||(t[s]=n))}else t[s]=n.map(e=>this.stripCorrelatedExists(e));return t}async filterCorrelated(e,t){const s=[];for(const n of e){let e=this.resolveCaseKeys(t,n);e=await this.resolveSubqueries(e,n),o(n,e)&&s.push(n)}return s}resolveCaseKeys(e,t){const s={};for(const[n,i]of Object.entries(e))if("$and"!==n&&"$or"!==n)if("$not"!==n){if(/^\s*CASE\b/i.test(n)){const e=Ke(n);if(!e)continue;const r=ze(e,t);if(!this.caseConditionMatches(r,i))return{$caseResult:!1};s.$caseResult=!0;continue}s[n]=i}else s.$not=this.resolveCaseKeys(i,t);else s[n]=i.map(e=>this.resolveCaseKeys(e,t));return s}caseConditionMatches(e,t){if("object"!=typeof t||null===t||Array.isArray(t))return e===t;const s=t;for(const[t,n]of Object.entries(s))switch(t){case"$eq":if(e!==n)return!1;break;case"$ne":if(e===n)return!1;break;case"$gt":if(!(e>n))return!1;break;case"$gte":if(!(e>=n))return!1;break;case"$lt":if(!(ethis.bindWhereRefs(e,t)):"$not"===n&&"object"==typeof i&&null!==i?s[n]=this.bindColumnRefs(i,t):"object"==typeof i&&null!==i&&!Array.isArray(i)&&"$col"in i?s[n]=t[String(i.$col)]??null:s[n]=i;return s}bindWhereRefs(e,t){const s={};for(const[n,i]of Object.entries(e))"$and"===n||"$or"===n?s[n]=i.map(e=>this.bindWhereRefs(e,t)):"$not"===n?s.$not=this.bindWhereRefs(i,t):s[n]="$exists"===n?i:this.bindColumnRefs(i,t);return s}async resolveSubqueries(e,t){t&&(e=this.bindWhereRefs(e,t));const s={};for(const[n,i]of Object.entries(e)){if("$exists"===n&&"object"==typeof i&&null!==i){const e=i,n=e.$subquery,r=!!e.$negate;let a=n.where;this.hasCorrelatedRefs(a)&&(a=this.bindWhereRefs(a,t??{}));const o=await this.executeSelectPart({...n,where:a});s.$exists=o.length>0!==r;continue}"$and"===n&&Array.isArray(i)?s.$and=await Promise.all(i.map(e=>this.resolveSubqueries(e,t))):"$or"===n&&Array.isArray(i)?s.$or=await Promise.all(i.map(e=>this.resolveSubqueries(e,t))):"$not"!==n||"object"!=typeof i||null===i?s[n]="object"==typeof i&&null!==i?await this.resolveOperatorSubqueries(i):i:s.$not=await this.resolveSubqueries(i,t)}return s}async resolveOperatorSubqueries(e){const t={};for(const[s,n]of Object.entries(e))if("$and"===s&&Array.isArray(n))t.$and=await Promise.all(n.map(e=>this.resolveSubqueries(e)));else if("$or"===s&&Array.isArray(n))t.$or=await Promise.all(n.map(e=>this.resolveSubqueries(e)));else if("$not"!==s)if("object"==typeof n&&null!==n&&"$subquery"in n){const e=n.$subquery,i=await this.executeSelect(e);if("$in"===s||"$nin"===s){const e=Object.keys(i[0]||{})[0],n=i.map(t=>t[e]);t[s]=n}else if(0===i.length)t[s]=null;else{const e=Object.keys(i[0])[0];t[s]=i[0][e]}}else t[s]=n;else t.$not="object"==typeof n&&null!==n?await this.resolveOperatorSubqueries(n):n;return t}}function je(e){if(null==e)return"NULL";if("number"==typeof e)return Number.isFinite(e)?String(e):"NULL";if("boolean"==typeof e)return e?"TRUE":"FALSE";if("string"==typeof e)return`'${e.replace(/'/g,"''")}'`;throw new n("Object/array query parameters are not supported by SQL binding (pass JSON strings explicitly)","PARAM_ERROR")}class Ve{constructor(e){this.tables=new Map,this.completed=!1,this.engine=e}table(e){let t=this.tables.get(e);return t||(t=new Le(this.engine,e),this.tables.set(e,t)),t}_markCompleted(){this.completed=!0}isCompleted(){return this.completed}}class We{constructor(e){this.engine=e}async execute(e){const t=new Ve(this.engine);await this.engine.beginTransaction();try{const s=await e(t);return await this.engine.commitTransaction(),t._markCompleted(),s}catch(e){try{await this.engine.rollbackTransaction()}catch{}if(e instanceof n)throw e;throw new n(`Transaction failed: ${e.message}`,"TRANSACTION_ERROR",e)}}}class He{constructor(){this.plugins=[],this.hooks=new Map}register(e,t){const s=e.priority??0,n=this.plugins.findIndex(e=>(e.priority??0)t.name===e);-1!==t&&(this.plugins[t].destroy(),this.plugins.splice(t,1))}getPlugins(){return[...this.plugins]}on(e,t){const s=this.hooks.get(e)??[];s.push(t),this.hooks.set(e,s)}off(e,t){const s=this.hooks.get(e);if(s){const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}async trigger(e,...t){const s=this.hooks.get(e);if(s)for(const e of s)await e(...t)}destroy(){for(const e of this.plugins)try{e.destroy()}catch(e){}this.plugins=[],this.hooks.clear()}}class Ge{static async create(e){const t=new Ge(e);return await t.init(),t}get version(){return this._version}get maxRowsPerQuery(){return this.config.maxRowsPerQuery??0}get debug(){return this.config.debug??!1}constructor(e){this.ready=!1,this.tableCache=new Map,this.channel=null,this.listeners=new Map,this.migrations=new Map,this.config=e,this.name=e.name??s.name,this.mode=e.mode??s.mode,this._version=e.version??s.version,this.pluginManager=new He,e.multiTabSync&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`metona-sqlark:${this.name}`),this.channel.onmessage=e=>{const t=e.data;t&&"change"===t.type&&(this.emit(t.table??"",{type:"external",table:t.table??""}),this.engine instanceof Re&&this.engine.reloadMemoryFromDisk().catch(()=>{}))})}async init(){if(this.engine=this.createEngine(),await this.engine.open(this.name,this.version),"function"==typeof this.engine.getMeta)try{const e=await this.engine.getMeta("__metona_version");null!=e&&Number(e)>=1&&(this._version=Math.max(this._version,Math.floor(Number(e))))}catch{}if(this.executor=new Fe(this.engine,this.maxRowsPerQuery),this.transactionManager=new We(this.engine),this.config.plugins)for(const e of this.config.plugins)this.pluginManager.register(e,this);this.ready=!0,this.config.onReady&&this.config.onReady(this)}isReady(){return this.ready}async defineTable(e,t){this.ensureReady();const s=d(e,t);try{await this.pluginManager.trigger("beforeCreateTable",s),await this.engine.createTable(s),await this.pluginManager.trigger("afterCreateTable",s)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}table(e){this.ensureReady();let t=this.tableCache.get(e);return t||(t=new Le(this.engine,e,this.executor,e=>this.broadcastChange(e),(e,t)=>this.pluginManager.trigger(e,...t)),this.tableCache.set(e,t)),t}async dropTable(e){this.ensureReady();try{await this.pluginManager.trigger("beforeDropTable",e),await this.engine.dropTable(e),await this.pluginManager.trigger("afterDropTable",e)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}async getTableNames(){return this.ensureReady(),this.engine.getTableNames()}async query(e,t){this.ensureReady();const s=this.debug?Date.now():0;let i;await this.pluginManager.trigger("beforeQuery",e);try{const s=function(e,t){if(!t)return e;let s="",i=0,r=0,a=null;for(;i=t.length)throw new n(`Too few query parameters: placeholder #${r+1} has no value (got ${t.length} total)`,"PARAM_ERROR");s+=je(t[r]),r++,i++}else{for(s+=e[i]+e[i+1],i+=2;i/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e)),o=e=>{if(!e)return!1;for(const[t,s]of Object.entries(e))if("$and"!==t&&"$or"!==t){if("$not"!==t){if("$exists"===t)return!0;if("object"==typeof s&&null!==s)for(const[,e]of Object.entries(s))if("object"==typeof e&&null!==e&&("$subquery"in e||"$col"in e))return!0}else if(o(s))return!0}else if(s.some(e=>o(e)))return!0;return!1};if(!(r.joins||r.groupBy||r.having||r.distinct||a||r.orderBy&&r.orderBy.length>0||o(r.where))&&"function"==typeof this.engine.findStream&&"AsyncFunction"!==t.constructor?.name){const e=this.normalizeWhereForStream(r),s=r.columns.filter(e=>!/\s+AS\s+\w+$/i.test(e));return this.engine.findStream(r.from,{table:r.from,columns:s.length>0&&"*"!==s[0]?s:["*"],where:e&&Object.keys(e).length>0?e:void 0,limit:r.limit,offset:r.offset},t)}const h=await this.query(e);if(Array.isArray(h)){for(const e of h)await t(e);return h.length}return 0}normalizeWhereForStream(e){const t=[e.alias??e.from].filter(Boolean),s=e=>{for(const s of t)if(e.startsWith(`${s}.`))return e.slice(s.length+1);return e},n=e=>{const t={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?t[i]=r.map(n):"$not"===i&&"object"==typeof r&&null!==r?t.$not=n(r):t[s(i)]=r;return t};return n(e.where??{})}async transaction(e){this.ensureReady(),await this.pluginManager.trigger("beforeTransaction");try{const t=await this.transactionManager.execute(e);return await this.pluginManager.trigger("afterTransaction"),t}catch(e){throw this._onError(e),e}}async exportTable(e){return this.ensureReady(),this.engine.find(e,{table:e})}async importTable(e,t){this.ensureReady();try{return await this.engine.insert(e,t)}catch(e){throw this._onError(e),e}}async exportAll(){this.ensureReady();const e={},t=await this.engine.getTableNames();for(const s of t)e[s]=await this.engine.find(s,{table:s});return e}async backup(){return this.ensureReady(),"function"==typeof this.engine.backup?this.engine.backup():this.exportAll()}subscribe(e,t){const s=`change:${e}`;return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(t),()=>this.listeners.get(s)?.delete(t)}emit(e,t){const s=`change:${e}`;this.listeners.get(s)?.forEach(e=>e(t))}broadcastChange(e){if(this.channel)try{this.channel.postMessage({type:"change",table:e})}catch{}}writeStatementTable(e){switch(e.type){case"INSERT":return e.into;case"UPDATE":case"CREATE_INDEX":case"DROP_INDEX":return e.table;case"DELETE":return e.from;case"CREATE_TABLE":case"DROP_TABLE":case"TRUNCATE_TABLE":case"ALTER_TABLE":return e.name;default:return null}}async triggerStatementHooks(e,t,s){switch(e.type){case"INSERT":{let n=e.columns??[];if(0===n.length)try{const t=await this.engine.getTableSchema(e.into);n=t?Object.keys(t.columns):[]}catch{n=[]}const i=(e.values??[]).map(e=>{const t={};for(let s=0;se[0]-t[0]))t<=e&&t>this._version&&(await s(this),this._version=t);if("function"==typeof this.engine.setMeta)try{await this.engine.setMeta("__metona_version",String(this._version))}catch{}}async repair(){if(this.ensureReady(),"function"==typeof this.engine.repair)return await this.engine.repair(),void this.tableCache.clear();this.tableCache.clear()}async clearAll(){if(this.ensureReady(),"function"==typeof this.engine.clearAll)await this.engine.clearAll();else{const e=await this.engine.getTableNames();for(const t of e)await this.engine.dropTable(t)}this.tableCache.clear()}getPluginManager(){return this.pluginManager}on(e,t){this.pluginManager.on(e,t)}async close(){this.channel&&(this.channel.close(),this.channel=null),this.pluginManager.destroy(),this.engine&&await this.engine.close(),this.tableCache.clear(),this.ready=!1}getEngine(){return this.engine}createEngine(){const e=this.mode,t=this.config.diskEngine??"opfs";switch(e){case"memory":return new m;case"disk":return new _;case"aria":return new Ae({storageBackend:"memory"===t?"memory":"kv"===t?"kv":"opfs",...this.config.aria??{}});case"hybrid":return new Re(t);default:throw new n(`Unknown storage mode: ${e}`,"CONFIG_ERROR")}}ensureReady(){if(!this.ready)throw new n("Database not initialized. Call await db.init() first.","DB_NOT_READY")}_onError(e){if(this.config.onError)try{this.config.onError(e)}catch{}}_debug(e,...t){this.debug}}const Xe=new class{constructor(){this.connections=new Map,this.refCount=new Map}async connect(e){const t=e.name,s=this.connections.get(t);if(s&&s.isReady()){const e=(this.refCount.get(t)??0)+1;return this.refCount.set(t,e),s}const n=new Ge(e);return await n.init(),this.connections.set(t,n),this.refCount.set(t,1),n.disconnect=async()=>{await this.release(t)},n}async release(e){const t=(this.refCount.get(e)??1)-1;if(t<=0){const t=this.connections.get(e);t&&(await t.close(),this.connections.delete(e)),this.refCount.delete(e)}else this.refCount.set(e,t)}async forceClose(e){const t=this.connections.get(e);t&&(await t.close(),this.connections.delete(e)),this.refCount.delete(e)}async closeAll(){for(const[,e]of this.connections)try{await e.close()}catch{}this.connections.clear(),this.refCount.clear()}getActiveConnections(){return Array.from(this.connections.keys())}},Je=Ge;async function Qe(e){return Ge.create(e)}Je.connect=e=>Xe.connect(e),Je.disconnect=e=>Xe.release(e),Je.disconnectAll=()=>Xe.closeAll(),Je.getActiveConnections=()=>Xe.getActiveConnections();const Ye={VERSION:i,version:i,create:Qe,MetonaSqlark:Ge,MeSqlark:Ge};"undefined"!=typeof window&&(window.MetonaSqlark=Ye,window.MeSqlark=Ye);const Ze=Ge;e.AriaEngine=Ae,e.HybridEngine=Re,e.KVStoreEngine=_,e.MeSqlark=Ze,e.MemoryEngine=m,e.MetonaSqlark=Ge,e.OPFSBackend=w,e.Table=Le,e.VERSION=i,e.api=Ye,e.create=Qe,e.default=Ye,e.parse=function(e){return new _e(e).parseStatement()},e.parseAll=Me,e.tokenize=function(e){const t=new Ue(e),s=[];let n=t.nextToken();for(;n.type!==$e.EOF;)s.push(n),n=t.nextToken();return s.push(n),s},Object.defineProperty(e,"__esModule",{value:!0})}); diff --git a/jest.config.cjs b/jest.config.cjs index db008a1..fcb0102 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,32 +1,61 @@ -module.exports = { - testEnvironment: 'jsdom', - setupFiles: ['./jest.setup.js'], - transform: { - '^.+\\.ts$': 'babel-jest', - }, - transformIgnorePatterns: [ - '/node_modules/(?!(@rollup)/)', - ], - moduleFileExtensions: ['ts', 'js', 'json'], - // Playwright e2e(tests/e2e/)与共享测试工具(tests/helpers/)不属于 jest 单元测试 - testPathIgnorePatterns: [ - '/node_modules/', - '/tests/e2e/', - '/tests/helpers/', - ], - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/index.ts', - '!src/**/index.ts', - '!src/query/ast.ts', - '!src/engine/interface.ts', - '!src/engine/opfs.ts', - '!src/integrations/**', - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov'], - verbose: true, - // Must have: fake-indexeddb + debounce timers keep event loop alive - forceExit: true, - testTimeout: 15000, -}; +module.exports = { + testEnvironment: 'jsdom', + setupFiles: ['./jest.setup.js'], + transform: { + '^.+\\.ts$': 'babel-jest', + }, + transformIgnorePatterns: [ + '/node_modules/(?!(@rollup)/)', + ], + moduleFileExtensions: ['ts', 'js', 'json'], + // 默认 testMatch 不会发现 __tests__ 之外、名为 helpers/ 目录里的 .test.ts 文件, + // 因此显式声明匹配规则(v0.8.0:测试基座自身的自测放在 tests/helpers/ 下)。 + testMatch: ['**/*.test.ts'], + // Playwright e2e(tests/e2e/)不属于 jest 单元测试 + testPathIgnorePatterns: [ + '/node_modules/', + '/tests/e2e/', + ], + // + // v0.8.0 覆盖率口径修正(根治): + // + // 此前为 `'!src/**/index.ts'` —— 这条 glob 把 15 个文件整体排除,其中包含 + // src/engine/aria/index.ts(AriaEngine 主实现 2282 行)、src/engine/kvstore/index.ts + // (KVStore 本体)、src/hybrid/index.ts、src/migration/index.ts 等**实现文件**, + // 而非桶文件。后果:发布的"90.1% 行覆盖率"只在约 75% 的可执行源码上成立, + // 且 v0.2.6 曾在 CHANGELOG 里承认过同类问题("91.0% 为排除 Aria 模块的陈旧数据") + // 后又在 v0.5.1 以另一种写法复发。 + // + // 现在只排除**确实没有可执行语句**的文件(纯类型声明),并且逐个显式列出、 + // 附理由 —— 新增文件默认纳入统计,不会再被 glob 静默吞掉。 + // + collectCoverageFrom: [ + 'src/**/*.ts', + // 纯类型声明:可执行语句 0 行(接口/类型别名),纳入统计只会稀释分母 + '!src/engine/interface.ts', + '!src/query/ast.ts', + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov'], + // + // v0.8.0 覆盖率门禁(此前完全不存在:jest.config 与 CI 都没有任何阈值, + // 覆盖率从 90% 掉到 60% 也全绿)。 + // + // 阈值 = 2026-08-15 在**修正口径后**(不再排除实现文件)实测的真实基线, + // 下取整到整数百分点。v0.8.0 各阶段会持续补测,M4 收口时再上调。 + // 实测基线:Statements 90.66% / Branches 82.94% / Functions 94.36% / Lines 93.43% + // + coverageThreshold: { + global: { + statements: 90, + branches: 82, + functions: 94, + lines: 93, + }, + }, + verbose: true, + // 注意:forceExit 会掩盖句柄/定时器泄漏。v0.8.0 保留它以便常规开发快速迭代, + // 但新增了独立的 `npm run test:leaks`(--detectOpenHandles 且不 forceExit)用于泄漏检查。 + forceExit: true, + testTimeout: 15000, +}; diff --git a/package.json b/package.json index 75c0c7f..eb51bda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metona-team/metona-sqlark", - "version": "0.7.4", + "version": "0.8.0", "description": "Frontend SQL database with in-memory and disk dual-mode storage", "type": "module", "main": "dist/metona-sqlark.cjs", @@ -35,12 +35,15 @@ "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch", + "test:leaks": "jest --detectOpenHandles --forceExit=false", "test:e2e": "playwright test", - "lint": "eslint \"src/**/*.ts\"", - "lint:fix": "eslint \"src/**/*.ts\" --fix", + "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint:fix": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --fix", "format": "prettier --write \"src/**/*.ts\"", "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run typecheck && npm test && npm run build" + "typecheck:tests": "tsc -p tsconfig.test.json", + "verify": "npm run typecheck && npm run typecheck:tests && npm run lint && npm run test:coverage", + "prepublishOnly": "npm run typecheck && npm run typecheck:tests && npm test && npm run build" }, "repository": { "type": "git", diff --git a/src/constants.ts b/src/constants.ts index ff5551e..defec30 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -214,4 +214,4 @@ export class DatabaseError extends Error { // 版本 // --------------------------------------------------------------------------- -export const VERSION = '0.7.4'; +export const VERSION = '0.8.0'; diff --git a/src/engine/index.ts b/src/engine/index.ts index ae906c7..b65705e 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -8,3 +8,9 @@ export { MemoryEngine } from './memory'; export { KVStoreEngine } from './kvstore_engine'; export { AriaEngine } from './aria/index'; export type { AriaEngineConfig } from './aria/types'; +// v0.8.0: 补齐后端与 KVStore 的桶导出(此前只能深路径 import) +export { KVStore } from './kvstore/index'; +export { SharedMemoryBackend } from './kvstore/shared_memory_medium'; +export { OPFSBackend } from './aria/store/opfs_backend'; +export { MemoryBackend } from './aria/store/backend'; +export type { IStorageBackend } from './aria/store/backend'; diff --git a/src/index.ts b/src/index.ts index 6ceb2e1..f3db628 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,13 +76,23 @@ export const MeSqlark = MetonaSqlark; export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants'; export type { IStorageEngine } from './engine/interface'; export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast'; +// v0.8.0: 补齐公开 API —— 此前 CONTRIBUTING 示例的 +// `import type { MetonaPlugin } from '@metona-team/metona-sqlark'` 并不成立, +// 且 DatabaseError 未导出导致调用方无法 instanceof 判错,只能比 error.code 字符串。 +export { DatabaseError } from './constants'; +export type { MetonaPlugin, HookName, WhereCondition, WhereOperator, QueryPlan, OrderBy } from './constants'; +export { PluginManager } from './plugin/index'; +export type { HookCallback } from './plugin/index'; +export { Transaction } from './transaction/index'; +export { TransactionManager } from './transaction/index'; export { MemoryEngine } from './engine/memory'; export { KVStoreEngine } from './engine/kvstore_engine'; export { AriaEngine } from './engine/aria/index'; export { HybridEngine } from './hybrid/index'; export { Table } from './table/table'; -export { parse, parseAll } from './sql/parser'; +export { parse, parseAll, parseWhereCondition } from './sql/parser'; export { tokenize } from './sql/lexer'; +export { bindParameters } from './sql/params'; // AriaEngine 类型 & 后端 export type { AriaEngineConfig } from './engine/aria/types'; diff --git a/src/query/index.ts b/src/query/index.ts index a9b59c6..9eb8741 100644 --- a/src/query/index.ts +++ b/src/query/index.ts @@ -7,3 +7,10 @@ export type * from './ast'; export { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from './builder'; export { compileStatement } from './compiler'; export { QueryExecutor } from './executor'; +// v0.8.0: WHERE 匹配与排序/投影的统一入口 +export { + matchWhere, + applyOrderBy, + projectColumns, + containsUnresolvedSubqueries, +} from './where-matcher'; diff --git a/src/sql/index.ts b/src/sql/index.ts index c7271e3..334df81 100644 --- a/src/sql/index.ts +++ b/src/sql/index.ts @@ -4,6 +4,8 @@ */ export { Lexer, tokenize } from './lexer'; -export { Parser, parse } from './parser'; +export { Parser, parse, parseAll, parseWhereCondition } from './parser'; +// v0.8.0: 参数绑定此前未从本桶导出(README 与核心 query() 都依赖它) +export { bindParameters } from './params'; export { TokenType } from './tokens'; export type { Token } from './tokens'; diff --git a/tests/aria-cascade.test.ts b/tests/aria-cascade.test.ts index 9af2193..02f1661 100644 --- a/tests/aria-cascade.test.ts +++ b/tests/aria-cascade.test.ts @@ -4,9 +4,9 @@ import { AriaEngine } from '../src/engine/aria/index'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); const mkEngine = async (name: string): Promise => { const e = new AriaEngine({ storageBackend: 'opfs', walSyncMode: 'full' }); diff --git a/tests/e2e/opfs.spec.ts b/tests/e2e/opfs.spec.ts index dc736fb..faff03b 100644 --- a/tests/e2e/opfs.spec.ts +++ b/tests/e2e/opfs.spec.ts @@ -20,10 +20,13 @@ async function openPage(page: Page): Promise { } /** 在页面中执行 harness 方法(统一包装错误信息) */ -async function run(page: Page, method: string, args: unknown): Promise { +async function run(page: Page, method: string, args: unknown = null): Promise { const result = await page.evaluate( - ([m, a]) => (window as unknown as { __ms: Record Promise> }).__ms[m](a), - [method, args] as const, + (payload: [string, unknown]) => { + const [m, a] = payload; + return (window as unknown as { __ms: Record Promise> }).__ms[m](a); + }, + [method, args] as [string, unknown], ); return result as T; } diff --git a/tests/engine/aria-advanced.test.ts b/tests/engine/aria-advanced.test.ts index 119669c..0d15c1f 100644 --- a/tests/engine/aria-advanced.test.ts +++ b/tests/engine/aria-advanced.test.ts @@ -8,11 +8,10 @@ import { WAL, type WALStore } from '../../src/engine/aria/wal/log'; import { WALRecordType } from '../../src/engine/aria/types'; import { BufferPool } from '../../src/engine/aria/buffer/pool'; import { MemoryBackend } from '../../src/engine/aria/store/backend'; -import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — Bloom + WAL + BufferPool', () => { // ---- BloomFilter 完整测试 ---- diff --git a/tests/engine/aria-batch.test.ts b/tests/engine/aria-batch.test.ts index 6140c95..104820f 100644 --- a/tests/engine/aria-batch.test.ts +++ b/tests/engine/aria-batch.test.ts @@ -5,9 +5,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; import { MemoryBackend } from '../../src/engine/aria/store/backend'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 批量扩展测试', () => { let engine: AriaEngine; diff --git a/tests/engine/aria-buffer.test.ts b/tests/engine/aria-buffer.test.ts index c8e249c..403aff7 100644 --- a/tests/engine/aria-buffer.test.ts +++ b/tests/engine/aria-buffer.test.ts @@ -98,8 +98,8 @@ describe('AriaEngine — EvictionManager', () => { } it('access 更新 LRU', async () => { - let evicted = -1; - const em = new EvictionManager(3, async (p) => { evicted = p.pageId; }); + let _evicted = -1; + const em = new EvictionManager(3, async (p) => { _evicted = p.pageId; }); const p = makePage(1); em.add(p); em.access(p); diff --git a/tests/engine/aria-cache.test.ts b/tests/engine/aria-cache.test.ts index 00f8481..237d47d 100644 --- a/tests/engine/aria-cache.test.ts +++ b/tests/engine/aria-cache.test.ts @@ -10,9 +10,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); /** 构造小缓存 + 小 MemTable 阈值的引擎,快速产生多个 SSTable */ function createSmallCacheEngine(bufferPoolPages = 2) { @@ -61,7 +61,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => { for (let round = 0; round < 5; round++) { const rows = await engine.find('users', { table: 'users', where: { age: 25 } }); expect(rows.length).toBe(10); - expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes); + expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit()); } await engine.close(); @@ -82,7 +82,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => { expect(all.length).toBe(300); const lsm = (engine as any).lsm; - expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes); + expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit()); await engine.close(); }); @@ -173,7 +173,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => { for (let batch = 0; batch < 10; batch++) { await engine.insert('users', makeRows(30).map((r, i) => ({ ...r, id: `b${batch}_u${i}` }))); const lsm = (engine as any).lsm; - expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes); + expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit()); } const all = await engine.find('users', { table: 'users' }); diff --git a/tests/engine/aria-checksum.test.ts b/tests/engine/aria-checksum.test.ts index b16adc7..b9cde48 100644 --- a/tests/engine/aria-checksum.test.ts +++ b/tests/engine/aria-checksum.test.ts @@ -9,9 +9,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { diff --git a/tests/engine/aria-edge-ext.test.ts b/tests/engine/aria-edge-ext.test.ts index 70aa4cc..1251fb7 100644 --- a/tests/engine/aria-edge-ext.test.ts +++ b/tests/engine/aria-edge-ext.test.ts @@ -5,9 +5,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 扩展边缘测试', () => { let engine: AriaEngine; @@ -69,7 +69,7 @@ describe('AriaEngine — 扩展边缘测试', () => { }); it('多列 ORDER BY', async () => { - const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'asc' }, { column: 'score', direction: 'desc' }] }); + const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'asc' }, { column: 'score', direction: 'desc' }] }) as Array<{ age: number }>; expect(rows[0].age).toBeLessThanOrEqual(rows[4].age); }); diff --git a/tests/engine/aria-encryption.test.ts b/tests/engine/aria-encryption.test.ts index 904e3df..5374932 100644 --- a/tests/engine/aria-encryption.test.ts +++ b/tests/engine/aria-encryption.test.ts @@ -15,9 +15,10 @@ import { EncryptedBackend } from '../../src/engine/aria/store/encrypted_backend' import { MemoryBackend } from '../../src/engine/aria/store/backend'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; +import { decode as decodeBytes } from '../helpers/assertions'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -48,11 +49,11 @@ describe('AriaEngine — EncryptedBackend 单元', () => { await backend.write('k1', payload); const back = await backend.read('k1'); expect(back).not.toBeNull(); - expect(new TextDecoder().decode(back)).toBe('hello encrypted world'); + expect(decodeBytes(back)).toBe('hello encrypted world'); // 底层是密文(非明文) const raw = await inner.read('k1'); - const rawStr = new TextDecoder().decode(raw); + const rawStr = decodeBytes(raw, 'ciphertext'); expect(rawStr).not.toContain('hello encrypted world'); await backend.close(); @@ -65,8 +66,8 @@ describe('AriaEngine — EncryptedBackend 单元', () => { const enc = (s: string) => new TextEncoder().encode(s).buffer; await backend.writeMany({ a: enc('AAA'), b: enc('BBB'), c: enc('CCC') }); - expect(new TextDecoder().decode(await backend.read('a'))).toBe('AAA'); - expect(new TextDecoder().decode(await backend.read('c'))).toBe('CCC'); + expect(decodeBytes(await backend.read('a'))).toBe('AAA'); + expect(decodeBytes(await backend.read('c'))).toBe('CCC'); await backend.deleteMany(['a', 'c']); expect(await backend.exists('a')).toBe(false); @@ -94,8 +95,8 @@ describe('AriaEngine — EncryptedBackend 单元', () => { expect((rawX as ArrayBuffer).byteLength).toBe((rawY as ArrayBuffer).byteLength); // 但解密一致 - expect(new TextDecoder().decode(await backend.read('x'))).toBe('same plaintext'); - expect(new TextDecoder().decode(await backend.read('y'))).toBe('same plaintext'); + expect(decodeBytes(await backend.read('x'))).toBe('same plaintext'); + expect(decodeBytes(await backend.read('y'))).toBe('same plaintext'); await backend.close(); }); @@ -115,7 +116,7 @@ describe('AriaEngine — EncryptedBackend 单元', () => { const backend2 = new EncryptedBackend(inner, 'pw'); await backend2.open('enc-unit-4'); await backend2.write('k2', new TextEncoder().encode('v2').buffer); - expect(new TextDecoder().decode(await backend2.read('k2'))).toBe('v2'); + expect(decodeBytes(await backend2.read('k2'))).toBe('v2'); // 换密码被拒(keymeta 与旧密码绑定)——注意:MemoryBackend close 清空 store, // 因此在 close 前验证(backend2 仍持有 inner) diff --git a/tests/engine/aria-extra.test.ts b/tests/engine/aria-extra.test.ts index fe7b42e..35ade05 100644 --- a/tests/engine/aria-extra.test.ts +++ b/tests/engine/aria-extra.test.ts @@ -4,9 +4,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 补充测试', () => { let engine: AriaEngine; diff --git a/tests/engine/aria-final.test.ts b/tests/engine/aria-final.test.ts index dece81f..2c01635 100644 --- a/tests/engine/aria-final.test.ts +++ b/tests/engine/aria-final.test.ts @@ -7,9 +7,9 @@ import { createSchema } from '../../src/table/schema'; import { MemoryBackend } from '../../src/engine/aria/store/backend'; import { checkFieldType } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 最终扩展测试', () => { let engine: AriaEngine; diff --git a/tests/engine/aria-idx-flush-race.test.ts b/tests/engine/aria-idx-flush-race.test.ts index 0a4ae11..504a59f 100644 --- a/tests/engine/aria-idx-flush-race.test.ts +++ b/tests/engine/aria-idx-flush-race.test.ts @@ -8,14 +8,14 @@ */ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; let counter = 0; function uniqueDB(): string { return `idxrace-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`; } -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 二级索引完整性(P0 回归)', () => { it('5 万行写入:索引查询与主表一致(修复前丢 106~771 条)', async () => { diff --git a/tests/engine/aria-index-lookup.test.ts b/tests/engine/aria-index-lookup.test.ts index 449d943..f65dbd3 100644 --- a/tests/engine/aria-index-lookup.test.ts +++ b/tests/engine/aria-index-lookup.test.ts @@ -5,9 +5,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — 二级索引查询', () => { let engine: AriaEngine; diff --git a/tests/engine/aria-kv-backend.test.ts b/tests/engine/aria-kv-backend.test.ts index e887e77..7106ad7 100644 --- a/tests/engine/aria-kv-backend.test.ts +++ b/tests/engine/aria-kv-backend.test.ts @@ -275,7 +275,7 @@ describe('MetonaSqlark + diskEngine: kv(高层 API)', () => { }); // 事务回滚 - await expect(db.transaction(async (trx) => { + await expect(db.transaction(async (trx: { table: (n: string) => any }) => { await trx.table('users').insert({ id: '1' }); throw new Error('boom'); })).rejects.toThrow('boom'); diff --git a/tests/engine/aria-locks.test.ts b/tests/engine/aria-locks.test.ts index a9a7502..6d8f392 100644 --- a/tests/engine/aria-locks.test.ts +++ b/tests/engine/aria-locks.test.ts @@ -12,9 +12,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { DatabaseLock, lockName } from '../../src/engine/aria/locks'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { diff --git a/tests/engine/aria-maintenance.test.ts b/tests/engine/aria-maintenance.test.ts index 710c100..c837854 100644 --- a/tests/engine/aria-maintenance.test.ts +++ b/tests/engine/aria-maintenance.test.ts @@ -7,9 +7,10 @@ import { createSchema } from '../../src/table/schema'; import { QueryExecutor } from '../../src/query/executor'; import { parse } from '../../src/sql/parser'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; +import { object } from '../helpers/assertions'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — ANALYZE/VACUUM/REINDEX + EXPLAIN', () => { let engine: AriaEngine; @@ -78,7 +79,9 @@ describe('AriaEngine — ANALYZE/VACUUM/REINDEX + EXPLAIN', () => { const executor = new QueryExecutor(engine); const selectStmt = parse('SELECT * FROM users WHERE id = \'1\''); const explainStmt: any = { type: 'EXPLAIN', query: selectStmt }; - const plan = await executor.execute(explainStmt); + const plan = object<{ type: string; table: string; usingIndex?: unknown; actualTimeMs: number }>( + await executor.execute(explainStmt), + ); expect(plan.type).toBe('SELECT'); expect(plan.table).toBe('users'); expect(plan.usingIndex).toBeDefined(); diff --git a/tests/engine/aria-matrix-audit.test.ts b/tests/engine/aria-matrix-audit.test.ts index b11bb7a..890bf81 100644 --- a/tests/engine/aria-matrix-audit.test.ts +++ b/tests/engine/aria-matrix-audit.test.ts @@ -9,7 +9,7 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; let counter = 0; function uniqueDB(): string { @@ -56,7 +56,7 @@ async function reopen(dbName: string, config: Record): Promise< beforeEach(() => { SharedMemoryBackend.clearRegistry(); - installOPFSMock(new Map()); + resetOPFSMock(); }); describe('生产矩阵审计 — 后端 × 核心功能', () => { diff --git a/tests/engine/aria-mvcc-savepoint.test.ts b/tests/engine/aria-mvcc-savepoint.test.ts index 95fdb18..8cccf95 100644 --- a/tests/engine/aria-mvcc-savepoint.test.ts +++ b/tests/engine/aria-mvcc-savepoint.test.ts @@ -5,9 +5,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); describe('AriaEngine — MVCC 事务 + Savepoint', () => { let engine: AriaEngine; diff --git a/tests/engine/aria-opfs-backend.test.ts b/tests/engine/aria-opfs-backend.test.ts index d4c0e28..00eb34f 100644 --- a/tests/engine/aria-opfs-backend.test.ts +++ b/tests/engine/aria-opfs-backend.test.ts @@ -10,82 +10,28 @@ * 6. open 清理崩溃残留临时文件(.crswap/.tmp) * 7. writeMany/deleteMany 语义 */ + +// =================================================================== +// v0.8.0: 删除本文件内的第三份 OPFS mock —— 改用共享的 storage-harness。 +// +// 原实现的问题(见 PLAN-v0.7.5.md 工作流 C-1): +// - 与 tests/helpers/opfs-mock.ts 重复(两份都在测同一件事,语义各自漂移); +// - `close()` 是空函数、写入立即可见 → 无法表达"提交前崩溃"; +// - 定义了 `writeCalls` 记录但**从未被任何断言使用**(死代码); +// - `entry.content.subarray ? entry.content : entry.content` 是恒等表达式(无意义分支)。 +// +// 共享 harness 提供:真实提交语义(close 才可见)、读返回副本、 +// keepExistingData:false 截断、字节级故障注入、真崩溃模拟。 +// =================================================================== import { OPFSBackend } from '../../src/engine/aria/store/opfs_backend'; - -// =================================================================== -// 真实语义 OPFS mock:记录文件内容、支持 keepExistingData+position -// =================================================================== -interface MockFile { - content: ArrayBuffer; - writeCalls: { position?: number; data: ArrayBuffer; keepExistingData?: boolean }[]; -} - -function createOPFSMock() { - const files = new Map(); - - const getFileHandle = async (name: string, opts?: { create?: boolean }) => { - if (!files.has(name)) { - if (!opts?.create) throw new Error(`NotFoundError: ${name}`); - files.set(name, { content: new ArrayBuffer(0), writeCalls: [] }); - } - const entry = files.get(name)!; - return { - getFile: async () => ({ size: entry.content.byteLength, arrayBuffer: async () => entry.content }), - createWritable: async (wOpts?: { keepExistingData?: boolean }) => { - const w: { - write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise; - close: () => Promise; - } = { - write: async (arg) => { - const keepExisting = wOpts?.keepExistingData ?? false; - const isChunk = typeof arg !== 'object' || !('type' in (arg as object)) || (arg as { type?: string }).type === undefined - ? { data: arg as ArrayBuffer, position: keepExisting ? entry.content.byteLength : 0 } - : { data: (arg as { data: ArrayBuffer }).data, position: (arg as { position: number }).position }; - entry.writeCalls.push({ position: isChunk.position, data: isChunk.data, keepExistingData: keepExisting }); - const merged = new Uint8Array(isChunk.position + isChunk.data.byteLength); - if (keepExisting || isChunk.position > 0) { - merged.set(new Uint8Array(entry.content.subarray ? entry.content : entry.content), 0); - } - merged.set(new Uint8Array(isChunk.data), isChunk.position); - entry.content = merged.buffer; - }, - close: async () => { /* no-op */ }, - }; - return w; - }, - }; - }; - - const dir = { - getFileHandle, - entries: async function* () { - for (const [name] of files) yield [name]; - }, - removeEntry: async (name: string) => { - files.delete(name); - }, - }; - - Object.defineProperty(globalThis, 'navigator', { - value: { - storage: { - getDirectory: async () => ({ - getDirectoryHandle: async (_name: string, _opts?: unknown) => dir, - }), - }, - }, - configurable: true, - writable: true, - }); - - return { files, dir }; -} +import { resetOPFSMock } from '../helpers/storage-harness'; +import { decode as decodeBytes } from '../helpers/assertions'; const enc = (s: string) => new Uint8Array(new TextEncoder().encode(s)).buffer; describe('AriaEngine — OPFSBackend v2', () => { beforeEach(() => { - createOPFSMock(); + resetOPFSMock(); }); it('read/write/exists/delete 基本语义', async () => { @@ -95,7 +41,7 @@ describe('AriaEngine — OPFSBackend v2', () => { await backend.write('k1', enc('hello')); expect(await backend.exists('k1')).toBe(true); - expect(new TextDecoder().decode(await backend.read('k1'))).toBe('hello'); + expect(decodeBytes(await backend.read('k1'))).toBe('hello'); expect(await backend.read('missing')).toBeNull(); await backend.delete('k1'); @@ -111,7 +57,7 @@ describe('AriaEngine — OPFSBackend v2', () => { await backend.append('wal', enc('BBB')); await backend.append('wal', enc('CCC')); const all = await backend.read('wal'); - expect(new TextDecoder().decode(all)).toBe('AAABBBCCC'); + expect(decodeBytes(all)).toBe('AAABBBCCC'); await backend.close(); }); @@ -120,7 +66,7 @@ describe('AriaEngine — OPFSBackend v2', () => { await backend.open('opfs-test-3'); await backend.write('f', enc('OLD-CONTENT')); await backend.write('f', enc('NEW')); - expect(new TextDecoder().decode(await backend.read('f'))).toBe('NEW'); + expect(decodeBytes(await backend.read('f'))).toBe('NEW'); await backend.close(); }); @@ -132,7 +78,7 @@ describe('AriaEngine — OPFSBackend v2', () => { const p2 = backend.append('log', enc('B')); const p3 = backend.append('log', enc('C')); await Promise.all([p1, p2, p3]); - expect(new TextDecoder().decode(await backend.read('log'))).toBe('ABC'); + expect(decodeBytes(await backend.read('log'))).toBe('ABC'); await backend.close(); }); @@ -157,7 +103,7 @@ describe('AriaEngine — OPFSBackend v2', () => { await expect(origWrite('boom', enc('x'))).rejects.toThrow('Injected write failure'); // 队列链恢复:后续写成功 await backend.write('ok', enc('fine')); - expect(new TextDecoder().decode(await backend.read('ok'))).toBe('fine'); + expect(decodeBytes(await backend.read('ok'))).toBe('fine'); await backend.close(); }); @@ -189,7 +135,7 @@ describe('AriaEngine — OPFSBackend v2', () => { expect(keys).toEqual(['data']); expect(keys.some((k) => k.endsWith('.crswap') || k.endsWith('.tmp'))).toBe(false); // 正常数据不受影响 - expect(new TextDecoder().decode(await backend2.read('data'))).toBe('real'); + expect(decodeBytes(await backend2.read('data'))).toBe('real'); await backend2.close(); }); @@ -197,8 +143,8 @@ describe('AriaEngine — OPFSBackend v2', () => { const backend = new OPFSBackend(); await backend.open('opfs-test-8'); await backend.writeMany({ a: enc('AAA'), b: enc('BBB') }); - expect(new TextDecoder().decode(await backend.read('a'))).toBe('AAA'); - expect(new TextDecoder().decode(await backend.read('b'))).toBe('BBB'); + expect(decodeBytes(await backend.read('a'))).toBe('AAA'); + expect(decodeBytes(await backend.read('b'))).toBe('BBB'); await backend.deleteMany(['a']); expect(await backend.exists('a')).toBe(false); expect(await backend.exists('b')).toBe(true); diff --git a/tests/engine/aria-page-store.test.ts b/tests/engine/aria-page-store.test.ts index 941346d..7e9eef8 100644 --- a/tests/engine/aria-page-store.test.ts +++ b/tests/engine/aria-page-store.test.ts @@ -20,7 +20,8 @@ import { PAGE_SIZE } from '../../src/engine/aria/types'; // =================================================================== // OPFS mock(共享工具) // =================================================================== -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; +import { decode as decodeBytes } from '../helpers/assertions'; const SCHEMA = () => createSchema('users', { id: { type: 'string', primaryKey: true }, @@ -47,7 +48,7 @@ describe('AriaEngine — PageSSTableStore 单元', () => { const back = await store.load(1, pageIds, data.byteLength); expect(back).not.toBeNull(); - expect(new TextDecoder().decode(back)).toBe('hello page store'); + expect(decodeBytes(back)).toBe('hello page store'); // 页面已落盘(backend 有 pg_ 键) expect(await backend.exists('pg_1')).toBe(true); @@ -130,7 +131,7 @@ describe('AriaEngine — PageSSTableStore 单元', () => { // =================================================================== describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { it('写入 → flush → close → reopen 数据完整(页面模式默认启用)', async () => { - const files = installOPFSMock(new Map()); + resetOPFSMock(); const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024, @@ -173,7 +174,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { }); it('多级 compaction 后页面化数据仍完整', async () => { - const files = installOPFSMock(new Map()); + resetOPFSMock(); const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 16 * 1024, @@ -204,7 +205,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { }); it('页面损坏(篡改 pg_ 文件)→ 打开自愈清理,其余数据可读', async () => { - const files = installOPFSMock(new Map()); + resetOPFSMock(); const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 64 * 1024 * 1024, @@ -245,7 +246,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { }); it('页面模式与非页面模式混合兼容(pageIds 缺失 → 整 value 读取)', async () => { - const { files } = installOPFSMock(new Map()); + const opfs = resetOPFSMock(); // 阶段 1:非页面模式写入(pageStorage: false → 整 value SSTable) const engine = new AriaEngine({ storageBackend: 'opfs', @@ -259,7 +260,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { await (engine as any).lsm.flush(); await engine.close(); - const backendKeys1 = Array.from(files.keys()); + const backendKeys1 = await opfs.listKeys(); expect(backendKeys1.some((k) => k.startsWith('sst_'))).toBe(true); // 阶段 2:页面模式打开(默认 opfs → 启用),读旧数据 + 写新数据 @@ -285,7 +286,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => { }); it('页面化 + 加密 + 压缩组合:往返完整', async () => { - const files = installOPFSMock(new Map()); + resetOPFSMock(); const engine = new AriaEngine({ storageBackend: 'opfs', compression: true, diff --git a/tests/engine/aria-prod-load.test.ts b/tests/engine/aria-prod-load.test.ts index 2c47d76..29d98ff 100644 --- a/tests/engine/aria-prod-load.test.ts +++ b/tests/engine/aria-prod-load.test.ts @@ -14,7 +14,7 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; let counter = 0; function uniqueDB(): string { @@ -30,7 +30,7 @@ const SCHEMA = () => createSchema('big', { beforeEach(() => { SharedMemoryBackend.clearRegistry(); - installOPFSMock(new Map()); + resetOPFSMock(); }); describe('AriaEngine — 生产负载验证', () => { @@ -317,6 +317,7 @@ describe('AriaEngine — 生产负载验证', () => { // 修复后本机 ~12.5s。CI(debian runner 慢 2~3 倍、重型套件串行)下 // 健康耗时约 30~80s;护栏放宽到 240s —— 仍能拦截性能悬崖回归(353s >> 240s), // 不误报健康慢环境。 + // eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时 console.log(`10万行 kv 插入耗时: ${insertMs}ms`); expect(insertMs).toBeLessThan(240000); expect(await engine.count('big')).toBe(TOTAL); @@ -364,6 +365,7 @@ describe('AriaEngine — 生产负载验证', () => { } const insertMs = Date.now() - t0; // 同上:CI 慢环境护栏放宽(本机 ~25s;悬崖回归仍会被拦截) + // eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时 console.log(`10万行 opfs 插入耗时: ${insertMs}ms`); expect(insertMs).toBeLessThan(300000); expect(await engine.count('big')).toBe(TOTAL); diff --git a/tests/engine/aria-repair-hardening.test.ts b/tests/engine/aria-repair-hardening.test.ts index 497bc25..a255d06 100644 --- a/tests/engine/aria-repair-hardening.test.ts +++ b/tests/engine/aria-repair-hardening.test.ts @@ -11,9 +11,9 @@ */ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -65,20 +65,20 @@ describe('AriaEngine — repair 自愈增强', () => { it('清理 OPFS 残留临时文件(.crswap/.tmp)', async () => { // 用 OPFS mock 后端验证 cleanupStaleFiles 被调用 - const { files, dir } = installOPFSMock(new Map()); + const opfs = resetOPFSMock(); const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 }); await engine.open('repair-opfs-1', 1); await engine.createTable(SCHEMA()); await engine.insert('items', [{ id: 'a', val: 1, tag: 'x' }]); - // 制造残留 - await dir.getFileHandle('junk.crswap', { create: true }); - await dir.getFileHandle('junk2.tmp', { create: true }); - expect(Array.from(files.keys()).some((k) => k.endsWith('.crswap'))).toBe(true); + // 制造残留(模拟 createWritable 中断留下的临时文件) + await opfs.createFile('junk.crswap'); + await opfs.createFile('junk2.tmp'); + expect((await opfs.listKeys()).some((k) => k.endsWith('.crswap'))).toBe(true); await (engine as any).repair(); - const after = Array.from(files.keys()); + const after = await opfs.listKeys(); expect(after.some((k) => k.endsWith('.crswap'))).toBe(false); expect(after.some((k) => k.endsWith('.tmp'))).toBe(false); expect(await engine.count('items')).toBe(1); @@ -193,7 +193,7 @@ describe('AriaEngine — 随机操作压力 + 模拟崩溃', () => { }); it('随机操作 + 页面化 + 模拟崩溃 → 重开验证', async () => { - const files = installOPFSMock(new Map()); + resetOPFSMock(); const dbName = `rand-opfs-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; const engine = new AriaEngine({ storageBackend: 'opfs', diff --git a/tests/engine/aria-wal-segment.test.ts b/tests/engine/aria-wal-segment.test.ts index 1851fab..c054566 100644 --- a/tests/engine/aria-wal-segment.test.ts +++ b/tests/engine/aria-wal-segment.test.ts @@ -14,9 +14,9 @@ import { MemoryBackend } from '../../src/engine/aria/store/backend'; import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { diff --git a/tests/engine/aria.test.ts b/tests/engine/aria.test.ts index ca34423..936a97a 100644 --- a/tests/engine/aria.test.ts +++ b/tests/engine/aria.test.ts @@ -9,9 +9,9 @@ import { AriaEngine } from '../../src/engine/aria/index'; import { createSchema } from '../../src/table/schema'; import { MetonaSqlark } from '../../src/core'; -import { installOPFSMock } from '../helpers/opfs-mock'; +import { resetOPFSMock } from '../helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); // =================================================================== // AriaEngine 引擎级测试 (Memory Backend) diff --git a/tests/engine/kvstore.test.ts b/tests/engine/kvstore.test.ts index c7ce868..79b9534 100644 --- a/tests/engine/kvstore.test.ts +++ b/tests/engine/kvstore.test.ts @@ -24,7 +24,7 @@ function uniqueDB(): string { } const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer; -const dec = (b: ArrayBuffer | null) => (b ? new TextDecoder().decode(b) : null); +const dec = (b: ArrayBuffer | null | undefined) => (b ? new TextDecoder().decode(b) : null); beforeEach(() => { SharedMemoryBackend.clearRegistry(); diff --git a/tests/foreign-key-cascade.test.ts b/tests/foreign-key-cascade.test.ts index d86c2d8..25dbf05 100644 --- a/tests/foreign-key-cascade.test.ts +++ b/tests/foreign-key-cascade.test.ts @@ -117,7 +117,7 @@ describe('v0.1.13 外键级联 — ON DELETE CASCADE', () => { it('删除没有订单的用户 → 不会级联删除', async () => { // 先查总数 - const userCount = await db.table('users').count(); + const _userCount = await db.table('users').count(); const orderCount = await db.table('orders').count(); // 删除一个没有任何订单的用户(创建临时用户) diff --git a/tests/groupby.test.ts b/tests/groupby.test.ts index ab6c8ae..44f0803 100644 --- a/tests/groupby.test.ts +++ b/tests/groupby.test.ts @@ -4,6 +4,14 @@ import { MetonaSqlark } from '../src/core'; import { parse } from '../src/sql/parser'; +import type { SelectStatement } from '../src/query/ast'; + +/** v0.8.0: parse() 返回联合类型 Statement,需显式收窄后才能访问 SELECT 字段 */ +function asSelect(stmt: ReturnType): SelectStatement { + expect(stmt.type).toBe('SELECT'); + if (stmt.type !== 'SELECT') throw new Error(`expected SELECT, got ${stmt.type}`); + return stmt; +} describe('GROUP BY (v0.1.2)', () => { let db: MetonaSqlark; @@ -101,18 +109,19 @@ describe('GROUP BY (v0.1.2)', () => { describe('Parser', () => { it('解析 COUNT(*)', () => { - const ast = parse('SELECT COUNT(*) FROM employees'); + const ast = asSelect(parse('SELECT COUNT(*) FROM employees')); expect(ast.columns[0]).toBe('COUNT(*)'); }); it('解析 GROUP BY', () => { - const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept'); + const ast = asSelect(parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept')); expect(ast.groupBy).toEqual(['dept']); }); it('解析 HAVING', () => { - const ast = parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1'); - expect(ast.having).toBeDefined(); + const ast = asSelect(parse('SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 1')); + // v0.8.0: 断言真实结构而非 toBeDefined() + expect(ast.having).toEqual({ 'COUNT(*)': { $gt: 1 } }); }); }); }); diff --git a/tests/helpers/assertions.ts b/tests/helpers/assertions.ts new file mode 100644 index 0000000..064f1b4 --- /dev/null +++ b/tests/helpers/assertions.ts @@ -0,0 +1,81 @@ +/** + * 测试共享 — 类型化断言辅助(v0.8.0) + * + * 背景:v0.8.0 起 tests/ 纳入类型检查(tsconfig.test.json)。测试里最常见的两类 + * 类型错误是: + * 1. `backend.read(key)` 返回 `ArrayBuffer | null` → 直接喂给 TextDecoder 报错; + * 2. `db.query()` 返回 `unknown` → 取 `.length` / `[0].field` 报错。 + * 旧做法是 `as any`,那等于把类型检查关掉;这里提供**断言 + 收窄**的辅助函数: + * 断言失败会明确报出"期望非空",而不是让后续断言落空。 + * + * 用法: + * expect(decode(await backend.read('k'))).toBe('v'); + * const rows = rows<{ id: string }>(await db.query('SELECT * FROM t')); + */ + +/** 断言非空并收窄(失败信息指向调用点,便于定位) */ +export function nonNull(value: T | null | undefined, what = 'value'): T { + if (value === null || value === undefined) { + throw new Error(`expected ${what} to be non-null, got ${String(value)}`); + } + return value; +} + +/** 解码字节为字符串(自动处理 null → 明确失败) */ +export function decode(value: ArrayBuffer | Uint8Array | null | undefined, what = 'buffer'): string { + if (value === null || value === undefined) { + throw new Error(`expected ${what} to be non-null for decoding`); + } + return new TextDecoder().decode(value instanceof Uint8Array ? value : new Uint8Array(value)); +} + +/** 把 query/executor 的 unknown 结果收窄为行数组 */ +export function rows>(value: unknown): T[] { + if (!Array.isArray(value)) { + throw new Error(`expected query result to be an array, got ${typeof value}`); + } + return value as T[]; +} + +/** 把 query 结果收窄为单行(断言至少一行并返回首行) */ +export function firstRow>(value: unknown): T { + const list = rows(value); + if (list.length === 0) throw new Error('expected at least one row, got empty result'); + return list[0]; +} + +/** 把 query 结果收窄为对象(EXPLAIN 等返回单对象的场景) */ +export function object>(value: unknown): T { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`expected an object result, got ${Array.isArray(value) ? 'array' : typeof value}`); + } + return value as T; +} + +/** 断言并收窄错误码(替代 `catch { threw = true }` 这种无法区分原因的写法) */ +export async function expectCode(promise: Promise, code: string): Promise { + try { + await promise; + } catch (error) { + const actual = (error as { code?: string }).code; + if (actual !== code) { + throw new Error(`expected error code "${code}", got "${actual ?? '(none)'}": ${(error as Error).message}`); + } + return; + } + throw new Error(`expected rejection with code "${code}", but the promise resolved`); +} + +/** + * 取用引擎上的**可选**方法(如 clearAll / backup / repair / createIndex)。 + * + * `IStorageEngine` 把这些方法声明为可选,直接 `engine.clearAll()` 在类型上不成立。 + * 测试里需要它们时用本函数:缺失即明确失败,而不是运行时 TypeError。 + */ +export function engineMethod(engine: unknown, name: string): M { + const fn = (engine as Record | null)?.[name]; + if (typeof fn !== 'function') { + throw new Error(`engine is missing required test method "${name}"`); + } + return (fn as (...a: unknown[]) => unknown).bind(engine) as M; +} diff --git a/tests/helpers/faulty-backend.ts b/tests/helpers/faulty-backend.ts new file mode 100644 index 0000000..8fcb49e --- /dev/null +++ b/tests/helpers/faulty-backend.ts @@ -0,0 +1,242 @@ +/** + * 测试共享 — 故障注入后端包装器(v0.8.0 工作流 C-1) + * + * ============================================================================ + * 为什么需要它(PLAN-v0.7.5.md 根因 5) + * ============================================================================ + * 项目所有"崩溃恢复"测试用的都是 `await engine.backend.close()`,而 OPFSBackend.close() + * 会 `await this.writeQueue` 把在途写**全部刷完** —— 那是优雅停机,不是崩溃。 + * 加上 mock 永远原子、绝不撕裂,崩溃相关的声称在结构上无法被验证。 + * + * 本包装器让任意 IStorageBackend 具备可编程故障: + * - failNextWrite(n) / failNextAppend(n) / failNextDelete(n) 抛错注入 + * - truncateNextAppendTo(n) 撕裂写(只落前 n 字节) + * - dropNextAppend(n) / dropNextWrite(n) 静默丢弃(模拟掉电丢失) + * - corruptNextWrite(mutator) 写入落盘后篡改字节 + * - crash() 丢弃未提交写(委托介质) + * + * 用法: + * const faulty = new FaultyBackend(new OPFSBackend()); + * await faulty.open('db'); + * faulty.failNextAppend(); // 下一次追加失败 + * ... 触发写入 ... + * await expect(...).rejects.toThrow(); + * faulty.clearFaults(); + */ + +import type { IStorageBackend } from '../../src/engine/aria/store/backend'; + +type Mutator = (bytes: Uint8Array) => void; + +interface Faults { + failWrite: number; + failAppend: number; + failDelete: number; + dropWrite: number; + dropAppend: number; + truncateAppendTo: number; + corruptWrite: Mutator | null; +} + +/** 支持崩溃模拟的介质(OPFS mock / TransactionalFileStore 等) */ +export interface CrashableMedium { + crashPending(): void; + hasPending(): boolean; +} + +/** 支持崩溃模拟的后端(OPFSBackend 等在 v0.8.0 提供了 simulateCrash) */ +export interface CrashableBackend { + simulateCrash(): void; +} + +function isCrashableMedium(m: unknown): m is CrashableMedium { + return typeof (m as CrashableMedium)?.crashPending === 'function'; +} + +function isCrashableBackend(m: unknown): m is CrashableBackend { + return typeof (m as CrashableBackend)?.simulateCrash === 'function'; +} + +export class FaultyBackend implements IStorageBackend { + private readonly inner: IStorageBackend; + private faults: Faults = { + failWrite: 0, + failAppend: 0, + failDelete: 0, + dropWrite: 0, + dropAppend: 0, + truncateAppendTo: -1, + corruptWrite: null, + }; + + /** 注入统计(测试可断言注入真的生效了,避免"注入了但没走到"的假绿) */ + readonly injected = { write: 0, append: 0, delete: 0, dropped: 0, truncated: 0, corrupted: 0 }; + + constructor(inner: IStorageBackend) { + this.inner = inner; + } + + /** 暴露内层(需要访问具体 backing store 时使用,例如 crash()) */ + unwrap(): T { + return this.inner as T; + } + + // ---- 故障注入 API ---- + + failNextWrite(n = 1): void { this.faults.failWrite = n; } + failNextAppend(n = 1): void { this.faults.failAppend = n; } + failNextDelete(n = 1): void { this.faults.failDelete = n; } + /** 静默丢弃接下来 n 次 write(不抛错、"看起来成功",模拟掉电丢失) */ + dropNextWrite(n = 1): void { this.faults.dropWrite = n; } + /** 静默丢弃接下来 n 次 append */ + dropNextAppend(n = 1): void { this.faults.dropAppend = n; } + /** 下一次 append 只落前 n 字节(撕裂写) */ + truncateNextAppendTo(n: number): void { this.faults.truncateAppendTo = n; } + /** 下一次写入的字节落盘后被就地篡改(模拟 bit flip) */ + corruptNextWrite(mutator: Mutator): void { this.faults.corruptWrite = mutator; } + + clearFaults(): void { + this.faults = { + failWrite: 0, failAppend: 0, failDelete: 0, + dropWrite: 0, dropAppend: 0, truncateAppendTo: -1, corruptWrite: null, + }; + } + + /** + * 模拟崩溃:优先让内层后端自己处理(OPFSBackend.simulateCrash 会丢弃未提交的 swap 写入), + * 否则若内层介质本身可崩溃(TransactionalFileStore 等)则直接委托。 + * + * 注意:**不要**用 inner.close() 代替崩溃 —— 那会把写队列刷完(优雅停机)。 + * 返回 false 表示"该后端无法模拟崩溃",调用方应据此改用丢写注入表达崩溃。 + */ + crash(): boolean { + if (isCrashableBackend(this.inner)) { + this.inner.simulateCrash(); + return true; + } + if (isCrashableMedium(this.inner)) { + this.inner.crashPending(); + return true; + } + return false; + } + + // ---- IStorageBackend 委托 ---- + + open(name: string): Promise { + return this.inner.open(name); + } + + close(): Promise { + return this.inner.close(); + } + + isOpen(): boolean { + return this.inner.isOpen(); + } + + read(key: string): Promise { + return this.inner.read(key); + } + + async write(key: string, data: ArrayBuffer): Promise { + if (this.faults.dropWrite > 0) { + this.faults.dropWrite--; + this.injected.dropped++; + return; // 静默成功但没落盘 + } + if (this.faults.failWrite > 0) { + this.faults.failWrite--; + this.injected.write++; + throw new Error(`[FaultyBackend] injected write failure: ${key}`); + } + await this.inner.write(key, data); + if (this.faults.corruptWrite) { + const mutator = this.faults.corruptWrite; + this.faults.corruptWrite = null; + const stored = await this.inner.read(key); + if (stored) { + mutator(new Uint8Array(stored)); + await this.inner.write(key, stored); + this.injected.corrupted++; + } + } + } + + async append(key: string, data: ArrayBuffer): Promise { + if (this.faults.dropAppend > 0) { + this.faults.dropAppend--; + this.injected.dropped++; + return; + } + if (this.faults.failAppend > 0) { + this.faults.failAppend--; + this.injected.append++; + throw new Error(`[FaultyBackend] injected append failure: ${key}`); + } + if (this.faults.truncateAppendTo >= 0) { + const n = this.faults.truncateAppendTo; + this.faults.truncateAppendTo = -1; + this.injected.truncated++; + // 撕裂写:只把前 n 字节交给介质 + // (不用 ArrayBuffer.slice —— jsdom 下可能被 Blob.slice 语义遮蔽,见 storage-harness 注释) + const len = Math.max(0, Math.min(n, data.byteLength)); + const torn = new Uint8Array(len); + torn.set(new Uint8Array(data, 0, len)); + const tornBuf = torn.buffer as ArrayBuffer; + if (typeof this.inner.append === 'function') { + await this.inner.append(key, tornBuf); + } else { + const existing = await this.inner.read(key); + const merged = new Uint8Array((existing?.byteLength ?? 0) + tornBuf.byteLength); + if (existing) merged.set(new Uint8Array(existing), 0); + merged.set(new Uint8Array(tornBuf), existing?.byteLength ?? 0); + await this.inner.write(key, merged.buffer as ArrayBuffer); + } + return; + } + if (typeof this.inner.append === 'function') { + await this.inner.append(key, data); + } else { + const existing = await this.inner.read(key); + const merged = new Uint8Array((existing?.byteLength ?? 0) + data.byteLength); + if (existing) merged.set(new Uint8Array(existing), 0); + merged.set(new Uint8Array(data), existing?.byteLength ?? 0); + await this.inner.write(key, merged.buffer as ArrayBuffer); + } + } + + async writeMany(entries: Record): Promise { + // 逐 key 走出本包装的 write,使注入对批量写同样生效 + for (const [key, data] of Object.entries(entries)) { + await this.write(key, data); + } + } + + async delete(key: string): Promise { + if (this.faults.failDelete > 0) { + this.faults.failDelete--; + this.injected.delete++; + throw new Error(`[FaultyBackend] injected delete failure: ${key}`); + } + await this.inner.delete(key); + } + + async deleteMany(keys: string[]): Promise { + for (const key of keys) { + await this.delete(key); + } + } + + listKeys(): Promise { + return this.inner.listKeys(); + } + + exists(key: string): Promise { + return this.inner.exists(key); + } + + clear(): Promise { + return this.inner.clear(); + } +} diff --git a/tests/helpers/opfs-mock.ts b/tests/helpers/opfs-mock.ts deleted file mode 100644 index f7feb21..0000000 --- a/tests/helpers/opfs-mock.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 测试共享 — 真实语义 OPFS mock - * - * 模拟 OPFS 的完整语义: - * - createWritable({ keepExistingData }) + position 追加写 - * - 文件内容以 ArrayBuffer 存储(跨 backend 实例共享于同一 Map) - * - entries() 迭代、removeEntry 删除 - * - * 用法: - * const files = installOPFSMock(new Map()); - * const backend = new OPFSBackend(); - * await backend.open('any'); - */ - -export interface MockOpfsFile { - content: ArrayBuffer; -} - -export function installOPFSMock(files: Map): { - files: Map; - dir: { - getFileHandle: (name: string, opts?: { create?: boolean }) => Promise<{ - getFile: () => Promise<{ size: number; arrayBuffer: () => Promise }>; - createWritable: (wOpts?: { keepExistingData?: boolean }) => Promise<{ - write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise; - close: () => Promise; - }>; - }>; - entries: () => AsyncGenerator<[string]>; - removeEntry: (name: string) => Promise; - }; -} { - const getFileHandle = async (name: string, opts?: { create?: boolean }) => { - if (!files.has(name)) { - if (!opts?.create) throw new Error(`NotFoundError: ${name}`); - files.set(name, { content: new ArrayBuffer(0) }); - } - const entry = files.get(name)!; - return { - getFile: async () => ({ size: entry.content.byteLength, arrayBuffer: async () => entry.content }), - createWritable: async (wOpts?: { keepExistingData?: boolean }) => { - const w: { - write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise; - close: () => Promise; - } = { - write: async (arg) => { - const keepExisting = wOpts?.keepExistingData ?? false; - const chunk = (arg as { type?: string }).type === 'write' - ? { data: (arg as { data: ArrayBuffer }).data, position: (arg as { position: number }).position } - : { data: arg as ArrayBuffer, position: keepExisting ? entry.content.byteLength : 0 }; - const merged = new Uint8Array(chunk.position + chunk.data.byteLength); - if (keepExisting || chunk.position > 0) { - merged.set(new Uint8Array(entry.content), 0); - } - merged.set(new Uint8Array(chunk.data), chunk.position); - entry.content = merged.buffer; - }, - close: async () => { /* no-op */ }, - }; - return w; - }, - }; - }; - - const dir = { - getFileHandle, - entries: async function* () { - for (const [name] of files) yield [name]; - }, - removeEntry: async (name: string) => { - files.delete(name); - }, - }; - - Object.defineProperty(globalThis, 'navigator', { - value: { - storage: { - getDirectory: async () => ({ - getDirectoryHandle: async (_name: string, _opts?: unknown) => dir, - }), - }, - }, - configurable: true, - writable: true, - }); - - return { files, dir }; -} diff --git a/tests/helpers/storage-harness.test.ts b/tests/helpers/storage-harness.test.ts new file mode 100644 index 0000000..6bab52a --- /dev/null +++ b/tests/helpers/storage-harness.test.ts @@ -0,0 +1,225 @@ +/** + * 故障注入基座自测(v0.8.0 工作流 C-1) + * + * 这些测试的作用不是测产品代码,而是**证明测试基座本身是有效的**: + * 如果故障注入不能稳定复现"崩溃丢数据",那么后面所有崩溃相关测试都是空头支票。 + */ + +import { TransactionalFileStore, installOPFSMock, clearRegistry, CrashableStoreBackend } from './storage-harness'; +import { FaultyBackend } from './faulty-backend'; +import { OPFSBackend } from '../../src/engine/aria/store/opfs_backend'; +import { decode as decodeBytes } from './assertions'; + +describe('[v0.8.0] 测试基座:TransactionalFileStore', () => { + let store: TransactionalFileStore; + + beforeEach(() => { + clearRegistry(); + store = new TransactionalFileStore({ dbName: 'unit' }); + }); + + test('提交语义:未提交的写入对读不可见(真实 OPFS 是 close 才原子替换)', async () => { + await store.write('k', new TextEncoder().encode('v1').buffer as ArrayBuffer); + expect(await store.read('k')).toBeNull(); // 未 commit → 读不到 + store.commitAll(); + expect(decodeBytes(await store.read('k'))).toBe('v1'); + }); + + test('崩溃语义:丢弃未提交写入,已提交内容保持', async () => { + await store.write('k', new TextEncoder().encode('committed').buffer as ArrayBuffer); + store.commitAll(); + + await store.write('k', new TextEncoder().encode('pending').buffer as ArrayBuffer); + expect(store.hasPending()).toBe(true); + store.crashPending(); // 崩溃:丢掉 pending + + expect(decodeBytes(await store.read('k'))).toBe('committed'); + expect(store.hasPending()).toBe(false); + }); + + test('读返回副本:调用方原地修改不会污染"磁盘"(旧 mock 的缺陷 1)', async () => { + await store.write('k', new TextEncoder().encode('original').buffer as ArrayBuffer); + store.commitAll(); + + const first = await store.read('k'); + new Uint8Array(first!)[0] = 0x58; // 'X' + + const second = await store.read('k'); + expect(decodeBytes(second)).toBe('original'); + }); + + test('故障注入:failNextWrite / failNextAppend / failNextDelete 精确计数', async () => { + store.failNextWrite(); + await expect(store.write('k', new ArrayBuffer(4))).rejects.toThrow(/injected write failure/); + await expect(store.write('k', new ArrayBuffer(4))).resolves.toBeUndefined(); + + store.failNextAppend(2); + await expect(store.append('k', new ArrayBuffer(2))).rejects.toThrow(/injected append failure/); + await expect(store.append('k', new ArrayBuffer(2))).rejects.toThrow(/injected append failure/); + await expect(store.append('k', new ArrayBuffer(2))).resolves.toBeUndefined(); + + store.failNextDelete(); + await expect(store.delete('k')).rejects.toThrow(/injected delete failure/); + }); + + test('撕裂写:truncateNextAppendTo 只落前 n 字节', async () => { + const text = new TextEncoder(); + await store.write('log', text.encode('AAAA').buffer as ArrayBuffer); + store.commitAll(); + + store.truncateNextAppendTo(2); + await store.append('log', text.encode('BBBB').buffer as ArrayBuffer); + store.commitAll(); + + // 已提交的 'AAAA' 保留 + 撕裂后只追加了 'BB' → 'AAAABB' + expect(decodeBytes(await store.read('log'))).toBe('AAAABB'); + }); + + test('篡改已提交字节:corruptCommitted 用于 bit-flip 场景', async () => { + await store.write('k', new TextEncoder().encode('hello').buffer as ArrayBuffer); + store.commitAll(); + + const ok = store.corruptCommitted('k', (bytes) => { bytes[0] ^= 0xff; }); + expect(ok).toBe(true); + const after = new Uint8Array((await store.read('k'))!); + expect(after[0]).not.toBe('h'.charCodeAt(0)); + }); + + test('提交原子性:commitAll 一次性生效全部 pending', async () => { + await store.write('a', new ArrayBuffer(1)); + await store.write('b', new ArrayBuffer(1)); + expect(await store.listKeys()).toEqual(['a', 'b']); + store.commitAll(); + expect((await store.listKeys()).sort()).toEqual(['a', 'b']); + }); +}); + +describe('[v0.8.0] 测试基座:installOPFSMock 真实语义', () => { + beforeEach(() => clearRegistry()); + + test('keepExistingData:false 会截断(旧 mock 的缺陷 2)', async () => { + installOPFSMock('trunc'); + const backend = new OPFSBackend(); + await backend.open('trunc'); + await backend.write('f', new TextEncoder().encode('LONGCONTENT').buffer as ArrayBuffer); + + // 覆盖写更短内容 → 真实 OPFS 应截断为 5 字节 + await backend.write('f', new TextEncoder().encode('short').buffer as ArrayBuffer); + const read = await backend.read('f'); + expect(decodeBytes(read)).toBe('short'); + }); + + test('append 是真追加且不破坏已有内容', async () => { + installOPFSMock('app'); + const backend = new OPFSBackend(); + await backend.open('app'); + await backend.write('log', new TextEncoder().encode('AAA').buffer as ArrayBuffer); + await backend.append('log', new TextEncoder().encode('BBB').buffer as ArrayBuffer); + expect(decodeBytes(await backend.read('log'))).toBe('AAABBB'); + }); + + test('读返回副本,跨 backend 实例隔离', async () => { + installOPFSMock('iso'); + const b1 = new OPFSBackend(); + await b1.open('iso'); + await b1.write('k', new TextEncoder().encode('v').buffer as ArrayBuffer); + + const b2 = new OPFSBackend(); + await b2.open('iso'); + const buf = await b2.read('k'); + new Uint8Array(buf!)[0] = 0x5a; + + expect(decodeBytes(await b1.read('k'))).toBe('v'); + }); + + test('崩溃后重开:已提交数据在,未提交的不在', async () => { + const installed = installOPFSMock('crash'); + const store = installed.store; + store.restoreCommitted(new Map()); // 清空 + + await store.write('committed', new TextEncoder().encode('yes').buffer as ArrayBuffer); + store.commitAll(); + await store.write('pending', new TextEncoder().encode('no').buffer as ArrayBuffer); + store.crashPending(); + + expect(decodeBytes(await store.read('committed'))).toBe('yes'); + expect(await store.read('pending')).toBeNull(); + }); +}); + +describe('[v0.8.0] 测试基座:FaultyBackend', () => { + beforeEach(() => clearRegistry()); + + test('注入的写失败必须真的让调用方拿到 rejection', async () => { + installOPFSMock('faulty'); + const faulty = new FaultyBackend(new OPFSBackend()); + await faulty.open('faulty'); + + faulty.failNextWrite(); + await expect(faulty.write('k', new ArrayBuffer(4))).rejects.toThrow(/injected write failure/); + expect(faulty.injected.write).toBe(1); + + faulty.clearFaults(); + await expect(faulty.write('k', new ArrayBuffer(4))).resolves.toBeUndefined(); + }); + + test('静默丢弃写:调用方看到成功,但介质上没有(掉电场景)', async () => { + installOPFSMock('drop'); + const faulty = new FaultyBackend(new OPFSBackend()); + await faulty.open('drop'); + + faulty.dropNextWrite(); + await expect(faulty.write('k', new ArrayBuffer(4))).resolves.toBeUndefined(); + expect(await faulty.read('k')).toBeNull(); // 静默丢失 + expect(faulty.injected.dropped).toBe(1); + }); + + test('撕裂追加:写到一半的 WAL 记录可被构造', async () => { + installOPFSMock('torn'); + const faulty = new FaultyBackend(new OPFSBackend()); + await faulty.open('torn'); + + const record = new Uint8Array(100).fill(7); + faulty.truncateNextAppendTo(37); + await faulty.append('__wal_000000.bin', record.buffer as ArrayBuffer); + + const stored = await faulty.read('__wal_000000.bin'); + expect(stored!.byteLength).toBe(37); + expect(faulty.injected.truncated).toBe(1); + }); + + test('crash() 丢弃未提交写(这才是崩溃,不是 close 优雅停机)', async () => { + const store = new TransactionalFileStore({ dbName: 'crash2' }); + const backend = new CrashableStoreBackend(store); + const faulty = new FaultyBackend(backend); + + const text = new TextEncoder(); + await faulty.write('saved', text.encode('KEEP').buffer as ArrayBuffer); + backend.commit(); // 模拟 createWritable.close() 完成 + + await faulty.write('unsaved', text.encode('LOST').buffer as ArrayBuffer); + expect(store.hasPending()).toBe(true); // 存在真实的"提交前崩溃窗口" + + expect(faulty.crash()).toBe(true); + + expect(store.hasPending()).toBe(false); + expect(await store.read('unsaved')).toBeNull(); + expect(decodeBytes(await store.read('saved'))).toBe('KEEP'); + }); + + test('对照组:close() 是优雅停机,会把未提交写刷完 —— 因此不能用来模拟崩溃', async () => { + const store = new TransactionalFileStore({ dbName: 'graceful' }); + const backend = new CrashableStoreBackend(store); + + await backend.write('k', new TextEncoder().encode('PENDING').buffer as ArrayBuffer); + expect(store.hasPending()).toBe(true); + + // close() 不丢弃 pending(真实 OPFSBackend.close 还会等写队列排空) + await backend.close(); + expect(store.hasPending()).toBe(true); + + // 提交后才可见 —— 证明"用 close 当崩溃"会掩盖崩溃窗口 + backend.commit(); + expect(decodeBytes(await store.read('k'))).toBe('PENDING'); + }); +}); diff --git a/tests/helpers/storage-harness.ts b/tests/helpers/storage-harness.ts new file mode 100644 index 0000000..3df7c53 --- /dev/null +++ b/tests/helpers/storage-harness.ts @@ -0,0 +1,452 @@ +/** + * 测试共享 — OPFS 事务性文件存储(v0.8.0 根治版) + * + * ============================================================================ + * 为什么需要重写(审计结论,见 PLAN-v0.7.5.md 工作流 C-1) + * ============================================================================ + * 旧 mock(tests/helpers/opfs-mock.ts)有三处与真实 OPFS 语义不符,会掩盖真实缺陷: + * 1. `read()` 返回内部 ArrayBuffer **引用**(真实 OPFS 返回快照副本) + * → 调用方原地修改会"污染磁盘",而真实环境不会; + * 2. `write()` 中 `if (keepExisting || position > 0)` 会在 keepExistingData:false + * 时也保留旧字节(真实 OPFS 该场景应截断); + * 3. `close()` 是空函数、写入**立即对读可见**(真实 OPFS 是写 swap 文件、 + * close 时才原子替换)。因此"提交前可见"这类缺陷在旧 mock 下永远测不出来。 + * + * 更关键的是:**没有任何 mock 能表达"半写/撕裂/丢失一次写/部分删除失败"**, + * 而项目所有"崩溃恢复"测试用的都是 `backend.close()`(优雅停机,会把写队列刷完), + * 于是崩溃相关的声称在结构上无法被验证。 + * + * ============================================================================ + * 本实现的两层设计 + * ============================================================================ + * 第 1 层 `TransactionalFileStore`: + * - 纯数据 + 字节级故障注入 + 崩溃模拟(commit/discard),**不依赖任何浏览器 API**。 + * 可直接用于单元测试:`store.write('k', buf); store.crashPending(); store.commitAll();` + * - 忠实实现 OPFS 的写语义:createWritable 后写入进入 pending 区,close 时原子提交; + * crash 丢弃全部 pending,已提交内容保持崩溃前状态。 + * + * 第 2 层 `installOPFSMock()`: + * - 把 store 包装成 `navigator.storage.getDirectory()` 的 OPFS 门面,供 OPFSBackend 使用。 + * - 读返回**副本**(不再泄漏内部引用)、keepExistingData:false 时截断、 + * close 前不可见(与真实 OPFS 一致)。 + * + * 故障注入 API(供故障矩阵测试使用): + * - `failNextWrite(n)` / `failNextAppend(n)` / `failNextDelete(n)` + * - `truncateNextAppendTo(n)` —— 追加只写入前 n 字节(撕裂写) + * - `crashPending()` —— 丢弃全部未提交写入(模拟进程崩溃) + * - `commitAll()` —— 提交全部 pending(模拟 createWritable.close 全部完成) + */ + +import type { IStorageBackend } from '../../src/engine/aria/store/backend'; + +// --------------------------------------------------------------------------- +// 第 1 层:事务性文件存储(无浏览器依赖,可直接单测) +// --------------------------------------------------------------------------- + +export interface TransactionalFileStoreOptions { + /** 库名(仅用于日志/调试) */ + dbName?: string; +} + +/** 一次写入的故障注入配置 */ +interface FaultConfig { + failWrite: number; + failAppend: number; + failDelete: number; + /** 追加写只落盘前 N 字节(撕裂写);-1 表示不启用 */ + truncateAppendTo: number; +} + +export class TransactionalFileStore { + /** 已提交内容(= 真实"磁盘"状态) */ + private committed = new Map(); + /** 未提交写入(= OPFS createWritable 的 swap 区) */ + private pending = new Map(); + + private readonly dbName: string; + private faults: FaultConfig = { failWrite: 0, failAppend: 0, failDelete: 0, truncateAppendTo: -1 }; + + /** 统计(供断言"到底发生了几次 IO") */ + readonly stats = { writes: 0, appends: 0, deletes: 0, reads: 0, commits: 0, crashes: 0 }; + + constructor(options: TransactionalFileStoreOptions = {}) { + this.dbName = options.dbName ?? 'mock'; + } + + get name(): string { + return this.dbName; + } + + // ---- 故障注入 ---- + + /** 接下来 n 次 write() 抛错 */ + failNextWrite(n = 1): void { this.faults.failWrite = n; } + /** 接下来 n 次 append() 抛错 */ + failNextAppend(n = 1): void { this.faults.failAppend = n; } + /** 接下来 n 次 delete() 抛错(模拟 OPFS removeEntry 失败) */ + failNextDelete(n = 1): void { this.faults.failDelete = n; } + /** 下一次 append() 只落盘前 n 字节(撕裂写:模拟写入中途断电) */ + truncateNextAppendTo(n: number): void { this.faults.truncateAppendTo = n; } + /** 清空全部故障注入 */ + clearFaults(): void { + this.faults = { failWrite: 0, failAppend: 0, failDelete: 0, truncateAppendTo: -1 }; + } + + // ---- 崩溃 / 提交 ---- + + /** + * 模拟进程崩溃:丢弃**全部**未提交写入。 + * 已提交内容保持崩溃前状态(这正是 OPFS copy-on-write 的保证)。 + */ + crashPending(): void { + this.pending.clear(); + this.stats.crashes++; + } + + /** 模拟所有未完成的 createWritable 正常 close:把 pending 原子提交 */ + commitAll(): void { + for (const [key, value] of this.pending) { + if (value === null) this.committed.delete(key); + else this.committed.set(key, value); + } + this.pending.clear(); + this.stats.commits++; + } + + /** 是否存在未提交写入(测试可断言"崩溃窗口"是否真的存在) */ + hasPending(): boolean { + return this.pending.size > 0; + } + + // ---- 数据操作(与 IStorageBackend 语义对齐,供 FaultyBackend 复用) ---- + + async read(key: string): Promise { + this.stats.reads++; + // 读取只看已提交内容 —— 未 close 的写入对读不可见(真实 OPFS 语义) + const buf = this.committed.get(key); + if (!buf) return null; + return copyBuffer(buf); // 返回副本,杜绝调用方原地修改污染"磁盘" + } + + async write(key: string, data: ArrayBuffer): Promise { + if (this.faults.failWrite > 0) { + this.faults.failWrite--; + throw new Error(`[OPFS-mock] injected write failure on "${key}"`); + } + this.stats.writes++; + this.pending.set(key, copyBuffer(data)); + } + + async append(key: string, data: ArrayBuffer): Promise { + if (this.faults.failAppend > 0) { + this.faults.failAppend--; + throw new Error(`[OPFS-mock] injected append failure on "${key}"`); + } + this.stats.appends++; + + let chunk = copyBuffer(data); + if (this.faults.truncateAppendTo >= 0) { + // 撕裂写:只落盘前 n 字节 + chunk = copyBufferHead(chunk, this.faults.truncateAppendTo); + this.faults.truncateAppendTo = -1; + } + + // 追加目标是"已提交内容 + pending 中同 key 的写入"(同一 swap 文件内的续写) + const base = this.pending.has(key) + ? (this.pending.get(key) as ArrayBuffer | null) + : this.committed.get(key) ?? null; + + if (!base) { + this.pending.set(key, chunk); + return; + } + const merged = new Uint8Array(base.byteLength + chunk.byteLength); + merged.set(new Uint8Array(base), 0); + merged.set(new Uint8Array(chunk), base.byteLength); + this.pending.set(key, merged.buffer as ArrayBuffer); + } + + /** 覆盖写(keepExistingData: false,真实 OPFS 会截断) */ + async overwrite(key: string, data: ArrayBuffer): Promise { + await this.write(key, data); + } + + async delete(key: string): Promise { + if (this.faults.failDelete > 0) { + this.faults.failDelete--; + throw new Error(`[OPFS-mock] injected delete failure on "${key}"`); + } + this.stats.deletes++; + this.pending.set(key, null); // 删除也在提交时才生效 + } + + async listKeys(): Promise { + const keys = new Set(this.committed.keys()); + for (const [key, value] of this.pending) { + if (value === null) keys.delete(key); + else keys.add(key); + } + return [...keys]; + } + + async has(key: string): Promise { + if (this.pending.has(key)) return this.pending.get(key) !== null; + return this.committed.has(key); + } + + /** 直接查看已提交字节数(不经过 read 的副本语义,供结构断言用) */ + committedSize(key: string): number { + return this.committed.get(key)?.byteLength ?? 0; + } + + /** 已提交内容的只读快照(供"篡改磁盘"类测试:写坏字节后重开观察恢复) */ + snapshotCommitted(): Map { + const out = new Map(); + for (const [k, v] of this.committed) out.set(k, copyBuffer(v)); + return out; + } + + /** 用快照替换已提交内容(模拟外部损坏/离线篡改) */ + restoreCommitted(snapshot: Map): void { + this.committed = new Map(); + for (const [k, v] of snapshot) this.committed.set(k, copyBuffer(v)); + this.pending.clear(); + } + + /** 就地篡改已提交字节(模拟 bit flip / 部分覆盖),不做副本 */ + corruptCommitted(key: string, mutate: (bytes: Uint8Array) => void): boolean { + const buf = this.committed.get(key); + if (!buf) return false; + mutate(new Uint8Array(buf)); + return true; + } + + clear(): void { + this.committed.clear(); + this.pending.clear(); + } +} + +/** + * 复制 ArrayBuffer(杜绝引用泄漏 —— 旧 mock 的缺陷 1) + * + * 注意:不能用 `buf.slice(0)` —— 在 jsdom 环境下 `ArrayBuffer.prototype.slice` 可能被 + * Blob/File 的 slice 语义遮蔽,导致 `slice(0, n)` 的参数被忽略、返回整段内容。 + * 这里用 Uint8Array 显式复制,行为在任何环境都确定。 + */ +function copyBuffer(buf: ArrayBuffer): ArrayBuffer { + const out = new Uint8Array(buf.byteLength); + out.set(new Uint8Array(buf)); + return out.buffer as ArrayBuffer; +} + +/** 复制前 n 字节(撕裂写用;同上不使用 ArrayBuffer.slice) */ +function copyBufferHead(buf: ArrayBuffer, n: number): ArrayBuffer { + const len = Math.max(0, Math.min(n, buf.byteLength)); + const out = new Uint8Array(len); + out.set(new Uint8Array(buf, 0, len)); + return out.buffer as ArrayBuffer; +} + +/** + * 结构判别:是否为 `createWritable().write({type,position,data})` 形式。 + * 不用 `instanceof ArrayBuffer` —— 跨 realm 时会误判(见 write 内注释)。 + */ +function isPositionedWrite( + arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }, +): arg is { type: string; position: number; data: ArrayBuffer } { + const anyArg = arg as { type?: unknown; position?: unknown; data?: unknown }; + return typeof anyArg?.type === 'string' + && typeof anyArg?.position === 'number' + && anyArg?.data != null; +} + +// --------------------------------------------------------------------------- +// 第 2 层:OPFS 门面(供 OPFSBackend 使用) +// --------------------------------------------------------------------------- + +/** 全局注册表:库名 → store(跨 backend 实例共享 = 持久化语义) */ +const registry = new Map(); + +/** 取(或创建)某库的共享 store */ +export function getStore(dbName: string): TransactionalFileStore { + let store = registry.get(dbName); + if (!store) { + store = new TransactionalFileStore({ dbName }); + registry.set(dbName, store); + } + return store; +} + +/** 清空全局注册表(测试隔离) */ +export function clearRegistry(): void { + registry.clear(); +} + +/** + * 每次调用都重置的 OPFS 安装(推荐在 `beforeEach` 中使用)。 + * + * 等价于 `clearRegistry(); installOPFSMock(dbName);`。 + * 命名里强调 reset 是为了避免旧写法的误导:此前测试写 `installOPFSMock(new Map())`, + * 传入的 Map 会被 mock **静默忽略**(各测试文件各自传 Map 并不能隔离共享状态)。 + */ +export function resetOPFSMock(dbName = 'mock'): InstalledOPFSMock { + clearRegistry(); + return installOPFSMock(dbName); +} + +export interface InstalledOPFSMock { + store: TransactionalFileStore; + /** 当前 mock 看到的所有文件名 */ + listKeys(): Promise; + /** + * 直接创建(或覆盖)一个文件 —— 用于制造"崩溃残留临时文件"等场景, + * 替代旧 API 暴露内部 dir/files 的做法。 + */ + createFile(name: string, content?: ArrayBuffer): Promise; + /** 文件是否存在 */ + hasFile(name: string): Promise; + /** 读取文件内容(返回副本) */ + readFile(name: string): Promise; + /** 删除文件 */ + removeFile(name: string): Promise; +} + +/** + * 安装 OPFS mock 到 globalThis.navigator.storage.getDirectory()。 + * + * @param dbName 库名(决定共享 store;同名多次安装拿到同一个 store) + */ +export function installOPFSMock(dbName = 'mock'): InstalledOPFSMock { + const store = getStore(dbName); + + const makeFileHandle = async (name: string, opts?: { create?: boolean }) => { + if (!(await store.has(name)) && !opts?.create) { + throw new Error(`NotFoundError: ${name}`); + } + return { + /** + * 真实 OPFS 的 getFile() 返回 File,读的是**已提交**内容(size 与 arrayBuffer 一致)。 + * append 路径会读 `existing.size` 来定位追加位置,因此这里保持二者同源。 + */ + getFile: async () => { + const content = (await store.read(name)) ?? new ArrayBuffer(0); + return { + size: content.byteLength, + arrayBuffer: async () => content, + }; + }, + createWritable: async (wOpts?: { keepExistingData?: boolean }) => { + const keepExisting = wOpts?.keepExistingData ?? false; + // OPFS 写语义:keepExistingData:false 时从空缓冲开始(旧 mock 会保留旧字节) + let buffer = keepExisting ? (await store.read(name)) ?? new ArrayBuffer(0) : new ArrayBuffer(0); + return { + write: async (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => { + // 注意:不能用 `arg instanceof ArrayBuffer` 判别 —— 跨 realm / 跨 Buffer 实现时 + // 会失效(jsdom 与 Node 的 ArrayBuffer 可能不是同一个构造函数), + // 从而把 ArrayBuffer 误当成 {position,data} 分支。改用结构判别。 + if (!isPositionedWrite(arg)) { + buffer = copyBuffer(arg as ArrayBuffer); + return; + } + const chunk = arg; + const end = chunk.position + chunk.data.byteLength; + const merged = new Uint8Array(Math.max(end, buffer.byteLength)); + merged.set(new Uint8Array(buffer), 0); + merged.set(new Uint8Array(chunk.data), chunk.position); + buffer = merged.buffer as ArrayBuffer; + }, + close: async () => { + // 仅在 close 时提交 —— 未 close 的写入对读不可见(旧 mock 会立即可见) + await store.write(name, buffer); + store.commitAll(); + }, + }; + }, + }; + }; + + const dir = { + getFileHandle: makeFileHandle, + entries: async function* () { + for (const key of await store.listKeys()) yield [key]; + }, + removeEntry: async (name: string) => { + await store.delete(name); + store.commitAll(); + }, + }; + + Object.defineProperty(globalThis, 'navigator', { + value: { + storage: { + getDirectory: async () => ({ + getDirectoryHandle: async (_name: string, _opts?: unknown) => dir, + }), + }, + }, + configurable: true, + writable: true, + }); + + return { + store, + listKeys: () => store.listKeys(), + createFile: async (name, content) => { + await store.write(name, content ?? new ArrayBuffer(0)); + store.commitAll(); + }, + hasFile: (name) => store.has(name), + readFile: (name) => store.read(name), + removeFile: async (name) => { + await store.delete(name); + store.commitAll(); + }, + }; +} + +/** + * 直接由 TransactionalFileStore 驱动的可崩溃后端。 + * + * 用途:验证"真崩溃"(丢弃未提交写入)而非"优雅停机"(close 刷完队列)。 + * OPFSBackend 之上无法表达 pending 语义(它每次 write 都会 close 提交), + * 因此需要这一层直连介质的后端来构造"写入进行中崩溃"的窗口。 + */ +export class CrashableStoreBackend implements IStorageBackend { + private readonly store: TransactionalFileStore; + + constructor(store: TransactionalFileStore) { + this.store = store; + } + + open(_name: string): Promise { return Promise.resolve(); } + close(): Promise { return Promise.resolve(); } + isOpen(): boolean { return true; } + read(key: string) { return this.store.read(key); } + write(key: string, data: ArrayBuffer) { return this.store.write(key, data); } + append(key: string, data: ArrayBuffer) { return this.store.append(key, data); } + writeMany(entries: Record) { + return (async () => { + for (const [k, v] of Object.entries(entries)) await this.store.write(k, v); + })(); + } + delete(key: string) { return this.store.delete(key); } + deleteMany(keys: string[]) { + return (async () => { + for (const k of keys) await this.store.delete(k); + })(); + } + listKeys() { return this.store.listKeys(); } + exists(key: string) { return this.store.has(key); } + clear(): Promise { this.store.clear(); return Promise.resolve(); } + + /** 崩溃:丢弃全部未提交写入 */ + simulateCrash(): void { + this.store.crashPending(); + } + + /** 正常提交(模拟所有 createWritable 完成 close) */ + commit(): void { + this.store.commitAll(); + } +} diff --git a/tests/hooks-crud.test.ts b/tests/hooks-crud.test.ts index 79257a9..1b46990 100644 --- a/tests/hooks-crud.test.ts +++ b/tests/hooks-crud.test.ts @@ -65,12 +65,12 @@ describe('v0.5.1 — CRUD hooks 真实接线', () => { const db = await createDb('sql-crud'); await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)"); const events: string[] = []; - db.on('beforeInsert', () => events.push('beforeInsert')); - db.on('afterInsert', () => events.push('afterInsert')); - db.on('beforeUpdate', () => events.push('beforeUpdate')); - db.on('afterUpdate', () => events.push('afterUpdate')); - db.on('beforeDelete', () => events.push('beforeDelete')); - db.on('afterDelete', () => events.push('afterDelete')); + db.on('beforeInsert', () => { events.push('beforeInsert'); }); + db.on('afterInsert', () => { events.push('afterInsert'); }); + db.on('beforeUpdate', () => { events.push('beforeUpdate'); }); + db.on('afterUpdate', () => { events.push('afterUpdate'); }); + db.on('beforeDelete', () => { events.push('beforeDelete'); }); + db.on('afterDelete', () => { events.push('afterDelete'); }); await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)"); await db.query("UPDATE users SET age = 26 WHERE id = '2'"); @@ -87,7 +87,7 @@ describe('v0.5.1 — CRUD hooks 真实接线', () => { it('SQL INSERT 多行语句按列名映射触发 afterInsert', async () => { const db = await createDb('sql-insert-cols'); const seen: unknown[] = []; - db.on('afterInsert', (_rows, pks) => seen.push(pks)); + db.on('afterInsert', (_rows, pks) => { seen.push(pks); }); await db.query("INSERT INTO users (id, name) VALUES ('9', 'Zed')"); @@ -107,7 +107,7 @@ describe('v0.5.1 — CRUD hooks 真实接线', () => { 'beforeQuery', 'afterQuery', 'beforeTransaction', 'afterTransaction', ] as const; - for (const h of hooks) db.on(h, () => fired.add(h)); + for (const h of hooks) db.on(h, () => { fired.add(h); }); await db.defineTable('t2', { id: { type: 'string', primaryKey: true } }); // createTable hooks await db.table('t2').insert({ id: '1' }); // insert hooks diff --git a/tests/hybrid/index.test.ts b/tests/hybrid/index.test.ts index 67333f8..b0a5872 100644 --- a/tests/hybrid/index.test.ts +++ b/tests/hybrid/index.test.ts @@ -21,7 +21,7 @@ describe('HybridEngine', () => { beforeEach(async () => { dbName = `test-hybrid-${++hybridCounter}`; - engine = new HybridEngine('indexeddb'); + engine = new HybridEngine('opfs'); await engine.open(dbName, 1); }); diff --git a/tests/maintenance-sql.test.ts b/tests/maintenance-sql.test.ts index d6d3268..a4ce187 100644 --- a/tests/maintenance-sql.test.ts +++ b/tests/maintenance-sql.test.ts @@ -6,11 +6,10 @@ */ import { MetonaSqlark } from '../src/core'; import { parse } from '../src/sql/parser'; -import { createSchema } from '../src/table/schema'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let dbCounter = 0; function uniqueDB(): string { diff --git a/tests/query/query-system.test.ts b/tests/query/query-system.test.ts index 7c3cd72..703e5c3 100644 --- a/tests/query/query-system.test.ts +++ b/tests/query/query-system.test.ts @@ -3,7 +3,6 @@ */ import { MemoryEngine } from '../../src/engine/memory'; -import { createSchema } from '../../src/table/schema'; import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../../src/query/builder'; import { compileStatement } from '../../src/query/compiler'; import { QueryExecutor } from '../../src/query/executor'; diff --git a/tests/sql-ext.test.ts b/tests/sql-ext.test.ts index daf781d..accd1fe 100644 --- a/tests/sql-ext.test.ts +++ b/tests/sql-ext.test.ts @@ -8,9 +8,9 @@ import { MetonaSqlark } from '../src/core'; import '../src/connection-manager'; import { parse, parseAll } from '../src/sql/parser'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); async function createDb(mode: 'memory' | 'disk' | 'aria' = 'memory') { const db = new MetonaSqlark({ name: `sql-ext-${mode}-${Date.now()}-${Math.random()}`, mode, diskEngine: 'opfs' }); @@ -288,9 +288,9 @@ describe('[v0.3.0] CREATE INDEX / DROP INDEX', () => { test('CREATE INDEX 不存在列报错', async () => { const db = await createDb(); - const err = await db.query('CREATE INDEX idx_bad ON users (nonexistent)').catch((e: any) => e); - expect(err).toBeDefined(); - expect(err.code).toBe('COLUMN_NOT_FOUND'); + const err = await db.query('CREATE INDEX idx_bad ON users (nonexistent)').catch((e: unknown) => e); + expect(err).toBeInstanceOf(Error); + expect((err as { code?: string }).code).toBe('COLUMN_NOT_FOUND'); await db.close(); }); diff --git a/tests/sql-ext2.test.ts b/tests/sql-ext2.test.ts index 3c08de5..ce6aead 100644 --- a/tests/sql-ext2.test.ts +++ b/tests/sql-ext2.test.ts @@ -9,9 +9,9 @@ import { parse } from '../src/sql/parser'; import { WAL, type WALStore } from '../src/engine/aria/wal/log'; import { WALRecordType } from '../src/engine/aria/types'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); async function createDb(mode: 'memory' | 'aria' = 'memory') { const db = new MetonaSqlark({ name: `sql-ext2-${mode}-${Date.now()}-${Math.random()}`, mode, diskEngine: 'opfs' }); diff --git a/tests/sql/parser.test.ts b/tests/sql/parser.test.ts index 0ff0c59..0d96b77 100644 --- a/tests/sql/parser.test.ts +++ b/tests/sql/parser.test.ts @@ -1,69 +1,152 @@ /** * Parser 边缘场景测试 + * + * v0.8.0 强化说明: + * 1. 此前 12 处断言写着 `expect(ast.where).toBeDefined()` —— 既没有验证结构也没有验证 + * 值,属于"空断言"。审计(AUDIT-query-layer / SQL 层)指出:`parser.test.ts` 无一处 + * 断言混合 AND/OR 的**结构**,这正是 AND/OR 无优先级缺陷(总账第 11 项)能长期存活的原因。 + * 2. 本文件此前 `parse()` 的返回类型是联合类型 `Statement`,直接访问 `.where` / `.columns` + * 在类型上不成立(tests 从不做类型检查,所以没人发现)。现在用类型收窄辅助函数显式 + * 区分语句种类 —— 收窄失败即测试失败,而不是静默读到一个 undefined。 */ import { parse } from '../../src/sql/parser'; +import type { + SelectStatement, + InsertStatement, + UpdateStatement, + DeleteStatement, + CreateTableStatement, +} from '../../src/query/ast'; + +// --------------------------------------------------------------------------- +// 类型收窄辅助:断言语句种类并返回收窄后的类型 +// (替代 `as any` —— 收窄失败会立刻让测试失败,而不是让断言落空) +// --------------------------------------------------------------------------- + +function asSelect(stmt: ReturnType): SelectStatement { + expect(stmt.type).toBe('SELECT'); + if (stmt.type !== 'SELECT') throw new Error(`expected SELECT, got ${stmt.type}`); + return stmt; +} + +function asInsert(stmt: ReturnType): InsertStatement & { values: unknown[][] } { + expect(stmt.type).toBe('INSERT'); + if (stmt.type !== 'INSERT') throw new Error(`expected INSERT, got ${stmt.type}`); + if (!stmt.values) throw new Error('INSERT statement is missing values'); + return stmt as InsertStatement & { values: unknown[][] }; +} + +function asUpdate(stmt: ReturnType): UpdateStatement { + expect(stmt.type).toBe('UPDATE'); + if (stmt.type !== 'UPDATE') throw new Error(`expected UPDATE, got ${stmt.type}`); + return stmt; +} + +function asDelete(stmt: ReturnType): DeleteStatement { + expect(stmt.type).toBe('DELETE'); + if (stmt.type !== 'DELETE') throw new Error(`expected DELETE, got ${stmt.type}`); + return stmt; +} + +function asCreateTable(stmt: ReturnType): CreateTableStatement { + expect(stmt.type).toBe('CREATE_TABLE'); + if (stmt.type !== 'CREATE_TABLE') throw new Error(`expected CREATE_TABLE, got ${stmt.type}`); + return stmt; +} describe('Parser 边缘场景', () => { // ---- SELECT 扩展 ---- describe('SELECT 扩展', () => { it('WHERE 多条件 AND', () => { - const ast = parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'"); - expect(ast.type).toBe('SELECT'); - expect(ast.where).toBeDefined(); + const ast = asSelect(parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'")); + // v0.8.0: 断言真实结构而非 toBeDefined() + expect(ast.where).toEqual({ + $and: [{ age: { $gt: 18 } }, { name: { $like: 'A%' } }], + }); }); it('WHERE IN 列表', () => { - const ast = parse('SELECT * FROM users WHERE id IN (1, 2, 3)'); - expect(ast.type).toBe('SELECT'); - expect(ast.where).toBeDefined(); - const cond = (ast.where as any).id; - expect(cond.$in).toEqual([1, 2, 3]); + const ast = asSelect(parse('SELECT * FROM users WHERE id IN (1, 2, 3)')); + expect(ast.where).toEqual({ id: { $in: [1, 2, 3] } }); }); it('WHERE IS NULL', () => { - const ast = parse('SELECT * FROM users WHERE bio IS NULL'); - expect(ast.type).toBe('SELECT'); - expect(ast.where).toBeDefined(); + const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL')); + expect(ast.where).toEqual({ bio: { $eq: null } }); }); it('WHERE IS NOT NULL', () => { - const ast = parse('SELECT * FROM users WHERE bio IS NOT NULL'); - expect(ast.type).toBe('SELECT'); + const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NOT NULL')); + expect(ast.where).toEqual({ bio: { $ne: null } }); }); it('WHERE NOT', () => { - const ast = parse("SELECT * FROM users WHERE NOT age = 18"); - expect(ast.type).toBe('SELECT'); + const ast = asSelect(parse('SELECT * FROM users WHERE NOT age = 18')); + expect(ast.where).toEqual({ $not: { age: { $eq: 18 } } }); }); it('WHERE 括号分组', () => { - const ast = parse("SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE"); - expect(ast.type).toBe('SELECT'); + const ast = asSelect(parse('SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE')); + expect(ast.where).toEqual({ + $and: [{ $or: [{ age: { $gt: 18 } }, { age: { $lt: 10 } }] }, { active: { $eq: true } }], + }); + }); + + // v0.8.0 新增:AND 优先级必须高于 OR(SQL 标准),此前是纯左折叠 + it('AND 优先级高于 OR(SQL 标准)', () => { + const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 OR a = 2 AND b = 3')); + expect(ast.where).toEqual({ + $or: [{ a: { $eq: 1 } }, { $and: [{ a: { $eq: 2 } }, { b: { $eq: 3 } }] }], + }); + }); + + it('显式括号可覆盖默认优先级', () => { + const ast = asSelect(parse('SELECT * FROM t WHERE (a = 1 OR a = 2) AND b = 3')); + expect(ast.where).toEqual({ + $and: [{ $or: [{ a: { $eq: 1 } }, { a: { $eq: 2 } }] }, { b: { $eq: 3 } }], + }); + }); + + it('NOT 优先级高于 AND/OR', () => { + const ast = asSelect(parse('SELECT * FROM t WHERE NOT a = 1 OR b = 2')); + expect(ast.where).toEqual({ + $or: [{ $not: { a: { $eq: 1 } } }, { b: { $eq: 2 } }], + }); + }); + + it('多层 AND/OR 交替的结合性', () => { + const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 AND b = 2 OR c = 3 AND d = 4')); + expect(ast.where).toEqual({ + $or: [ + { $and: [{ a: { $eq: 1 } }, { b: { $eq: 2 } }] }, + { $and: [{ c: { $eq: 3 } }, { d: { $eq: 4 } }] }, + ], + }); }); it('ORDER BY 多列', () => { - const ast = parse('SELECT * FROM users ORDER BY age DESC, name ASC'); - expect(ast.type).toBe('SELECT'); + const ast = asSelect(parse('SELECT * FROM users ORDER BY age DESC, name ASC')); expect(ast.orderBy).toHaveLength(2); expect(ast.orderBy![0]).toEqual({ column: 'age', direction: 'desc' }); expect(ast.orderBy![1]).toEqual({ column: 'name', direction: 'asc' }); }); it('只有 LIMIT 没有 OFFSET', () => { - const ast = parse('SELECT * FROM users LIMIT 5'); + const ast = asSelect(parse('SELECT * FROM users LIMIT 5')); expect(ast.limit).toBe(5); expect(ast.offset).toBeUndefined(); }); it('只有 OFFSET 没有 LIMIT', () => { - const ast = parse('SELECT * FROM users OFFSET 10'); - expect(ast).toMatchObject({ offset: 10 }); + const ast = asSelect(parse('SELECT * FROM users OFFSET 10')); + expect(ast.offset).toBe(10); + expect(ast.limit).toBeUndefined(); }); it('不带 WHERE 的 SELECT', () => { - const ast = parse('SELECT id, name FROM users ORDER BY id LIMIT 5'); + const ast = asSelect(parse('SELECT id, name FROM users ORDER BY id LIMIT 5')); expect(ast.where).toEqual({}); }); }); @@ -72,18 +155,18 @@ describe('Parser 边缘场景', () => { describe('INSERT 扩展', () => { it('不带列名的 INSERT', () => { - const ast = parse("INSERT INTO users VALUES ('1', 'Alice', 30)"); + const ast = asInsert(parse("INSERT INTO users VALUES ('1', 'Alice', 30)")); expect(ast.columns).toBeUndefined(); expect(ast.values).toEqual([['1', 'Alice', 30]]); }); it('INSERT 布尔值和 NULL', () => { - const ast = parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)'); + const ast = asInsert(parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)')); expect(ast.values[0]).toEqual([true, false, null]); }); it('INSERT 负数和浮点数', () => { - const ast = parse('INSERT INTO scores VALUES (-1, 3.14)'); + const ast = asInsert(parse('INSERT INTO scores VALUES (-1, 3.14)')); expect(ast.values[0]).toEqual([-1, 3.14]); }); }); @@ -92,13 +175,13 @@ describe('Parser 边缘场景', () => { describe('UPDATE 扩展', () => { it('UPDATE 多列', () => { - const ast = parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'"); - expect(ast.type).toBe('UPDATE'); + const ast = asUpdate(parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'")); expect(ast.sets).toEqual({ name: 'Bob', age: 26 }); + expect(ast.where).toEqual({ id: { $eq: '1' } }); }); it('UPDATE 不带 WHERE', () => { - const ast = parse('UPDATE users SET active = FALSE'); + const ast = asUpdate(parse('UPDATE users SET active = FALSE')); expect(ast.where).toEqual({}); }); }); @@ -107,13 +190,15 @@ describe('Parser 边缘场景', () => { describe('DELETE 扩展', () => { it('DELETE 不带 WHERE', () => { - const ast = parse('DELETE FROM users'); + const ast = asDelete(parse('DELETE FROM users')); expect(ast.where).toEqual({}); }); it('DELETE 带复杂 WHERE', () => { - const ast = parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'"); - expect(ast.type).toBe('DELETE'); + const ast = asDelete(parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'")); + expect(ast.where).toEqual({ + $or: [{ age: { $lt: 18 } }, { status: { $eq: 'inactive' } }], + }); }); }); @@ -121,8 +206,7 @@ describe('Parser 边缘场景', () => { describe('DDL 扩展', () => { it('CREATE TABLE 完整修饰符', () => { - const ast = parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)"); - expect(ast.type).toBe('CREATE_TABLE'); + const ast = asCreateTable(parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)")); expect(ast.columns).toHaveLength(4); expect(ast.columns[0]).toMatchObject({ name: 'id', primaryKey: true }); expect(ast.columns[1]).toMatchObject({ name: 'name', required: true, unique: true }); @@ -145,13 +229,13 @@ describe('Parser 边缘场景', () => { describe('常量值解析', () => { it('布尔值 TRUE/FALSE', () => { - const ast = parse("SELECT * FROM users WHERE active = TRUE"); - expect(ast.where).toBeDefined(); + const ast = asSelect(parse('SELECT * FROM users WHERE active = TRUE')); + expect(ast.where).toEqual({ active: { $eq: true } }); }); it('NULL 值', () => { - const ast = parse('SELECT * FROM users WHERE bio IS NULL'); - expect(ast.where).toBeDefined(); + const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL')); + expect(ast.where).toEqual({ bio: { $eq: null } }); }); }); }); diff --git a/tests/table/schema.test.ts b/tests/table/schema.test.ts index 491e303..360bcfd 100644 --- a/tests/table/schema.test.ts +++ b/tests/table/schema.test.ts @@ -35,7 +35,7 @@ describe('Schema 边缘场景', () => { describe('getPrimaryKey', () => { it('没有标记主键时返回第一列', () => { - const schema = createSchema('test', { + const _schema = createSchema('test', { uuid: { type: 'string', primaryKey: true }, name: { type: 'string' }, }); diff --git a/tests/v025-fixes.test.ts b/tests/v025-fixes.test.ts index da4227e..427d482 100644 --- a/tests/v025-fixes.test.ts +++ b/tests/v025-fixes.test.ts @@ -5,30 +5,31 @@ import { VERSION } from '../src/constants'; import { MetonaSqlark } from '../src/core'; -import { AriaEngine } from '../src/engine/aria/index'; import { MemoryEngine } from '../src/engine/memory'; import { parse } from '../src/sql/parser'; import { QueryExecutor } from '../src/query/executor'; import { WAL } from '../src/engine/aria/wal/log'; import { SSTableReader } from '../src/engine/aria/index/sstable'; -import { BloomFilter } from '../src/engine/aria/index/bloom'; import { CryptoManager } from '../src/engine/aria/crypto'; import { PluginManager } from '../src/plugin/index'; import type { SSTableMeta } from '../src/engine/aria/types'; import type { MetonaPlugin } from '../src/constants'; import { createSchema } from '../src/table/schema'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); // --------------------------------------------------------------------------- // P0-1: 版本号统一 // --------------------------------------------------------------------------- describe('[v0.2.5] P0-1: 版本号统一', () => { - test('VERSION 常量为当前版本(0.6.0)', () => { - expect(VERSION).toBe('0.7.4'); + // v0.8.0: 版本号一致性收敛到单一契约测试(tests/version-contract.test.ts), + // 此处不再硬编码版本字面量 —— 此前每次发版必须手改本文件,且只校验源码常量、 + // 与 package.json / dist 之间没有任何一致性检查。 + test('VERSION 常量是合法的语义化版本', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/); }); }); @@ -173,6 +174,7 @@ describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => { let receivedDb: unknown = null; const plugin: MetonaPlugin = { name: 'test-plugin', + version: '1.0.0', install: (db) => { receivedDb = db; }, destroy: () => {}, }; diff --git a/tests/v033-fixes.test.ts b/tests/v033-fixes.test.ts index 616c891..b12c0b6 100644 --- a/tests/v033-fixes.test.ts +++ b/tests/v033-fixes.test.ts @@ -19,9 +19,9 @@ import { MemoryEngine } from '../src/engine/memory'; import { WAL } from '../src/engine/aria/wal/log'; import { tokenize } from '../src/sql/lexer'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); // --------------------------------------------------------------------------- // P0-1: Aria WAL DROP_TABLE 崩溃恢复 @@ -403,7 +403,8 @@ describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => { describe('[v0.3.3] 端到端', () => { test('全部修复点可共存于 MetonaSqlark API', async () => { - expect(VERSION).toBe('0.7.4'); + // v0.8.0: 版本一致性由 tests/version-contract.test.ts 统一校验 + expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/); const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); await db.init(); await db.defineTable('users', { diff --git a/tests/v040-features.test.ts b/tests/v040-features.test.ts index 8e4f97f..88966c6 100644 --- a/tests/v040-features.test.ts +++ b/tests/v040-features.test.ts @@ -9,9 +9,10 @@ import { MetonaSqlark } from '../src/core'; import { AriaEngine } from '../src/engine/aria/index'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; +import { engineMethod } from './helpers/assertions'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); const uniqueName = (prefix: string): string => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; @@ -326,12 +327,15 @@ describe('[v0.4.0] B-4: COUNT(DISTINCT) + NULLS FIRST/LAST', () => { await db.query('CREATE INDEX idx_orders_user ON orders (user_id)'); // WHERE 主表前缀条件下推:引擎收到的 orders 查询 where 为 { user_id: { $eq: '1' } } - const eng = db.getEngine() as { find: (...a: unknown[]) => Promise }; - const origFind = eng.find.bind(eng); + const eng = db.getEngine() as { find?: (...a: unknown[]) => Promise }; + if (!eng.find) throw new Error('engine.find is required for this probe'); + const engineFind = eng.find; + const origFind = engineFind.bind(eng) as (...a: unknown[]) => Promise; let pushed: unknown = null; - eng.find = async (t: string, q: { where?: unknown }) => { + eng.find = async (...args: unknown[]): Promise => { + const [t, q] = args as [string, { where?: unknown } | undefined]; if (t === 'orders' && q?.where) pushed = q.where; - return origFind(t, q); + return origFind(...args); }; const rows = await db.query( "SELECT u.name, o.product FROM orders o JOIN users u ON u.id = o.user_id WHERE o.user_id = '1'", @@ -371,7 +375,7 @@ describe('[v0.4.0] B-4: COUNT(DISTINCT) + NULLS FIRST/LAST', () => { test('Aria $in 查询去重(IN 子查询含重复值不返回重复行)', async () => { const db = new MetonaSqlark({ name: uniqueName('indup'), mode: 'aria', diskEngine: 'opfs' }); await db.init(); - await db.getEngine().clearAll(); + await engineMethod<() => Promise>(db.getEngine(), 'clearAll')(); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } }); await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' } }); await db.query("INSERT INTO users VALUES ('1', 'Alice'), ('2', 'Bob')"); diff --git a/tests/v042-fixes.test.ts b/tests/v042-fixes.test.ts index 58174f9..dbf0037 100644 --- a/tests/v042-fixes.test.ts +++ b/tests/v042-fixes.test.ts @@ -18,12 +18,11 @@ import { SSTableReader } from '../src/engine/aria/index/sstable'; import { MetonaSqlark } from '../src/core'; import { OPFSBackend } from '../src/engine/aria/store/opfs_backend'; import { KVStoreEngine } from '../src/engine/kvstore_engine'; -import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium'; import type { SSTableMeta } from '../src/engine/aria/types'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -58,12 +57,14 @@ describe('P0-1a — SSTableReader 残缺数据防御', () => { truncated.set(sstableData.slice(0, indexOffset + 4), 0); truncated.set(sstableData.slice(sstableData.byteLength - 32), indexOffset + 4); // 构造必须不抛 RangeError - let reader: SSTableReader; + let reader: SSTableReader | undefined; expect(() => { reader = new SSTableReader(truncated, makeMeta(truncated)); }).not.toThrow(); - expect(() => (reader as any).get('k-001')).not.toThrow(); - expect(() => (reader as any).rangeScan('a', 'z', () => {})).not.toThrow(); - expect(() => (reader as any).scanAll(() => {})).not.toThrow(); - expect((reader as any).get('k-001')).toBeNull(); + if (!reader) throw new Error('SSTableReader 构造未成功'); + const r = reader; + expect(() => (r as any).get('k-001')).not.toThrow(); + expect(() => (r as any).rangeScan('a', 'z', () => {})).not.toThrow(); + expect(() => (r as any).scanAll(() => {})).not.toThrow(); + expect((r as any).get('k-001')).toBeNull(); }); it('索引条目 blockSize 越界 → 该块跳过,其余块仍可读', () => { diff --git a/tests/v042-hardening.test.ts b/tests/v042-hardening.test.ts index 33e33e5..0b04663 100644 --- a/tests/v042-hardening.test.ts +++ b/tests/v042-hardening.test.ts @@ -14,9 +14,9 @@ import { MemoryEngine } from '../src/engine/memory'; import { HybridEngine } from '../src/hybrid/index'; import { MetonaSqlark } from '../src/core'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -290,12 +290,12 @@ describe('全模式抽查 — 生命周期与元数据', () => { it('HybridEngine getMeta/setMeta 委托磁盘', async () => { const dbName = uniqueDB(); - const e = new HybridEngine('indexeddb'); + const e = new HybridEngine('opfs'); await e.open(dbName, 1); await e.setMeta('__metona_version', '7'); expect(await e.getMeta('__metona_version')).toBe('7'); await e.close(); - const e2 = new HybridEngine('indexeddb'); + const e2 = new HybridEngine('opfs'); await e2.open(dbName, 1); expect(await e2.getMeta('__metona_version')).toBe('7'); await e2.close(); diff --git a/tests/v043-hardening.test.ts b/tests/v043-hardening.test.ts index 3b5fd33..3845d61 100644 --- a/tests/v043-hardening.test.ts +++ b/tests/v043-hardening.test.ts @@ -12,9 +12,9 @@ import { createSchema } from '../src/table/schema'; import { MemTable } from '../src/engine/aria/index/memtable'; import { MetonaSqlark } from '../src/core'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -25,47 +25,6 @@ function uniqueDB(): string { // OPFS mock(模拟真实 I/O:getFileHandle 延迟 + 写入按内容差异化耗时) // =================================================================== -function mockOPFS( - files?: Map, - opts: { ioDelay?: number; writeDelayFor?: (data: string) => number } = {}, -): Map { - const store = files ?? new Map(); - - const dirMock = { - getDirectoryHandle: async (_name: string, _opts?: any) => dirMock as any, - getFileHandle: async (name: string, fileOpts?: any) => { - if (fileOpts?.create) { - if (opts.ioDelay) { - await new Promise((r) => setTimeout(r, opts.ioDelay)); - } - return { - createWritable: async () => ({ - write: async (d: string) => { - const delay = opts.writeDelayFor ? opts.writeDelayFor(d) : 0; - if (delay > 0) { - await new Promise((r) => setTimeout(r, delay)); - } - store.set(name, d); - }, - close: async () => {}, - }), - }; - } - if (!store.has(name)) throw new Error('Not found'); - return { getFile: async () => ({ text: async () => store.get(name)!, arrayBuffer: async () => new ArrayBuffer(0) }) }; - }, - removeEntry: async (name: string) => { store.delete(name); }, - }; - (dirMock as any).entries = () => ({ - [Symbol.asyncIterator]: async function* () { - for (const [k] of store) yield [k]; - }, - }); - const nav = (globalThis as any).navigator || {}; - nav.storage = { getDirectory: async () => dirMock }; - (globalThis as any).navigator = nav; - return store; -} // =================================================================== // P0: 事务与 checkpoint 冲突 diff --git a/tests/v044-hardening.test.ts b/tests/v044-hardening.test.ts index 336ee41..4cd6368 100644 --- a/tests/v044-hardening.test.ts +++ b/tests/v044-hardening.test.ts @@ -11,9 +11,9 @@ import { AriaEngine } from '../src/engine/aria/index'; import { createSchema } from '../src/table/schema'; import { LSM } from '../src/engine/aria/index/lsm'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -92,7 +92,7 @@ describe('P0 — flush 报告后台失败', () => { saved = 0; deleted = 0; metas: { id: number; level: number }[] = []; - async save(id: number, _data: Uint8Array): Promise { + async save(_id: number, _data: Uint8Array): Promise { if (this.failNext) { this.failNext = false; throw new Error('disk full (simulated)'); @@ -159,7 +159,7 @@ describe('P1 — 提交顺序与 close 等待', () => { await engine.commitTransaction(); // 崩溃恢复路径:WAL 完整则恢复数据 const backend = (engine as any).backend; - const walKeys = (await backend.listKeys()).filter((k) => k.startsWith('__wal_')); + const walKeys = (await backend.listKeys()).filter((k: string) => k.startsWith('__wal_')); expect(walKeys.length).toBeGreaterThan(0); await engine.close(); }); diff --git a/tests/v045-hardening.test.ts b/tests/v045-hardening.test.ts index 88bcace..fd08de9 100644 --- a/tests/v045-hardening.test.ts +++ b/tests/v045-hardening.test.ts @@ -14,9 +14,9 @@ import { SSTableBuilder } from '../src/engine/aria/index/sstable_builder'; import { SSTableReader } from '../src/engine/aria/index/sstable'; import type { SSTableMeta } from '../src/engine/aria/types'; -import { installOPFSMock } from './helpers/opfs-mock'; +import { resetOPFSMock } from './helpers/storage-harness'; -beforeEach(() => { installOPFSMock(new Map()); }); +beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { @@ -115,7 +115,7 @@ describe('P1 — v1 旧格式兼容', () => { let blockSize = 4; for (const e of encoded) blockSize += 2 + e.keyBytes.length + 2 + e.valueBytes.length; const indexSize = 4 + 2 + encoded[encoded.length - 1].keyBytes.length + 8; - const bloomSize = 0; + const _bloomSize = 0; const total = blockSize + indexSize + 32; const buf = new ArrayBuffer(total); const view = new DataView(buf); diff --git a/tests/v073-fixes.test.ts b/tests/v073-fixes.test.ts index b3cdf72..4e1d6d0 100644 --- a/tests/v073-fixes.test.ts +++ b/tests/v073-fixes.test.ts @@ -15,6 +15,7 @@ import { MetonaSqlark } from '../src/core'; import { MemoryEngine } from '../src/engine/memory'; +import { rows as rowsOf } from './helpers/assertions'; describe('v0.7.3: 索引列 IS NULL(三引擎对齐)', () => { test.each([ @@ -28,9 +29,9 @@ describe('v0.7.3: 索引列 IS NULL(三引擎对齐)', () => { email: { type: 'string', index: true }, }); await db.query("INSERT INTO users VALUES ('1', NULL), ('2', 'a@b.c')"); - const rows = await db.query('SELECT * FROM users WHERE email IS NULL'); + const rows = rowsOf<{ id: string }>(await db.query('SELECT * FROM users WHERE email IS NULL')); expect(rows).toHaveLength(1); - expect((rows[0] as Record).id).toBe('1'); + expect(rows[0].id).toBe('1'); await db.close(); }); @@ -176,9 +177,9 @@ describe('v0.7.3: insert 语句级原子性', () => { let threw = false; try { await db.query("INSERT INTO users VALUES ('b'), ('a')"); } catch { threw = true; } expect(threw).toBe(true); - const rows = await db.query('SELECT * FROM users'); + const rows = rowsOf<{ id: string }>(await db.query('SELECT * FROM users')); expect(rows).toHaveLength(1); - expect((rows[0] as Record).id).toBe('a'); + expect(rows[0].id).toBe('a'); await db.close(); }); @@ -357,9 +358,9 @@ describe('v0.7.3: SELECT * 混别名列投影', () => { const db = await MetonaSqlark.create({ name: 'v073-star-alias', mode: 'memory' }); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, age: { type: 'number' } }); await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)"); - const rows = await db.query('SELECT *, name AS nick FROM users'); + const rows = rowsOf>(await db.query('SELECT *, name AS nick FROM users')); expect(rows).toHaveLength(1); - const row = rows[0] as Record; + const row = rows[0]; expect(row.id).toBe('1'); expect(row.name).toBe('Alice'); expect(row.age).toBe(30); @@ -371,8 +372,8 @@ describe('v0.7.3: SELECT * 混别名列投影', () => { const db = await MetonaSqlark.create({ name: 'v073-star-only', mode: 'memory' }); await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } }); await db.query("INSERT INTO users VALUES ('1', 'Alice')"); - const rows = await db.query('SELECT * FROM users'); - expect(Object.keys(rows[0] as Record).sort()).toEqual(['id', 'name']); + const rows = rowsOf>(await db.query('SELECT * FROM users')); + expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']); await db.close(); }); @@ -380,8 +381,8 @@ describe('v0.7.3: SELECT * 混别名列投影', () => { const db = await MetonaSqlark.create({ name: 'v073-star-const', mode: 'memory' }); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO users VALUES ('1')"); - const rows = await db.query("SELECT *, 'lit' AS c FROM users"); - const row = rows[0] as Record; + const rows = rowsOf>(await db.query("SELECT *, 'lit' AS c FROM users")); + const row = rows[0]; expect(row.id).toBe('1'); expect(row.c).toBe('lit'); await db.close(); @@ -422,7 +423,7 @@ describe('v0.7.3: KVStore insert 持久化 validated 行', () => { await db.query("INSERT INTO users VALUES ('1')"); await db.close(); const db2 = await MetonaSqlark.create({ name: 'v073-kv-validated', mode: 'disk', diskEngine: 'memory' }); - const rows = await db2.query('SELECT * FROM users'); + const rows = rowsOf>(await db2.query('SELECT * FROM users')); expect(rows).toHaveLength(1); expect(rows[0]).toEqual({ id: '1', age: 18 }); await db2.close(); @@ -434,8 +435,8 @@ describe('v0.7.3: KVStore insert 持久化 validated 行', () => { await db.table('users').insert({ id: '1', junk: 'x' } as never); await db.close(); const db2 = await MetonaSqlark.create({ name: 'v073-kv-extra-col', mode: 'disk', diskEngine: 'memory' }); - const rows = await db2.query('SELECT * FROM users'); - expect(Object.keys(rows[0] as Record).sort()).toEqual(['id']); + const rows = rowsOf>(await db2.query('SELECT * FROM users')); + expect(Object.keys(rows[0]).sort()).toEqual(['id']); await db2.close(); }); @@ -537,9 +538,9 @@ describe('v0.7.3: $and 等值条件下推', () => { age: { type: 'number' }, }); await db.query("INSERT INTO users VALUES ('1', 'a', 10), ('2', 'a', 20), ('3', 'b', 10)"); - const rows = await db.query("SELECT * FROM users WHERE tag = 'a' AND age = 10"); + const rows = rowsOf<{ id: string }>(await db.query("SELECT * FROM users WHERE tag = 'a' AND age = 10")); expect(rows).toHaveLength(1); - expect((rows[0] as Record).id).toBe('1'); + expect(rows[0].id).toBe('1'); await db.close(); }); @@ -569,15 +570,15 @@ describe('v0.7.3: SELECT 常量列 SQL 标准转义', () => { const db = await MetonaSqlark.create({ name: 'v073-escape', mode: 'memory' }); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO users VALUES ('1')"); - const rows = await db.query("SELECT 'O''Brien' AS name FROM users"); - expect((rows[0] as Record).name).toBe("O'Brien"); + const rows = rowsOf<{ name: string }>(await db.query("SELECT 'O''Brien' AS name FROM users")); + expect(rows[0].name).toBe("O'Brien"); await db.close(); }); test("无表查询 SELECT 'a''b' AS x 转义还原", async () => { const db = await MetonaSqlark.create({ name: 'v073-escape-notable', mode: 'memory' }); - const rows = await db.query("SELECT 'a''b' AS x"); - expect((rows[0] as Record).x).toBe("a'b"); + const rows = rowsOf<{ x: string }>(await db.query("SELECT 'a''b' AS x")); + expect(rows[0].x).toBe("a'b"); await db.close(); }); @@ -585,8 +586,8 @@ describe('v0.7.3: SELECT 常量列 SQL 标准转义', () => { const db = await MetonaSqlark.create({ name: 'v073-escape-star', mode: 'memory' }); await db.defineTable('users', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO users VALUES ('1')"); - const rows = await db.query("SELECT *, 'x''y' AS c FROM users"); - const row = rows[0] as Record; + const rows = rowsOf>(await db.query("SELECT *, 'x''y' AS c FROM users")); + const row = rows[0]; expect(row.id).toBe('1'); expect(row.c).toBe("x'y"); await db.close(); diff --git a/tests/v074-fixes.test.ts b/tests/v074-fixes.test.ts index b25fb5c..bfaa586 100644 --- a/tests/v074-fixes.test.ts +++ b/tests/v074-fixes.test.ts @@ -16,7 +16,6 @@ */ import { MetonaSqlark } from '../src/core'; -import { MemoryEngine } from '../src/engine/memory'; import { KVStore } from '../src/engine/kvstore/index'; import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium'; @@ -449,7 +448,6 @@ describe('v0.7.4: Hybrid beginTransaction 部分成功补偿', () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, }); - const disk = (db.getEngine() as { getDiskEngineType?: () => string } & Record); // 让磁盘引擎 begin 抛错:先把磁盘引擎置于活跃事务 const diskEngine = (db.getEngine() as unknown as { getDiskEngine?: () => { beginTransaction(): Promise } }).getDiskEngine ? undefined @@ -458,6 +456,7 @@ describe('v0.7.4: Hybrid beginTransaction 部分成功补偿', () => { // 内存引擎先 begin 成功、磁盘引擎第二个 begin 报 TX_ACTIVE const hybridEngine = db.getEngine() as unknown as { beginTransaction: () => Promise; + rollbackTransaction: () => Promise; }; await hybridEngine.beginTransaction(); // 磁盘引擎现在活跃;再次 begin → 内存 begin 成功、磁盘抛 TX_ACTIVE → 补偿回滚 diff --git a/tests/version-contract.test.ts b/tests/version-contract.test.ts new file mode 100644 index 0000000..e624be7 --- /dev/null +++ b/tests/version-contract.test.ts @@ -0,0 +1,45 @@ +/** + * 版本一致性契约测试(v0.8.0) + * + * 背景:此前版本号被硬编码在多个测试文件里(tests/v025-fixes.test.ts:31、 + * tests/v033-fixes.test.ts:406),每次发版必须手改测试;而且只校验 src 常量, + * 与 package.json / dist 产物之间没有任何一致性检查 —— 三者可以静默漂移。 + * + * 本测试把"版本号"变成一个单点校验的契约: + * 1. src/constants.ts 的 VERSION 是合法 semver; + * 2. package.json 的 version 与 VERSION 完全一致; + * 3. (可选)dist 产物存在时,其内嵌 VERSION 必须与上述一致。 + * + * 第 3 条让"dist 与 src 漂移"(CI 无同步校验)变成红灯。 + */ +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { VERSION } from '../src/constants'; + +const ROOT = join(__dirname, '..'); + +describe('[v0.8.0] 版本一致性契约', () => { + test('src/constants.ts VERSION 是合法语义化版本', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); + + test('package.json version 与 VERSION 常量一致', () => { + const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) as { version: string }; + expect(pkg.version).toBe(VERSION); + }); + + test('dist 产物内嵌 VERSION 与源码一致(dist 存在时)', () => { + const distFile = join(ROOT, 'dist', 'metona-sqlark.esm.js'); + if (!existsSync(distFile)) { + // dist 未构建时不失败(CI 会先 build),但要显式跳过而不是静默通过 + expect(existsSync(distFile)).toBe(false); + return; + } + const content = readFileSync(distFile, 'utf8'); + // 构建产物把源码常量内联为 `const VERSION = 'x.y.z';` + // (见 rollup 输出,不要去扫描文件里全部字符串字面量 —— 那会命中无关数字) + const match = content.match(/\bVERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/); + expect(match).not.toBeNull(); + expect(match![1]).toBe(VERSION); + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..9cc15d3 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,25 @@ +{ + /* + * v0.8.0 新增:测试代码的类型检查配置。 + * + * 背景:jest 用 babel-jest 转译,@babel/preset-typescript **只剥离类型、不做类型检查**, + * 而根 tsconfig.json 的 include/exclude 把 tests/ 排除在外 —— 于是测试代码里的 + * 类型错误、拼写错误、未 await 的断言全部不可见。审计中发现的实例: + * tests/v025-fixes.test.ts 里 `diskEngine: 'opfs'` 出现在标题为 indexeddb 的用例中, + * 长期无人发现。 + * + * 用法:npm run typecheck:tests + */ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + /* 根 tsconfig 的 rootDir 是 src;测试要一起检查,必须放宽到仓库根 */ + "rootDir": ".", + "types": ["node", "jest"], + /* 测试里大量使用 `as any` 构造边界输入,保留宽松度但不放弃检查 */ + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["src/**/*.ts", "tests/**/*.ts"], + "exclude": ["node_modules", "dist", "coverage"] +}