lines-db
lines-db将JSONL文件加载到SQLite表中,并将更改写回它们的源文件。将它用于保存在文件中的种子数据和测试装置。
列是从行推断出来的。用标准模式库(如Valibot或Zod)定义一个模式,以验证行并生成TypeScript类型。对象和数组存储为JSON列。模式可以在加载时转换值,并在保存时转换回来。
CLI验证、迁移、填充缺失的值,并生成类型。安装 @lines-db/studio 在浏览器中编辑数据,或使用 VS Code扩展 在编辑器中进行验证。
安装
要求Node.js 22.18.0或更高版本。
npm install lines-db
对于pnpm,使用 pnpm add lines-db。
CLI使用
定义模式
在JSONL文件旁边放置一个模式文件,使用相同的表名。
data/
├── users.jsonl
├── users.schema.ts
├── products.jsonl
└── products.schema.ts
以下 users.schema.ts 使用Valibot。加上 npm install valibot。
import * as v from 'valibot';
import { defineSchema } from 'lines-db';
export const schema = defineSchema(
v.object({
id: v.pipe(v.number(), v.integer(), v.minValue(1)),
name: v.pipe(v.string(), v.minLength(1)),
age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150)),
email: v.pipe(v.string(), v.email()),
}),
);
export default schema;
任何库实现 标准模式 可以使用。
initialize() 如果无法导入架构文件或未将标准架构导出为,则失败 schema 或 default. 错误包括文件和错误原因。
验证json文件
根据模式验证一个文件或目录中的所有json文件。
npx lines-db validate <path>
npx lines-db validate ./data
npx lines-db validate ./data/users.jsonl
npx lines-db validate ./data --verbose
该命令加载相应的模式文件并检查每一行。验证错误包括失败的文件和详细信息。
验证命名数据集
中定义共享数据和场景数据 lines-db.config.json:
{
"schemaDir": "./data",
"base": ["./data"],
"datasets": {
"cogs": ["./data/cogs"],
"sales": ["./data/sales"]
}
}
省略要读取的位置路径 lines-db.config.json 在当前工作目录中,并仅进行验证 base。
--dataset <name> 将命名的数据集添加到 base;重复该标志,以指定的顺序组合多个集合。 --all-datasets 结合 base 每个数据集都是一个
验证运行;不同场景共享的id会导致重复错误。
npx lines-db validate
npx lines-db validate --dataset cogs
npx lines-db validate --dataset cogs --dataset sales
npx lines-db validate --all-datasets
npx lines-db validate --config ./seed/lines-db.config.json --dataset cogs
使用 --config <path> 选择其他配置文件。 schemaDir, base 和 datasets 是必需的。
base 并且每个数据集必须包含至少一个目录;使用 datasets: {} 当没有额外的集合时。
相对路径是从配置文件的目录中解析的。目录以base-then-dataset格式加载
顺序,每个目录只加载一次。不会自动发现子目录。模式,独特的
在合并的数据中检查约束和外键,错误识别原始文件。
结合 --dataset 与 --all-datasets,或使用带有位置路径的配置选项是错误的。
未知的数据集名称和无效的配置也会失败。现有的 validate <path> 命令继续工作
不读取配置文件。只有 validate CLI命令使用该配置。
程序可以加载相同的配置 loadDatasetConfig():
import { LinesDB, loadDatasetConfig } from 'lines-db';
const config = await loadDatasetConfig('./lines-db.config.json', {
datasets: ['cogs', 'sales'],
});
const db = LinesDB.create(config);
省略 datasets 或通过 [] 只针对碱。数组按数组顺序将基本集合和选定集合组合在一起;
'all' 组合基础和每组在配置顺序。每个目录加载一次,包括符号链接别名。
调用 loadDatasetConfig() 不带参数读取 lines-db.config.json 在当前工作目录中。
它返回 Promise<DatabaseConfig> 拒绝不可读或无效的配置、未知的集合名称和缺失
选定的数据目录。一个失踪的 schemaDir 或者指向文件的也会被拒绝。呼叫 db.initialize() 加载并验证数据。
迁移数据
对每一行应用一个函数,验证结果,并将其保存到JSONL文件中。
npx lines-db migrate <file> <transform> [options]
npx lines-db migrate ./data/users.jsonl "(row) => ({ ...row, age: row.age + 1 })"
npx lines-db migrate ./data/users.jsonl "(row) => ({ ...row, active: true })" --filter "{ age: (age) => age > 18 }"
npx lines-db migrate ./data/users.jsonl "(row) => ({ ...row, age: row.age + 1 })" --errorOutput ./migrated.jsonl
npx lines-db migrate ./data "(row) => row" --fields id
可用的选项:
--filter, -f <expr>:选择带有过滤表达式的行。--fields <list>:只回写以逗号分隔的字段。其他值仍然存储在JSONL文件中。如果省略,则写入每个字段。--errorOutput, -e <path>:如果迁移失败,将转换后的数据保存到此路径。--verbose, -v:显示详细的错误信息。
迁移在事务中运行,并在提交之前验证所有转换后的行。
回写选定字段
正常的迁移将整行写回文件,包括模式默认值和省略的字段。使用 --fields 保持其他价值不变。
npx lines-db migrate ./data/users.jsonl "(row) => row" --fields id
行按主键匹配,或者在文件还没有主键时按位置匹配,比如在填充主键时。保留现有的行顺序,并追加新行。如果文件没有主键,并且行数已经更改,则迁移失败,因为无法匹配行。
--fields 适用于所有目标表。没有命名字段的表不会被回写,因此一个列表可以涵盖具有不同列的表。如果每个表中都没有字段,则会导致错误。
填充缺失的值
npx lines-db fill <path...> [--schema-dir ./schemas] [--fields id,createdAt]
传递一个或多个数据目录或json文件。使用 --schema-dir 对于共享模式;否则,模式将加载到每个JSONL文件旁边。该命令用验证模式提供的值填充缺失的字段。没有 --fields,它填充每个表的主键。
现有的值永远不会被替换。没有获得值的行保持不变,包括行尾。更新后的行使用模式的键顺序。不写入验证失败的行。在写入任何文件之前计算所有值。如果另一个工具在读取文件后更改了文件,则该命令不写入任何内容,并以一个 JsonlConflictError 命名该文件。
使用存储在文件中的字段名。当一个 backward 转换重命名主键,默认目标是数据库的主键名;传递文件的字段名 --fields。
fillFields() 接受一个函数来计算值。本例使用 hook 由模式文件导出。如果函数抛出,则不写入任何内容。
import { fillFields, unwrap } from 'lines-db';
const { filled } = unwrap(
await fillFields({
path: './data',
fields: ['id'],
loadFiller: (schemaModule, { schemaPath }) => {
if (typeof schemaModule.hook !== 'function') throw new Error(`${schemaPath} does not export \`hook\``);
return schemaModule.hook;
},
}),
);
结果的 tablesWithoutSchema 列出没有模式的表。 unreadableLines 列出非JSON对象的行,以及 unproducedFields 列出没有表生成值的字段。
fillFields() 的字符串或非空的只读字符串数组 path. 在所有路径上,计算每个值,并在写入之前检查每个挂起的写入是否有外部更改。如果写入失败,则恢复已写入的文件。重叠路径只处理每个文件一次。不可读的行仍然被报告并且保持不变。
unwrap(
await fillFields({
path: ['./data/dataset-a', './data/dataset-b'],
schemaDir: './data',
}),
);
在浏览器中浏览和编辑
在项目中安装Studio并使用CLI启动它。
npm install -D @lines-db/studio
npx lines-db studio ./data --open
默认URL为 http://127.0.0.1:4848. 保存时将验证更改并将其写入json文件。看到 工作室的文档 用于编辑、共享模式和多个数据目录。
使用打印稿
生成类型
从模式生成数据库配置和类型。默认输出为 <dataDir>/db.ts。
npx lines-db generate <dataDir>
npx lines-db generate ./data
来运行这些命令 package.json:
{
"scripts": {
"db:validate": "lines-db validate ./data",
"db:generate": "lines-db generate ./data"
}
}
加载数据
创建 data/users.jsonl。
{"id":1,"name":"Alice","age":30,"email":"[email protected]"}
{"id":2,"name":"Bob","age":25,"email":"[email protected]"}
{"id":3,"name":"Charlie","age":35,"email":"[email protected]"}
从TypeScript中加载目录。
import { LinesDB, unwrap } from 'lines-db';
const db = LinesDB.create({ dataDir: './data' });
unwrap(await db.initialize());
const users = unwrap(db.find('users'));
console.log(users); // [{ id: 1, name: "Alice", ... }, ...]
const user = unwrap(db.findOne('users', { id: 1 }));
console.log(user); // { id: 1, name: "Alice", age: 30, ... }
const adults = unwrap(db.find('users', { age: (age) => age >= 30 }));
unwrap(await db.close());
unwrap() 返回成功对象的值 Result 或者抛出错误。看到 错误处理 用于显式处理故障。
使用生成的类型
生成的配置从表名推断行类型。
import { LinesDB, unwrap } from 'lines-db';
import { config } from './data/db.js';
const db = LinesDB.create(config);
unwrap(await db.initialize());
const users = unwrap(db.find('users'));
unwrap(
db.insert('users', {
id: 10,
name: 'Alice',
age: 30,
email: '[email protected]',
}),
);
unwrap(await db.close());
错误处理
方法在 LinesDB 和 JsonlReader 可以失败返回 Result<T, Error>. 一个成功的结果是 { ok: true, value };失败的结果是 { ok: false, error }。 getSchema, getTableNames, getDb 直接返回它们的值。
const result = db.insert('users', { id: 2, name: '' });
if (result.ok) {
console.log(result.value.changes);
} else {
console.log(result.error.message);
}
unwrap(result) 成功时返回值,失败时抛出错误。当故障应该停止当前操作时使用它。
import { unwrap } from 'lines-db';
const users = unwrap(db.find('users'));
在一个事务中, tx 也返回Results。一个失败的 insert() 或 update() 不会自行回滚事务。要在失败时中止,请使用 unwrap() 或者检查结果并抛出 throw result.error。
通过写 tx 在事务内部。写在 db 来自其他代码的请求将成为同一事务的一部分,因此它们在事务打开时被拒绝。SQL通过 db.getDb(), db.execute(), db.query() 在事务期间是只读的。 SELECT, VALUES, WITH, EXPLAIN,以及只读的pragma,例如 PRAGMA table_info(...) 被接受。运行设置值的事务控制或程序 tx。
API
下面的操作返回 Result<T, Error> 除非另有说明。 getSchema, getTableNames, getDb 直接返回它们的值。
查询
find(table, where?):查找所有匹配的记录。findOne(table, where?)找一条记录。query(sql, params?):运行原始SQL查询。
变化
insert(table, data):插入一条记录。update(table, data, where):更新匹配记录。delete(table, where):删除匹配的记录。
批处理操作
batchInsert(table, data[]):插入几条记录。batchUpdate(table, updates[]):更新几条记录。batchDelete(table, where):删除多条记录。
事务和模式
transaction(fn):在事务中运行操作。sync(table?, options?):将更改写回json文件。getSchema(table):获取模式。返回TableSchema | undefined直接。validateRow(table, row):验证一行,包括由于验证错误而在加载时被排除的表中的一行。返回Result<JsonObject, ValidationError>。getTableNames():获取所有表名。返回string[]直接。
查询条件
unwrap(db.find('users', { age: 30 }));
unwrap(db.find('users', { age: 30, name: 'Alice' }));
unwrap(
db.find('users', {
age: (age) => age > 25,
name: (name) => name.startsWith('A'),
}),
);
JSON列
对象和数组作为JSON列存储,并作为原始值返回。
unwrap(
db.insert('orders', {
id: 1,
items: [{ name: 'Laptop', quantity: 1 }],
metadata: { source: 'web' },
}),
);
const order = unwrap(db.findOne('orders', { id: 1 }));
console.log(order.items[0].name); // "Laptop"
JSON列保留插入时的任何值。每个值都存储为JSON文本,字符串加引号,所以字符串 "123" 返回为 "123",和数字 123 作为 123. 因此,在SQL中,字符串以其引号形式进行比较: WHERE data = '"abc"'。
模式转换
如果模式将日期字符串转换为 Date 对象加载时,定义 backward 转换,以便在保存时将它们转换为字符串。JSONL存储字符串;应用程序使用 Date 对象。
import * as v from 'valibot';
import { defineSchema } from 'lines-db';
const eventSchema = v.pipe(
v.object({
id: v.number(),
date: v.pipe(
v.string(),
v.isoDate(),
v.transform((str) => new Date(str)),
),
}),
);
export const schema = defineSchema(eventSchema, {
backward: (output) => ({
...output,
date: output.date.toISOString().slice(0, 10),
}),
});
将日期存储为字符串 events.jsonl。
{"id":1,"date":"2024-01-01"}
加载的行将日期公开为 Date。
const event = unwrap(db.findOne('events', { id: 1 }));
console.log(event.date instanceof Date); // true
console.log(event.date.getFullYear()); // 2024
交易
事务外部的更改会自动同步到JSONL文件。
unwrap(db.insert('users', { id: 10, name: 'Alice', age: 30 }));
Sync保留原始的行顺序并追加新行。
使用事务将多个更改保存在一起。
unwrap(
await db.transaction(async (tx) => {
unwrap(tx.insert('users', { id: 10, name: 'Alice', age: 30 }));
unwrap(tx.update('users', { age: 31 }, { id: 1 }));
}),
);
使用 unwrap() 在回调中抛出失败的操作并回滚事务。
回调完成后,在提交之前将更改的表写回。在第一次写入之前检查所有目标文件。如果回写失败,则回滚事务并恢复已写入的文件。如果恢复也失败,则该文件保留其更改。
未修改的表不回写。但是,通过更改行的SQL tx.execute() 或 tx.query() 回写每个表,因为无法识别已更改的表。调用 sync() 在回调内部返回一个错误;事务在最后处理回写。
加载后更改的文件
数据库在读取或写入每个JSONL文件时记录其内容。如果编辑器或其他工具在此之后更改或删除了该文件,则sync将使其保持不变,并返回 JsonlConflictError 命名文件。
const result = await db.transaction((tx) => unwrap(tx.update('users', { age: 31 }, { id: 1 })));
if (!result.ok && result.error.name === 'JsonlConflictError') {
console.log(`${(result.error as JsonlConflictError).file} has changed on disk`);
}
要加载外部更改,请重新创建数据库,调用 initialize(),重试写操作。事务只回写已更改的表,并在发生冲突时回滚数据库更改。
与 sync() 或者在事务外自动同步,数据库更改在回写失败后仍然保留。一个 sync() 没有表名的目标是每个表,因此对任何文件的更改都会导致失败。自动同步通过报告错误 console.error。
文件在检查其内容和写入之间不会被锁定。在此期间由另一个进程保存的更改将被覆盖。这适用于一个人编辑本地文件;它不协调并发写器。
hasExternalChanges() 检测自上次读写以来JSONL和模式文件的更改、添加和删除。 findExternalChanges() 列出受影响的文件。使用它们检测其他工具所做的编辑,并在长时间运行的流程中重新加载数据。
if (unwrap(await db.hasExternalChanges())) {
await db.close();
db = LinesDB.create(config);
unwrap(await db.initialize());
}
模式填充的值
普通同步写入整行,包括模式默认值和计算值。与 writeFilledValues: 'primaryKey',它写入文件中存在的字段,传递给 insert(),或被 update(). 其他默认值保持省略。生成的主键将被保存,以便该行在下一次加载时保持其标识。
const db = LinesDB.create({ dataDir: './data', writeFilledValues: 'primaryKey' });
// people.jsonl: {"id":1,"name":"Alice"}
unwrap(await db.transaction((tx) => unwrap(tx.update('people', { name: 'Alicia' }, { id: 1 }))));
// people.jsonl: {"id":1,"name":"Alicia"}
使用 sync(tableName, { writeFilledValues }) 覆盖一个同步的设置。将字段设置为其现有值不是更改,因此将默认字段设置为相同的值不会将其写入文件。中显式命名的字段 writeBackFields 或 sync(..., { fields }) 即使在模式提供它们时也会编写。
resetToDefault 恢复字段的模式默认值。与 writeFilledValues: 'primaryKey',该字段将从文件中删除。
unwrap(db.update('people', {}, { id: 1 }, { resetToDefault: ['age'] }));
findWithDefaults() 返回模式提供的每一行和字段的名称。使用它来区分默认值和存储在文件中的值。
unwrap(await db.findWithDefaults('people'));
// [{ row: { id: 1, name: 'Alicia', age: 20 }, defaulted: ['age'] }]
行,其 backward 转换重命名字段或SQL添加的没有原始行或写记录的行,因为不能标识显式提供的字段,所以会完整地写入。通过SQL修改的表也会回写所有行。迁移设置的字段 transform 明确提供的负载计数。
原始行的未知字段在回写时保留其位置。去掉未知键的模式,比如Valibot的模式 v.object(),允许这些行通过验证。使用 v.strictObject() 或者另一个拒绝未知键并将其报告为错误的模式。
回写选定字段
当指定字段时,即使模式提供了它们,也会写入它们的数据库值。其他值仍然存储在文件中。
// users.jsonl: {"name":"Alice"}
unwrap(await db.sync('users', { fields: ['id'] }));
// users.jsonl: {"id":"...","name":"Alice"}
集 writeBackFields 将规则应用于每个同步,包括插入、更新和事务之后的自动同步。
const db = LinesDB.create({ dataDir: './data', writeBackFields: ['id'] });
行按主键匹配,如果文件还没有主键,则按位置匹配。保留行顺序,并追加新行。所选字段使用数据库值,包括null。如果行不能匹配, sync() 返回一个失败的结果。
新字段按模式顺序插入。例如, id 首先声明的在稍后填充时插入到行开头。现有的键不会移动,而未知的键会留在声明的键之后。
键顺序是从模式生成的行中读取的。首先生成的字段被优先排序;在后面的行中首先遇到的字段紧随其后。集 mergeFields的 keyOrder 显式指定顺序。
一个 sync() 对于特定表,拒绝表中没有的字段。跨所有表和配置同步 writeBackFields 跳过没有这些字段的表,因此相同的列表适用于不同的列集。
编辑没有数据库的行
mergeFields 获取原始行和计算行,并返回仅更新了所选字段的行。使用它来编辑文件而不将它们加载到SQLite中,例如当钩子提供id时。
import { JsonlReader, JsonlWriter, mergeFields, unwrap } from 'lines-db';
const rows = unwrap(await JsonlReader.read('./data/users.jsonl'));
const filled = rows.map((row) => mergeFields(row, fillIds(row), { fields: ['id'] }));
await JsonlWriter.write('./data/users.jsonl', filled);
// {"name":"Alice"} -> {"id":"018f...","name":"Alice"}
选择之外的字段不被验证。即使它们的值在模式中失败,所选字段仍然可以更新。计算行中缺少的字段保留其原始值。如果计算的键顺序与模式顺序不同,则使用 keyOrder 设置新键的位置。
配置
interface DatabaseConfig {
dataDir: string | readonly string[];
schemaDir?: string;
writeDataSets?: boolean;
writeBackFields?: readonly string[];
}
const db = LinesDB.create({ dataDir: './data' });
数据集
从特定于场景的数据中拆分共享的种子数据,并一起加载所需的目录。
data/
Item.schema.ts Item.jsonl # schemas and base data
ItemValuation.schema.ts
cogs/
Item.jsonl # additional Item rows
ItemValuation.jsonl # references Item rows from both sets
const db = LinesDB.create({ dataDir: ['./data', './data/cogs'], schemaDir: './data' });
const result = unwrap(await db.initialize({ detailedValidate: true }));
将同一表的行连接在 dataDir 秩序。在所有行中检查验证、唯一约束和外键。 cogs/ItemValuation.jsonl 可以从 data/Item.jsonl. 跨集合的重复id会导致约束冲突。错误标识源文件及其中的行。
多个目录需要 schemaDir 查找共享模式。它也可以与一个目录一起使用。如果省略单个目录,模式将在JSONL文件旁边查找。
默认情况下,具有多个目录的数据库是只读的。集 writeDataSets: true 将更新和删除写回其源文件。插入需要显式配置的目录。
const db = LinesDB.create({ dataDir: ['./data', './data/cogs'], schemaDir: './data', writeDataSets: true });
unwrap(await db.initialize());
unwrap(
await db.transaction((tx) => {
const inserted = tx.insert('Item', { id: 'new-item', name: 'New item' }, { dataDir: './data/cogs' });
if (!inserted.ok) throw inserted.error;
}),
);
使用 insert() 具有跨多个目录添加行的目标。 batchInsert() 没有目的地参数。SQL通过 execute(), query(), queryOne(), getDb() 保持只读以保留每行的源位置。
事务在写入前检查目标文件,如果稍后的写入失败,则恢复已写入的文件。工作室使 writeDataSets 自动。
类型映射
| JSON类型 | 列类型 | SQLite存储 |
|---|---|---|
| 数字(整数) | 整数 | 整数 |
| 号(浮动) | 真正的 | 真正的 |
| 字符串 | 文本 | 文本 |
| 布尔 | 整数 | 整数 |
| 对象 | JSON | 文本 |
| 数组 | JSON | 文本 |
许可证
麻省理工学院