Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions packages/db-tauri-sqlite-persisted-collection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# @tanstack/db-tauri-sqlite-persisted-collection

Thin SQLite persistence for Tauri apps using `@tauri-apps/plugin-sql`.

## Public API

- `createTauriSQLitePersistence(...)`
- `persistedCollectionOptions(...)` (re-exported from core)

## Install

```bash
pnpm add @tanstack/db-tauri-sqlite-persisted-collection @tauri-apps/plugin-sql
```

## Consumer-side Tauri setup

Install the official SQL plugin in your Tauri app:

```bash
cd src-tauri
cargo add tauri-plugin-sql --features sqlite
```

Register the plugin in `src-tauri/src/main.rs`:

```rust
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```

Enable the SQL permissions in `src-tauri/capabilities/default.json`:

```json
{
"permissions": ["core:default", "sql:default", "sql:allow-execute"]
}
```

## Quick start

```ts
import Database from '@tauri-apps/plugin-sql'
import { createCollection } from '@tanstack/db'
import {
createTauriSQLitePersistence,
persistedCollectionOptions,
} from '@tanstack/db-tauri-sqlite-persisted-collection'

type Todo = {
id: string
title: string
completed: boolean
}

const database = await Database.load(`sqlite:tanstack-db.sqlite`)

const persistence = createTauriSQLitePersistence({
database,
})

export const todosCollection = createCollection(
persistedCollectionOptions<Todo, string>({
id: `todos`,
getKey: (todo) => todo.id,
persistence,
schemaVersion: 1,
}),
)
```

## Notes

- `createTauriSQLitePersistence` is shared across collections.
- Reuse a single `Database.load('sqlite:...')` handle per SQLite file when using
this package. Opening multiple plugin handles to the same file can reintroduce
SQLite locking behavior outside this package's serialized transaction queue.
- Mode defaults (`sync-present` vs `sync-absent`) are inferred from whether a
`sync` config is present in `persistedCollectionOptions`.
- This package expects a database handle created by
`@tauri-apps/plugin-sql`, typically from `Database.load('sqlite:...')`.
- The database path is resolved by Tauri's SQL plugin, not by this package.
- This package does not publish or require package-specific Rust code. Only the
app-level Tauri SQL plugin registration shown above is required.

## Testing

- `pnpm --filter @tanstack/db-tauri-sqlite-persisted-collection test`
runs the driver and shared adapter contract tests.
- `pnpm --filter @tanstack/db-tauri-sqlite-persisted-collection test:e2e`
builds the repo-local Tauri harness and runs the persisted collection
conformance suite inside a real Tauri runtime.
48 changes: 48 additions & 0 deletions packages/db-tauri-sqlite-persisted-collection/e2e/app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri SQLite E2E</title>
</head>
<body>
<main>
<p id="status">Booting Tauri persisted collection e2e runtime</p>
<pre id="details"></pre>
</main>
<script type="module">
const reportUrl = '%VITE_TANSTACK_DB_TAURI_E2E_REPORT_URL%'

if (reportUrl && !reportUrl.startsWith('%')) {
fetch(reportUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
kind: 'status',
phase: 'html-loaded',
}),
}).catch(() => {})
}

