---
title: sql.js 实战：内存 SQLite、外键与显式保存
description: 从 ChatCrystal 数据层理解 sql.js 的加载、事务和导出边界，用隔离练习验证外键、回滚与重新加载。
image: "https://zengliangyi.com/og/blog/sqljs-wasm-basics.png"
---

sql.js 把 SQLite 编译为 WebAssembly，并提供 JavaScript 接口。ChatCrystal 用它在 Node 服务里加载数据库字节、执行 SQL，再显式导出保存。它没有为每次 INSERT 自动写入应用的数据库文件；理解这个区别，比先比较跑分更有用。

## 沿真实初始化入口阅读

固定 [数据层入口](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/db/index.ts) 先加载 WASM，再通过 readFileSync 读整个已有文件并创建 SQL.Database；不存在文件时新建。打包后的 Electron 通过 locateFile 从 resourcesPath 找 sql-wasm.wasm。随后设置 PRAGMA、执行迁移、补齐记忆元数据并保存。数据库已有 embedding 行时，启动代码还会把相应笔记状态改为 done；这不是检查 Vectra 文件是否完好的证明，参见 [索引恢复](/blog/vectra-index-recovery)。

源码执行 `PRAGMA journal_mode = WAL`，下面的独立练习在核对的 sql.js 1.14.2 环境里也返回 wal。但这个返回值不能证明每次提交会自动同步到应用的数据库文件，或与另一个宿主进程协调文件锁。应用持久化仍由整库 export 与 writeFileSync 完成；应同时检查 PRAGMA 结果与实际宿主文件写入路径。

## 一个不读取个人数据的练习

在空目录使用 Node 24 以上，安装固定版本，将代码保存为 lesson.mjs 后运行：



```bash
npm   init   -y  
  npm   install   sql.js@1.14.2  
  node   lesson.mjs
```





```javascript
// lesson.mjs: synthetic data, entirely in memory.  
  import   assert   from   'node:assert/strict'  ;  
  import   initSqlJs   from   'sql.js'  ;  
  
  const   SQL   =   await   initSqlJs  ();  
  const   db   =   new   SQL  .  Database  ();  
  let   restored;  
  try   {  
    db.  run  (  'PRAGMA foreign_keys = ON'  );  
    console.  log  (  'journal_mode:'  , db.  exec  (  'PRAGMA journal_mode = WAL'  )[  0  ].values[  0  ][  0  ]);  
    db.  run  (  `CREATE TABLE parent (id INTEGER PRIMARY KEY);  
      CREATE TABLE child (  
        id INTEGER PRIMARY KEY,  
        parent_id INTEGER REFERENCES parent(id)  
      );`  );  
    assert.  equal  (db.  run  (  'INSERT INTO parent VALUES (?)'  , [  1  ]), db);  
    assert.  equal  (db.  getRowsModified  (),   1  );  
    db.  run  (  'INSERT INTO child VALUES (?, ?)'  , [  1  ,   1  ]);  
    assert.  throws  (()   =>   db.  run  (  'INSERT INTO child VALUES (?, ?)'  , [  2  ,   99  ]));  
  
    db.  run  (  'BEGIN'  );  
    try   {  
      db.  run  (  'INSERT INTO parent VALUES (?)'  , [  2  ]);  
      db.  run  (  'INSERT INTO child VALUES (?, ?)'  , [  3  ,   99  ]);  
      db.  run  (  'COMMIT'  );  
    }   catch   {  
      db.  run  (  'ROLLBACK'  );  
    }  
    assert.  deepEqual  (db.  exec  (  'SELECT id FROM parent ORDER BY id'  )[  0  ].values, [[  1  ]]);  
  
    let   bytes;  
    try   {  
      bytes   =   db.  export  ();  
    }   finally   {  
      db.  run  (  'PRAGMA foreign_keys = ON'  );  
    }  
    assert.  equal  (db.  exec  (  'PRAGMA foreign_keys'  )[  0  ].values[  0  ][  0  ],   1  );  
    assert.  throws  (()   =>   db.  run  (  'INSERT INTO child VALUES (?, ?)'  , [  4  ,   99  ]));  
    restored   =   new   SQL  .  Database  (bytes);  
    restored.  run  (  'PRAGMA foreign_keys = ON'  );  
    assert.  deepEqual  (restored.  exec  (  'SELECT id, parent_id FROM child'  )[  0  ].values, [[  1  ,   1  ]]);  
    console.  log  (  'foreign keys, rollback, and exported bytes checked'  );  
  }   finally   {  
    restored?.  close  ();  
    db.  close  ();  
  }
```



