diff --git a/CHANGELOG.md b/CHANGELOG.md index f12203c..3a72a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,121 @@ All notable changes to MetonaSqlark will be documented in this file. +## [0.8.0] - 2026-08-16 + +### 根治性迭代 —— 统一语义 / 消灭复发结构 / 验证基础设施 + +> 依据 `PLAN-v0.7.5.md` 的三条并行工作流(A 缺陷修复 / B 结构根治 / C 验证基础设施) +> 完成的一次系统性迭代。**这一版的重点不是"再修一批 bug",而是砍掉让同类 bug +> 必然复发的结构**:多处并存的语义实现被收敛为唯一实现,并第一次让崩溃语义、 +> 错误码一致性、入口等价性变成可机器验证的门禁。 +> +> 测试规模 1304 → **1872(90 套件)+ 14 项 e2e**(另 4 个重型套件在独立 CI +> job 串行运行)。 + +### 工作流 B · 结构根治(消除整类缺陷) + +- **B-1 唯一校验 choke point** — 此前有**三份**行校验实现,覆盖面各不相同 + (memory 一份缺 `maxLength`/`min`/`max`,Aria 一份有,schema.ts 第三份): + 同一份 schema、同一条 INSERT 是否报错取决于选了哪个引擎(A12);四份实现 + 对未知列一律静默丢弃(A17:INSERT 报成功、`SELECT nope` 报 COLUMN_NOT_FOUND)。 + 现收敛为 `src/table/validation.ts#compileValidator` 唯一实现,四个引擎新增 + `validatePayload` 契约,校验先于任何副作用;未知列、`NaN`/±Infinity + (JSON 无法表示,落盘会变 null)显式拒绝。 +- **B-2 唯一值比较与编码** — `sqlCompare`/`sqlCompareOrder`/`encodeValueKey` + 成为唯一原语;GROUP BY 键、DISTINCT 键、UNION 去重、聚合去重全部改用它 + (此前四份编码并存,对 null/undefined 处理各不相同)。 +- **B-3 单管线** — QueryBuilder 此前**自己执行**:无 JOIN 时直通 `engine.find`, + 写操作直通 `engine.update/delete`,于是"同一条语义"在 TABLE API 与 SQL API + 两条路径上规则各写一份(投影、列校验、LIMIT 下推、`maxRowsPerQuery` 全缺失; + `$subquery` 无人解析 → 静默影响 0 行)。现在 builder 只产出 AST,执行一律经 + Executor;生命周期钩子由 `Table` 注入、顺序与传参不变。 + → 新增 `tests/v080-single-pipeline.test.ts`(两入口逐值等价,四引擎)。 +- **B-4 统一表达式求值** — CASE 此前用**正则**切分 WHEN/THEN/ELSE,不认字符串 + 字面量与嵌套:嵌套 CASE 返回字符串残片 `"big' END ELSE 'small"`;条件引用 + 不存在的列时静默把整列变成 ELSE 值;`GROUP BY CASE ... END` 完全不可用 + (报"未知列 CASE WHEN ...")。现复用 `sql/lexer` 的 token 流做递归下降, + 条件交给与 WHERE 相同的解析器,无法识别的表达式显式报错。 + → 顺带修正 `Token.position` 语义按类型不一致的缺陷(字符串 token 指向引号之内, + 导致按位置切片少一个字符)。 +- **B-5 输出列序号 + 分隔标识符** — `ORDER BY 1` / `GROUP BY 2` 此前直接 + `PARSE_ERROR`;`SELECT "1"`(列名就叫 1)被当成**常量 1**(与 `SELECT *` 结论相反)。 + 现支持输出列序号(越界、`ORDER BY 0`、`GROUP BY <聚合列>` 各自显式报错), + 并统一分隔标识符语义:引号只在"解析→执行"边界脱去。 + 顺带补上 **ORDER BY 的列存在性/歧义校验**(此前 JOIN 里裸写两表同名列既不报错 + 也不确定按哪列排)。 +- **B-6 存储提交点(KVStore)** — 两处 P0:① `open()` 遇损坏日志尾部会**清空整个 + 日志**(写 3 条 → 第 4 条撕裂 → 重开可见 → 再重开全空);② 自动 checkpoint + 失败会让**已确认写入**报错(而该写入已在 WAL 中,报错与事实相反)。另修陈旧实例 + 的 checkpoint 会**静默抹掉**新实例写入(现抛 `STALE_INSTANCE` 拒绝提交)。 + +### 工作流 A · 缺陷修复(24 项,含 6 项事故级) + +- **A15 三值逻辑** — `= NULL` 命中 NULL 行、`!= NULL` 返回所有非 NULL 行、 + `NOT LIKE` 把 NULL 判真、**`NOT BETWEEN 1 AND 2` 恒空集**(字段级 `$or` 递归进了 + where 子句级求值器)。现 WHERE 只有**一个**递归求值器;`IS NULL`/`IS NOT NULL` + 是与比较不同的**谓词**(此前与 `= NULL` 共用同一 AST,语义无法区分)。 +- **A9/A10 发布订阅与关联子查询** — `subscribe()` 对本地写入永不触发(仅跨标签页 + 广播);`WHERE t.x = t.y` 与 `WHERE id IN (SELECT ... WHERE o.user_id = u.id)` + 静默空结果(引擎层预过滤把逐行谓词判 UNKNOWN → 候选行 0)。 +- **A13 自引用外键** — `parent_id REFERENCES node(id)` 的级联被整体跳过: + `DELETE root` 只删根,子树**永久悬挂**(父行已不在,再也无法级联清理)。 + `ON DELETE SET NULL` / `ON UPDATE CASCADE` / RESTRICT 预检同样失效。 +- **A22/A23/A25/A26/A27/A29/A30/A36 查询层** — GROUP BY 别名、HAVING 未选中聚合、 + 带前缀聚合参数恒 0、UNION 尾部子句归属、DISTINCT 作用于输出列、 + `maxRowsPerQuery` 静默截断写入、INSERT 值多于列、派生表别名引用。 +- **A37 列引用** — 未限定列不能作比较操作数(`WHERE x = y` 报 PARSE_ERROR); + `$col` 引用不存在的列**静默返回空集**。 +- **A38/A39 Aria 存储** — `compression` 在页面化路径(默认)被静默忽略; + `compressLZ4` 匹配搜索 O(n²)(60KB 伪随机 2345ms → 6ms,**390×**)。 +- **A41 DDL 原子性** — DDL 的 WAL 意图记录写在生效**之后**:`dropTable` 后崩溃 → + 重开表又回来了(DROP 被静默撤销);`alterTable` 完全不写 WAL,崩溃丢失结构变更。 + 现统一为"先写 WAL 意图并刷盘 → 再改内存 → 最后落盘 schema"。 + +### 工作流 C · 验证基础设施(让门禁真的能拦) + +- **真崩溃注入(PC-2)** — e2e 的 `crashPage()` 此前只是 `page.close()` + (**优雅关闭**),所有"崩溃恢复"用例测的其实是"正常关闭后重开"。现改用 CDP + `Page.crash` 终止渲染进程,并新增两个真实窗口:`createWritable().write()` 中途、 + `close()` 原子替换前(copy-on-write 的核心不变量)。 +- **覆盖率门禁真正生效** — `collectCoverageFrom` 不再排除实现文件 + (此前 `!src/**/index.ts` 把 2282 行的 AriaEngine 整文件排除在统计外, + 于是"90.1% 行覆盖率"是虚高的口径);新增 `coverageThreshold` + (statements 90 / branches 82 / functions 94 / lines 93);CI 常规 job 带 + `--coverage`;lint 去掉 `continue-on-error`;新增 `tests/` 类型检查 + (修复 **103 个**被 babel 剥离类型掩盖的测试类型错误);CI 校验 dist 与源码同步。 + → 实测 Statements 90.43% / Branches 82.21% / Functions 94.27% / Lines 93.44% + (命令与 CI 常规 job 完全一致,可复现)。 +- **测试介质忠实性修正**(两处同源缺陷,此前让所有多实例/多库验证跑在错误语义上) + - `SharedMemoryBackend` 的读缓存是每实例私有的 → 介质退化为"每实例一份快照", + 跨实例写入不可见(这正是陈旧实例覆盖新实例写入那条 bug 起初查不出来的原因); + - OPFS mock 把 `getDirectoryHandle(name)` 的库名**丢弃** → 所有库共用一棵文件树 + (`open('db-beta')` 能看到 `db-alpha` 的表)。 +- **变异验证成为回归套件的标准做法** — 把修复回退到修复前的行为,对应用例必须 + 失败。本版 A15 / B-6(①②③) / A13 / A37 / A38 / A39 / A41 / B-4 / B-5 全部通过该检查 + ——这是"测试真能拦住回归"与"测试只是陪跑"的分界线。 + +### 文档与宣称同步(G6) + +- 修正 README 全部**不成立的能力宣称**:覆盖率数字改为**四个准确数字 + 明确口径**; + 测试规模与套件数更新为实测值;"5 种存储引擎"改为"4 种模式 + 3 种后端"; + `backup()` 由"在线一致性快照"改为"全库导出(逐表读取)"并写入已知限制 + (引擎层没有跨表快照原语);`db.disconnect()` 真正进入类型系统 + (此前仅运行时注入,TypeScript 使用者编译失败)。 +- **打包缺陷修复**:`package.json` 的 `./migration` 子路径此前指向的产物里 + **没有** `migrateFromIndexedDB`(主入口未导出该函数)→ 现主入口导出 + 子路径可用; + `./react` / `./vue` 此前指向**裸 TS 源码**且声明类型为 `any` + → 现构建 `dist/react.js` / `dist/vue.js` 并配套**手写精确类型声明**; + 补 `peerDependencies`(react / vue,均可选)。 + +### 已知限制(v0.8.0 新增/变更) + +- `backup()` 不是跨表一致性快照(逐表读取) +- 复合主键仍不支持(建表时 `SCHEMA_ERROR`) +- 简单 CASE 形式(`CASE <表达式> WHEN <值>`)不支持,仅支持搜索式 + (`CASE WHEN <条件> THEN ...`) + + ## [0.7.4] - 2026-08-15 ### 写语句子查询 / 约束硬化 / 真惰性流式 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba1c3e8..31d2b91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,7 +134,16 @@ MetonaSqlark follows a layered architecture: 2. **Write-Through Hybrid Strategy**: When using `mode: 'hybrid'`, all writes go to both memory and disk simultaneously. Reads always hit memory for maximum speed. -3. **Plugin Hook Pipeline**: 14 lifecycle hooks allow intercepting database operations without modifying core code. +3. **Plugin Hook Pipeline**: 14 lifecycle hooks let plugins observe and adjust database + operations without modifying core code. **Hook contract (v0.8.0)**: + - return values are **ignored** (you cannot cancel an operation by returning `false`, + nor rewrite the SQL by returning a string); + - **mutate the argument object in place** to change it — this works on the Table API + path (`db.table(...).insert(rows)`); + - **throw** to abort the operation (the error propagates to the caller); + - the SQL path passes a **copy** for `beforeInsert`, so mutating it there has no effect. + Use the Table API when you need to transform rows. + These rules are covered by tests; changing them requires updating this section. 4. **Hand-Written SQL Parser**: No dependencies on parser generators — a recursive-descent parser keeps the bundle size minimal. @@ -163,7 +172,9 @@ const myPlugin: MetonaPlugin = { name: 'myPlugin', version: '1.0.0', description: 'Description of my plugin', - priority: 50, // higher = executed first + // higher = executed first(v0.8.0 起真正生效:install 与钩子都按优先级降序; + // 同优先级保持 plugins 数组顺序) + priority: 50, install(db) { // Use db.on() to subscribe to hooks diff --git a/README.md b/README.md index a2846ee..cfd89b1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # MetonaSqlark
-
+
-
-
+
+
o+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 n=a.decode(l.subarray(f,f+t));f+=t;const s=u.getUint32(f,!1);if(f+=4,f+s>o+4){m=!1;break}const i=l.slice(f,f+s).buffer;f+=s,y.push({op:e,key:n,value:i})}if(!m){if(n&&!n(h))break;break}t({seq:d,entries:y,raw:l}),i++,s=c}return i}(e,e=>{e.seq<=n||(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.truncateLogTo(this.findValidLogLength(e))}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.instanceId="",this.stale=!1,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(await this.assertOwnership(),null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new s("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=oe(this.seq,this.index);await this.medium.write(le,e.buffer),await this.writeMeta(),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(ue,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(le);t&&!he(new Uint8Array(t))&&(await this.medium.delete(le),e++);const n=await this.medium.read(ce);if(n&&n.byteLength>0){const t=new Uint8Array(n),s=this.findValidLogLength(t);if(s o+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 n=a.decode(l.subarray(f,f+t));f+=t;const s=u.getUint32(f,!1);if(f+=4,f+s>o+4){m=!1;break}const i=l.slice(f,f+s).buffer;f+=s,y.push({op:e,key:n,value:i})}if(!m){if(n&&!n(c))break;break}t({seq:d,entries:y,raw:l}),i++,s=h}return i}(e,e=>{e.seq<=n||(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.truncateLogTo(this.findValidLogLength(e))}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.instanceId="",this.stale=!1,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(await this.assertOwnership(),null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new s("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=oe(this.seq,this.index);await this.medium.write(le,e.buffer),await this.writeMeta(),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(ue,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(le);t&&!ce(new Uint8Array(t))&&(await this.medium.delete(le),e++);const n=await this.medium.read(he);if(n&&n.byteLength>0){const t=new Uint8Array(n),s=this.findValidLogLength(t);if(sthis.bindWhereRefs(e,t,n));else if("$not"===i&&"object"==typeof r&&null!==r)s[i]=this.bindColumnRefs(r,t,n);else if("object"==typeof r&&null!==r&&!Array.isArray(r)&&"$col"in r)s[i]=this.lookupOuterValue(t,String(r.$col),n);else if("object"==typeof r&&null!==r&&!Array.isArray(r)&&"$subquery"in r){const e=r.$subquery,a=e.where&&this.hasCorrelatedRefs(e.where)?this.bindWhereRefs(e.where,t,n):e.where;s[i]={$subquery:{...e,where:a}}}else s[i]=r;return s}lookupOuterValue(e,t,n){const s=this.stripAlias(t,n);return s in e?e[s]:t in e?e[t]:null}bindWhereRefs(e,t,n=[]){const s={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?s[i]=r.map(e=>this.bindWhereRefs(e,t,n)):"$not"===i?s.$not=this.bindWhereRefs(r,t,n):s[i]="$exists"===i?r:this.bindColumnRefs(r,t,n);return s}async resolveSubqueries(e,t,n=[]){t&&(e=this.bindWhereRefs(e,t,n));const s={};for(const[i,r]of Object.entries(e)){if("$exists"===i&&"object"==typeof r&&null!==r){const e=r,n=e.$subquery,i=!!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!==i;continue}"$and"===i&&Array.isArray(r)?s.$and=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n))):"$or"===i&&Array.isArray(r)?s.$or=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n))):"$not"!==i||"object"!=typeof r||null===r?s[i]="object"==typeof r&&null!==r?await this.resolveOperatorSubqueries(r,t,n):r:s.$not=await this.resolveSubqueries(r,t,n)}return s}async resolveOperatorSubqueries(e,t,n=[]){const s={};for(const[i,r]of Object.entries(e))if("$and"===i&&Array.isArray(r))s.$and=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n)));else if("$or"===i&&Array.isArray(r))s.$or=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n)));else if("$not"!==i)if("object"==typeof r&&null!==r&&"$subquery"in r){let e=r.$subquery;t&&this.hasCorrelatedRefs(e.where)&&(e={...e,where:this.bindWhereRefs(e.where,t,n)});const a=await this.executeSelect(e);if("$in"===i||"$nin"===i){const e=Object.keys(a[0]||{})[0],t=a.map(t=>t[e]);s[i]=t}else if(0===a.length)s[i]=null;else{const e=Object.keys(a[0])[0];s[i]=a[0][e]}}else s[i]=r;else s.$not="object"==typeof r&&null!==r?await this.resolveOperatorSubqueries(r,t,n):r;return s}}function _t(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 s("Object/array query parameters are not supported by SQL binding (pass JSON strings explicitly)","PARAM_ERROR")}function Mt(e,t){if(!t)return e;let n="",i=0,r=0,a=null;for(;ithis.bindWhereRefs(e,t,n));else if("$not"===i&&"object"==typeof r&&null!==r)s[i]=this.bindColumnRefs(r,t,n);else if("object"==typeof r&&null!==r&&!Array.isArray(r)&&"$col"in r)s[i]=this.lookupOuterValue(t,String(r.$col),n);else if("object"==typeof r&&null!==r&&!Array.isArray(r)&&"$subquery"in r){const e=r.$subquery,a=e.where&&this.hasCorrelatedRefs(e.where)?this.bindWhereRefs(e.where,t,n):e.where;s[i]={$subquery:{...e,where:a}}}else s[i]=r;return s}lookupOuterValue(e,t,n){const s=this.stripAlias(t,n);return s in e?e[s]:t in e?e[t]:null}bindWhereRefs(e,t,n=[]){const s={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?s[i]=r.map(e=>this.bindWhereRefs(e,t,n)):"$not"===i?s.$not=this.bindWhereRefs(r,t,n):s[i]="$exists"===i?r:this.bindColumnRefs(r,t,n);return s}async resolveSubqueries(e,t,n=[]){t&&(e=this.bindWhereRefs(e,t,n));const s={};for(const[i,r]of Object.entries(e)){if("$exists"===i&&"object"==typeof r&&null!==r){const e=r,n=e.$subquery,i=!!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!==i;continue}"$and"===i&&Array.isArray(r)?s.$and=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n))):"$or"===i&&Array.isArray(r)?s.$or=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n))):"$not"!==i||"object"!=typeof r||null===r?s[i]="object"==typeof r&&null!==r?await this.resolveOperatorSubqueries(r,t,n):r:s.$not=await this.resolveSubqueries(r,t,n)}return s}async resolveOperatorSubqueries(e,t,n=[]){const s={};for(const[i,r]of Object.entries(e))if("$and"===i&&Array.isArray(r))s.$and=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n)));else if("$or"===i&&Array.isArray(r))s.$or=await Promise.all(r.map(e=>this.resolveSubqueries(e,t,n)));else if("$not"!==i)if("object"==typeof r&&null!==r&&"$subquery"in r){let e=r.$subquery;t&&this.hasCorrelatedRefs(e.where)&&(e={...e,where:this.bindWhereRefs(e.where,t,n)});const a=await this.executeSelect(e);if("$in"===i||"$nin"===i){const e=Object.keys(a[0]||{})[0],t=a.map(t=>t[e]);s[i]=t}else if(0===a.length)s[i]=null;else{const e=Object.keys(a[0])[0];s[i]=a[0][e]}}else s[i]=r;else s.$not="object"==typeof r&&null!==r?await this.resolveOperatorSubqueries(r,t,n):r;return s}}function _t(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 s("Object/array query parameters are not supported by SQL binding (pass JSON strings explicitly)","PARAM_ERROR")}function Mt(e,t){if(!t)return e;let n="",i=0,r=0,a=null;for(;i