import('/src/main.ts').catch((error) => {
if (reportUrl && !reportUrl.startsWith('%')) {
fetch(reportUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
kind: 'status',
phase: 'main-import-failed',
details: {
message: String(error?.message ?? error),
},
}),
}).catch(() => {})
}
})
</script>
</body>
</html>
23 changes: 23 additions & 0 deletions packages/db-tauri-sqlite-persisted-collection/e2e/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@tanstack/db-tauri-sqlite-persisted-collection-e2e-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-sql": "^2.3.2",
"@tanstack/db": "workspace:*",
"@tanstack/db-tauri-sqlite-persisted-collection": "workspace:*"
},
"devDependencies": {
"@tauri-apps/cli": "^2.10.1",
"@types/node": "^25.2.2",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
/gen
/Cargo.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "db-tauri-sqlite-persisted-collection-e2e-app"
version = "0.0.0"
description = "Repo-local Tauri e2e harness for TanStack DB SQLite persistence"
authors = ["TanStack Team"]
edition = "2021"

[build-dependencies]
tauri-build = { version = "2.5.6", features = [] }

[dependencies]
serde_json = "1"
tauri = { version = "2.10.3", features = [] }
tauri-plugin-sql = { version = "2.3.2", features = ["sqlite"] }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"identifier": "default",
"description": "Default capability for the Tauri SQLite e2e harness",
"windows": ["main"],
"permissions": ["core:default", "sql:default", "sql:allow-execute"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running Tauri e2e application");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "TanStack DB Tauri E2E",
"version": "0.0.0",
"identifier": "com.tanstack.db.tauri.e2e",
"build": {
"beforeDevCommand": "pnpm dev --host 127.0.0.1 --port 1420",
"devUrl": "http://127.0.0.1:1420",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "TanStack DB Tauri E2E",
"width": 1280,
"height": 900
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": false,
"icon": []
}
}
139 changes: 139 additions & 0 deletions packages/db-tauri-sqlite-persisted-collection/e2e/app/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { createNativeTauriSQLiteTestDatabase } from './native-tauri-sql-test-db'
import { registerTauriNativeE2ESuite } from './register-tauri-e2e-suite'
import {
getRegisteredTestCount,
resetRegisteredTests,
runRegisteredTests,
} from './runtime-vitest'

const statusElement = document.querySelector(`#status`) as HTMLParagraphElement
const detailsElement = document.querySelector(`#details`) as HTMLPreElement
const runtimeRunId =
import.meta.env.VITE_TANSTACK_DB_TAURI_E2E_RUN_ID ?? Date.now().toString(36)
const reportUrl = import.meta.env.VITE_TANSTACK_DB_TAURI_E2E_REPORT_URL

function setStatus(status: string, details?: unknown): void {
statusElement.textContent = status
if (details !== undefined) {
detailsElement.textContent = JSON.stringify(details, null, 2)
}
}

async function postHarnessMessage(
payload: Record<string, unknown>,
): Promise<void> {
if (!reportUrl) {
return
}

await fetch(reportUrl, {
method: `POST`,
headers: {
'content-type': `application/json`,
},
body: JSON.stringify(payload),
})
}

async function reportRunResult(result: {
status: `passed` | `failed`
payload: unknown
}): Promise<void> {
await postHarnessMessage({
kind: `result`,
...result,
})
}

async function reportStatus(phase: string, details?: unknown): Promise<void> {
await postHarnessMessage({
kind: `status`,
phase,
details,
})
}

async function run(): Promise<void> {
setStatus(`Starting Tauri e2e runtime`)

try {
await reportStatus(`starting`, { runId: runtimeRunId })
const database = await createNativeTauriSQLiteTestDatabase({
runId: runtimeRunId,
})
await reportStatus(`database-loaded`)

resetRegisteredTests()
registerTauriNativeE2ESuite({
suiteName: `tauri persisted collection conformance`,
database,
runId: runtimeRunId,
})
await reportStatus(`suite-registered`)

const totalTests = getRegisteredTestCount()
setStatus(`Running Tauri e2e suite`, {
totalTests,
runId: runtimeRunId,
})
await reportStatus(`tests-starting`, {
totalTests,
})

const result = await runRegisteredTests({
onTestStart: ({ index, name, total }) => {
setStatus(`Running test ${String(index)}/${String(total)}`, {
currentTest: name,
})
},
})
await reportStatus(`tests-finished`, {
total: result.total,
failed: result.failed,
})

const failedResults = result.results.filter(
(entry) => entry.status === `failed`,
)
const summary = {
passed: result.passed,
failed: result.failed,
skipped: result.skipped,
total: result.total,
failures: failedResults.slice(0, 10),
}

if (result.failed > 0) {
await reportRunResult({
status: `failed`,
payload: summary,
})
setStatus(`Tauri e2e failed`, summary)
return
}

await reportRunResult({
status: `passed`,
payload: summary,
})
setStatus(`Tauri e2e passed`, summary)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const payload = {
error: message,
runId: runtimeRunId,
step: statusElement.textContent,
}

try {
await reportRunResult({
status: `failed`,
payload,
})
} catch {}

setStatus(`Tauri e2e failed: ${message}`, payload)
}
}

void run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Database from '@tauri-apps/plugin-sql'
import type { TauriSQLiteDatabaseLike } from '../../../src'

export async function createNativeTauriSQLiteTestDatabase(options: {
runId: string
}): Promise<TauriSQLiteDatabaseLike> {
return Database.load(`sqlite:tanstack_db_tauri_e2e_${options.runId}.db`)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function randomUUID(): string {
return globalThis.crypto.randomUUID()
}
Loading
Loading