这段代码只创建合成父子记录，不启动 ChatCrystal，不读取或覆盖数据库文件。故意删除 export 后 finally 中的外键恢复语句，外键状态断言应失败；恢复后再通过。成功完成 SQL 事务与成功保存文件，是两项不同的验证。

## API 与事务各自保证什么

`exec()` 返回包含 columns 与 values 的结果集数组；没有行时可能没有结果集。`run()` 忽略结果行，但返回 Database 本身，并非 undefined。`getRowsModified()` 读取最近一次 INSERT、UPDATE 或 DELETE 的受影响行数，不是 SELECT 的行数。这些返回边界可对照 [官方 Database API](https://sql.js.org/documentation/Database.html)。

应用的 [withTransaction](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/db/transaction.ts) 用 WeakMap 记录深度：顶层 BEGIN/COMMIT，内层 SAVEPOINT/RELEASE，异常则回滚对应层。回调是同步执行的；把 async 回调传进去会先返回 Promise，封装不会等待它，不能把 await 后的写入算进同一个受保护事务。网络或模型请求应先完成，再进入同步数据库修改段。

## 保存策略需要单独设计

应用的 saveDatabase 导出 Uint8Array，再用 Buffer.from 与 writeFileSync 写整库；export 的 finally 恢复 foreign_keys。默认自动保存间隔是 30 秒，closeDatabase 也先保存再关闭。定时器不会保证每 30 秒都成功落盘，进程崩溃、磁盘错误或退出超时仍可能丢失未保存更改。

当前实现没有临时文件加原子替换，也没有为多个进程共享文件建立写锁。因此应避免多个服务或桌面实例同时写同一数据目录。对文件大小、导出耗时、峰值内存与恢复结果做实际记录，参见 [导入内存预算](/blog/conversation-import-memory-budget)；不要仅凭 WASM 或内存数据库就给出“秒级”“GB 以下安全”的承诺。

完成练习后，再从 migrations 追踪真实 notes、messages 与关联表的约束。在合成库中验证重复迁移、级联删除和保存后重新加载，才能判断这些路径是否符合预期。

分享：

##  相关文章

[##  AI 对话格式差异：事件、快照与跨库消息定位

对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-format-divergence)    [##  AI 对话管理方案：四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入，区分原始副本、可读导出与知识库，并用合成清单验证身份和信息保留。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-management-options)    [##  批量导入 1000 条对话：成本拆分与性能验收

从固定导入链路区分解析、事务、WASM 导出与摘要队列，并用临时合成输入检查流式保留范围与数据等价性。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#SQLite

#Performance](/blog/bulk-import-performance-plan)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "sql.js 实战：内存 SQLite、外键与显式保存",
  "description": "从 ChatCrystal 数据层理解 sql.js 的加载、事务和导出边界，用隔离练习验证外键、回滚与重新加载。",
  "url": "https://zengliangyi.com/blog/sqljs-wasm-basics/",
  "image": "https://zengliangyi.com/images/blog/sqljs-wasm-basics.svg",
  "datePublished": "2026-10-04T00:00:00.000Z",
  "dateModified": "2026-10-04T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/sqljs-wasm-basics/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "sql.js 实战：内存 SQLite、外键与显式保存",
      "item": "https://zengliangyi.com/blog/sqljs-wasm-basics/"
    }
  ]
}
```
