tauri-plugin-libsql 0.1.0

Tauri plugin for libsql with encryption and drizzle ORM support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# Tauri libsql 插件

一个用于 [libsql](https://github.com/tursodatabase/libsql) 的 Tauri 插件,内置 AES-256-CBC 加密、Drizzle ORM 支持,以及浏览器兼容的迁移运行器。

## 为什么选择这个插件?

### 1. Rust ORM 在应用开发中很痛苦

在 Rust 中使用原始 SQL 很冗长,而 Rust ORM(Diesel、SeaORM)需要在 Rust 中定义模式,与 TypeScript 前端配合不佳,并且增加了显著的构建复杂性。对于真正的业务逻辑在 TypeScript 中的 Tauri 应用,你也希望在 TypeScript 中编写数据库代码。

### 2. 无需 Node.js 运行时的 Drizzle ORM

Drizzle ORM 非常出色 —— 类型安全的查询、简洁的迁移系统、出色的开发体验。但它通常需要 Node.js 或 Bun 运行时来直接打开数据库文件。Tauri 的 WebView 没有这样的运行时。

这个插件通过 Drizzle 的 [sqlite-proxy](https://orm.drizzle.team/docs/get-started-sqlite#http-proxy) 模式解决了这个问题:Drizzle 生成 SQL,代理通过 Tauri 的 `invoke()` 将其发送到 Rust 插件,Rust 插件使用 libsql 执行它。你的 TypeScript 代码使用完整的 Drizzle ORM,零 Node.js 依赖。

### 3. 在 WebView 中工作的迁移

Drizzle 内置的迁移器使用 Node 的 `fs` 模块在运行时从磁盘读取 `.sql` 文件 —— 这在浏览器/WebView 环境中不存在。有两种解决方法:

- **Tauri 资源文件夹** —— 将文件打包为应用资源,通过 Tauri 的 asset 协议读取。可以工作,但需要额外的 Tauri 配置。
- **Vite `import.meta.glob`** *(这个插件的方法)* —— Vite 在构建时将 SQL 文件内容直接打包到 JavaScript 中。无需运行时文件系统访问,无需额外配置。

```typescript
// Vite 在构建时解析这些 —— SQL 文本被内联到 JS 包中
const migrations = import.meta.glob<string>("./drizzle/*.sql", {
  eager: true,
  query: "?raw",
  import: "default",
});

await migrate("sqlite:myapp.db", migrations);
```

这个插件中的 `migrate()` 函数接收预加载的 SQL 字符串,在 `__drizzle_migrations` 表中跟踪已应用的迁移,并按顺序运行待处理的迁移。

### 4. 内置加密

`@tauri-apps/plugin-sql`(使用 sqlx)不支持加密。这个插件使用 libsql 的原生 AES-256-CBC 加密,无需额外的原生库或 FFI 包装器。

---

## 功能特性

- **完整的 SQLite 兼容性** 通过 libsql
- **原生加密** —— AES-256-CBC,可在插件级别或每个数据库配置
- **Drizzle ORM 集成** —— sqlite-proxy 模式与 `createDrizzleProxy`
- **迁移运行器** —— 浏览器安全的 `migrate()`,通过 Vite 在构建时打包 SQL 文件
- **API 兼容** 适用于 `@tauri-apps/plugin-sql`(在适用的地方)
- **跨平台**:macOS、Windows、Linux、iOS、Android
    **已测试**
    - [x] MacOS
    - [ ] Windows
    - [ ] Linux
    - [ ] iOS
    - [ ] Android

---

## 安装

### Rust

```toml
[dependencies]
tauri-plugin-libsql = "0.1.0"
```

### JavaScript / TypeScript

```bash
npm install tauri-plugin-libsql-api
# 
pnpm add tauri-plugin-libsql-api
```

---

## 快速开始

### 1. 注册插件(Rust)

```rust
// src-tauri/src/lib.rs

// 默认:数据库相对于当前工作目录解析
tauri::Builder::default()
    .plugin(tauri_plugin_libsql::init())
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
```

要在固定位置存储数据库:

```rust
use std::path::PathBuf;

let config = tauri_plugin_libsql::Config {
    base_path: Some(PathBuf::from("/path/to/data")),
    encryption: None,
};

tauri::Builder::default()
    .plugin(tauri_plugin_libsql::init_with_config(config))
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
```

### 2. 使用 Database 类(TypeScript)

```typescript
import { Database } from 'tauri-plugin-libsql-api';

const db = await Database.load('sqlite:myapp.db');

await db.execute(
  'INSERT INTO users (name) VALUES ($1)',
  ['Alice']
);

const users = await db.select<{ id: number; name: string }[]>(
  'SELECT * FROM users'
);

await db.close();
```

---

## 数据库位置

相对路径(例如 `sqlite:myapp.db`)相对于 `base_path` 解析:

- **默认**`std::env::current_dir()` —— 你启动 Tauri 进程的目录
- **自定义**:在插件配置中设置 `base_path`(见上文)
- **绝对路径** 按原样使用
- **内存中**`sqlite::memory:`

相对路径被规范化(`..` 组件被折叠)并且必须保持在 `base_path` 内。会逸出的路径(例如 `sqlite:../../secret`)将被拒绝并返回错误。

---

## Drizzle ORM 集成

### 设置

```typescript
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { createDrizzleProxy } from 'tauri-plugin-libsql-api';
import * as schema from './schema';

const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });

const users = await db.select().from(schema.users);
```

`createDrizzleProxy` 在首次使用时延迟加载数据库连接,因此使用它时无需单独调用 `Database.load()`。

### 使用加密

```typescript
import { createDrizzleProxyWithEncryption } from 'tauri-plugin-libsql-api';

const db = drizzle(
  createDrizzleProxyWithEncryption({
    path: 'sqlite:encrypted.db',
    encryption: {
      cipher: 'aes256cbc',
      key: myKey32Bytes, // number[] | Uint8Array, 32 字节
    },
  }),
  { schema }
);
```

---

## 迁移

标准的 `drizzle-orm/sqlite-proxy/migrator` 在运行时从文件系统读取,这在 Tauri WebView 中无法工作。这个插件提供了一个 `migrate()` 函数,它接受由 Vite 的 `import.meta.glob` 预打包的 SQL 内容。

### 工作流程

**1. 定义你的模式** (`src/lib/schema.ts`):

```typescript
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
});
```

**2. 配置 drizzle-kit** (`drizzle.config.ts`):

```typescript
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'sqlite',
  schema: './src/lib/schema.ts',
  out: './drizzle',
});
```

**3. 生成迁移文件**:

```bash
npx drizzle-kit generate
# 创建 drizzle/0000_init.sql, drizzle/0001_add_column.sql, 等等
```

**4. 在启动时运行迁移**:

```typescript
import { Database, migrate } from 'tauri-plugin-libsql-api';

// Vite 在构建时将这些 SQL 文件打包到应用中
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
  eager: true,
  query: '?raw',
  import: 'default',
});

// 启动顺序:加载 → 迁移 → 查询
await Database.load('sqlite:myapp.db');
await migrate('sqlite:myapp.db', migrations);

// 现在可以安全地查询
const db = drizzle(createDrizzleProxy('sqlite:myapp.db'), { schema });
```

### `migrate()` 如何工作

- 如果不存在,创建 `__drizzle_migrations` 跟踪表
- 通过数字前缀解析迁移文件名(`0000_``0001_` 等)
- 仅按顺序应用待处理的迁移
- 通过文件名记录每个已应用的迁移

### 添加模式更改

```bash
# 1. 编辑 src/lib/schema.ts
# 2. 生成新迁移
npx drizzle-kit generate
# 3. 新迁移在下次应用启动时自动运行
```

### 选项

```typescript
await migrate('sqlite:myapp.db', migrations, {
  migrationsTable: '__my_migrations', // 默认:'__drizzle_migrations'
});
```

---

## 加密

### 插件级别加密(应用于所有数据库)

在 Rust 中配置一次 —— 前端从不处理密钥:

```rust
let config = tauri_plugin_libsql::Config {
    base_path: None,
    encryption: Some(tauri_plugin_libsql::EncryptionConfig {
        cipher: tauri_plugin_libsql::Cipher::Aes256Cbc,
        key: my_32_byte_key, // Vec<u8>, 正好 32 字节
    }),
};
```

### 每个数据库加密(从前端)

```typescript
const key = new Uint8Array(32);
crypto.getRandomValues(key);

const db = await Database.load({
  path: 'sqlite:secrets.db',
  encryption: {
    cipher: 'aes256cbc',
    key: Array.from(key), // number[] 或 Uint8Array
  },
});
```

**安全注意事项:**
- AES-256-CBC 需要正好 32 字节
- 将密钥存储在操作系统钥匙串或安全存储中 —— 丢失密钥 = 丢失数据
- 首选插件级别加密;它将密钥排除在 JavaScript 之外

---

## API 参考

### `Database.load(pathOrOptions)`

```typescript
// 简单用法
const db = await Database.load('sqlite:myapp.db');

// 使用加密
const db = await Database.load({
  path: 'sqlite:myapp.db',
  encryption: { cipher: 'aes256cbc', key: myKey },
});
```

### `db.execute(query, values?)`

```typescript
const result = await db.execute(
  'INSERT INTO todos (title) VALUES ($1)',
  ['Buy milk']
);
// result.rowsAffected, result.lastInsertId
```

### `db.select<T>(query, values?)`

```typescript
const rows = await db.select<{ id: number; title: string }[]>(
  'SELECT * FROM todos WHERE completed = $1',
  [0]
);
```

### `db.batch(queries)`

在单个事务中原子执行多个 SQL 语句。用于 DDL 或批量 DML。语句不能使用绑定参数(`$1` 占位符)—— 对参数化查询使用 `execute()`。

```typescript
await db.batch([
  'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
  'CREATE INDEX idx_users_name ON users(name)',
]);
```

### `db.sync()`

从 Turso 远程拉取最新更改到本地副本。对纯本地数据库无操作(无错误返回)。需要 `replication` 功能。

```typescript
await db.sync();
```

### `db.close()`

```typescript
await db.close();
```

### `migrate(dbPath, migrationFiles, options?)`

```typescript
import { migrate } from 'tauri-plugin-libsql-api';

const migrations = import.meta.glob<string>('./drizzle/*.sql', {
  eager: true,
  query: '?raw',
  import: 'default',
});

await migrate('sqlite:myapp.db', migrations);
```

### `createDrizzleProxy(dbPath)`

返回一个用于 `drizzle()` 的 sqlite-proxy 回调。延迟加载连接。

### `createDrizzleProxyWithEncryption(options)`

同上,但带加密配置。

### `getConfig()`

```typescript
import { getConfig } from 'tauri-plugin-libsql-api';

const { encrypted } = await getConfig();
```

---

## 权限

添加到你的 `tauri.conf.json`:

```json
{
  "plugins": {
    "libsql": {}
  }
}
```

或配置细粒度能力:

```json
{
  "identifier": "libsql:default",
  "permissions": [
    "libsql:allow-load",
    "libsql:allow-batch",
    "libsql:allow-execute",
    "libsql:allow-select",
    "libsql:allow-close"
  ]
}
```

---

## 与 @tauri-apps/plugin-sql 的比较

| 功能 | tauri-plugin-libsql | @tauri-apps/plugin-sql |
|---------|---------------------|------------------------|
| SQLite | ✅ libsql | ✅ sqlx |
| 加密 | ✅ 内置 AES-256-CBC ||
| Drizzle ORM |||
| 迁移运行器 | ✅ 浏览器安全 ||
| MySQL / PostgreSQL |||
| API 兼容性 | 部分 | 完整 |

---

## Turso / 远程数据库

该插件支持两种由 libsql 提供支持的远程连接模式。

### 嵌入式副本(推荐用于 Tauri)

本地 SQLite 文件与 Turso 云数据库保持同步。查询从本地文件读取(快速、离线可用),写入同步到远程。

**1. 在你的应用 `Cargo.toml` 中启用 `replication` 功能**:

```toml
tauri-plugin-libsql = { version = "0.1.0", features = ["replication"] }
```

**2. 使用 `syncUrl` 和 `authToken` 加载:**

```typescript
import { Database, migrate } from 'tauri-plugin-libsql-api';

const db = await Database.load({
  path: 'sqlite:local.db',           // 本地副本文件
  syncUrl: 'libsql://mydb-org.turso.io',
  authToken: 'your-turso-auth-token',
});

// 按需同步(例如在应用恢复 / 网络重连时)
await db.sync();
```

在 `Database.load()` 时,初始同步将最新数据从 Turso 拉取到本地文件。后续的 `sync()` 调用拉取增量更改。

**使用 Drizzle ORM:**

```typescript
const migrations = import.meta.glob<string>('./drizzle/*.sql', {
  eager: true, query: '?raw', import: 'default',
});

const db = await Database.load({
  path: 'sqlite:local.db',
  syncUrl: 'libsql://mydb-org.turso.io',
  authToken: import.meta.env.VITE_TURSO_AUTH_TOKEN,
});

await migrate(db.path, migrations);

const drizzleDb = drizzle(createDrizzleProxy(db.path), { schema });
```

---

### 纯远程

所有查询直接在 Turso 上执行 —— 没有本地文件。每个查询都需要网络。

**启用 `remote` 功能:**

```toml
tauri-plugin-libsql = { version = "0.1.0", features = ["remote"] }
```

```typescript
const db = await Database.load({
  path: 'libsql://mydb-org.turso.io',
  authToken: 'your-turso-auth-token',
});
```

对于大多数 Tauri 应用,**嵌入式副本是更好的选择** —— 它离线工作,读取速度明显更快。

> **关于 `batch()` 与嵌入式副本的注意事项**:在某些版本中,libsql 的 `execute_batch()` 不能正确地通过嵌入式副本层路由写入。该插件使用显式 `BEGIN`/`COMMIT` 事务内的单个 `execute()` 调用来避免这个问题。

> **关于 URL 验证的注意事项**:libsql 的构建器在内部对同步 URL 调用 `unwrap()`,格式错误的值(例如前导/尾随空格、错误的协议)可能导致 panic。该插件将其包装在 `catch_unwind` 中,因此错误的 URL 会作为适当的错误显示,而不是无限期挂起 IPC。

---

## 包大小

基于包含的 Todo List 演示应用(macOS、aarch64、release 构建):

| 格式 | 带加密 | 不带加密 |
|--------|----------------|--------------------|
| `.app`| 15 MB | 15 MB |
| `.dmg` 安装程序 | 6.0 MB | 5.9 MB |

禁用加密基本上节省不了什么 —— 与始终存在的 SQLite 原生库相比,AES 密码代码可以忽略不计。`encryption` 功能标志仍然存在,以避免编译加密相关代码,如果你想在编译时强制没有数据库可以被加密。

### 禁用加密

加密是默认功能。要选择退出,请禁用默认功能并仅选择你需要的:

**`Cargo.toml`**(在你的 Tauri 应用中):

```toml
tauri-plugin-libsql = { version = "0.1.0", default-features = false, features = ["core"] }
```

**可用功能:**

| 功能 | 默认 | 描述 |
|---------|---------|-------------|
| `core` || 本地 SQLite 数据库(始终需要)|
| `encryption` || 通过 libsql 的 AES-256-CBC 加密 |
| `replication` || libsql 复制支持(添加 TLS)|
| `remote` || 远程数据库支持(计划中,见下文)|

当 `encryption` 被禁用时,向 `Database.load()` 传递 `EncryptionConfig` 将在运行时返回错误。TypeScript API 表面保持不变 —— 无需重新构建你的 JS 代码。

---

## 使用 AI 集成此插件

仓库根目录包含一个 `SKILL.md` 文件。它包含有关插件架构、启动顺序、迁移工作流、加密模式和常见错误的结构化上下文 —— 专为 AI 编码助手(Claude Code、Cursor、Copilot 等)编写。

### 使用 Claude Code

将 `SKILL.md` 复制到你项目的 `.claude/skills/tauri-plugin-libsql/` 目录:

```bash
mkdir -p .claude/skills/tauri-plugin-libsql
cp /path/to/tauri-plugin-libsql/SKILL.md .claude/skills/tauri-plugin-libsql/
```

Claude Code 自动发现技能。复制后,你可以自然地提示:

> "使用 tauri-plugin-libsql 为我的 Tauri 应用添加一个 `notes` 表。包括模式、迁移和启动顺序。"

Claude 将应用正确的启动顺序,对迁移使用 `import.meta.glob`,并处理 drizzle 代理模式,无需额外指导。

### 使用其他 AI 工具

直接将 `SKILL.md` 的内容粘贴到你的系统提示或上下文窗口中,然后描述你想构建什么。该技能涵盖足够的上下文,让 AI 能在第一次尝试就生成正确、可工作的代码。

---

## 项目结构

```
tauri-plugin-libsql/
├── src/                    # Rust 插件
│   ├── lib.rs              # 插件初始化、命令注册
│   ├── commands.rs         # load、execute、select、close、ping
│   ├── wrapper.rs          # DbConnection 包装 libsql
│   ├── decode.rs           # libsql::Value → serde_json::Value
│   ├── models.rs           # Cipher、EncryptionConfig、QueryResult
│   ├── error.rs            # 错误类型
│   ├── desktop.rs          # 桌面配置 & base_path
│   └── mobile.rs           # 移动端存根
├── guest-js/               # TypeScript 源代码
│   ├── index.ts            # Database 类、getConfig、重新导出
│   ├── drizzle.ts          # createDrizzleProxy、createDrizzleProxyWithEncryption
│   └── migrate.ts          # migrate() —— 浏览器安全的迁移运行器
├── permissions/            # Tauri 权限文件
├── examples/todo-list/     # 演示:带 Drizzle + 迁移的 Todo 应用(15 MB .app / 6 MB .dmg)
├── SKILL.md                # 适用于 Claude Code 和其他助手的 AI 技能上下文
├── build.rs
├── Cargo.toml
└── package.json
```