@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +304 -320
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
- package/dist/engine-wasm/wasm/lix_engine.js +9 -13
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +103 -14
- package/dist/open-lix.js +3 -0
- package/dist/sqlite/index.js +99 -22
- package/dist-engine-src/README.md +18 -0
- package/dist-engine-src/src/backend/kv.rs +358 -0
- package/dist-engine-src/src/backend/mod.rs +12 -0
- package/dist-engine-src/src/backend/testing.rs +658 -0
- package/dist-engine-src/src/backend/types.rs +96 -0
- package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
- package/dist-engine-src/src/binary_cas/codec.rs +346 -0
- package/dist-engine-src/src/binary_cas/context.rs +139 -0
- package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
- package/dist-engine-src/src/binary_cas/mod.rs +11 -0
- package/dist-engine-src/src/binary_cas/types.rs +121 -0
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/context.rs +86 -0
- package/dist-engine-src/src/cel/error.rs +19 -0
- package/dist-engine-src/src/cel/mod.rs +8 -0
- package/dist-engine-src/src/cel/provider.rs +9 -0
- package/dist-engine-src/src/cel/runtime.rs +167 -0
- package/dist-engine-src/src/cel/value.rs +50 -0
- package/dist-engine-src/src/commit_graph/context.rs +901 -0
- package/dist-engine-src/src/commit_graph/mod.rs +11 -0
- package/dist-engine-src/src/commit_graph/types.rs +109 -0
- package/dist-engine-src/src/commit_graph/walker.rs +756 -0
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/error.rs +313 -0
- package/dist-engine-src/src/common/fingerprint.rs +3 -0
- package/dist-engine-src/src/common/fs_path.rs +1336 -0
- package/dist-engine-src/src/common/identity.rs +145 -0
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +40 -0
- package/dist-engine-src/src/common/mod.rs +23 -0
- package/dist-engine-src/src/common/types.rs +105 -0
- package/dist-engine-src/src/common/wire.rs +222 -0
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +225 -0
- package/dist-engine-src/src/entity_identity.rs +405 -0
- package/dist-engine-src/src/functions/context.rs +292 -0
- package/dist-engine-src/src/functions/deterministic.rs +113 -0
- package/dist-engine-src/src/functions/mod.rs +18 -0
- package/dist-engine-src/src/functions/provider.rs +130 -0
- package/dist-engine-src/src/functions/state.rs +336 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +558 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +423 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +12 -0
- package/dist-engine-src/src/json_store/store.rs +1109 -0
- package/dist-engine-src/src/json_store/types.rs +217 -0
- package/dist-engine-src/src/lib.rs +62 -0
- package/dist-engine-src/src/live_state/context.rs +2019 -0
- package/dist-engine-src/src/live_state/mod.rs +15 -0
- package/dist-engine-src/src/live_state/overlay.rs +75 -0
- package/dist-engine-src/src/live_state/reader.rs +23 -0
- package/dist-engine-src/src/live_state/types.rs +222 -0
- package/dist-engine-src/src/live_state/visibility.rs +223 -0
- package/dist-engine-src/src/plugin/archive.rs +438 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +619 -0
- package/dist-engine-src/src/plugin/manifest.rs +516 -0
- package/dist-engine-src/src/plugin/materializer.rs +477 -0
- package/dist-engine-src/src/plugin/mod.rs +33 -0
- package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +187 -0
- package/dist-engine-src/src/schema/definition.rs +742 -0
- package/dist-engine-src/src/schema/key.rs +138 -0
- package/dist-engine-src/src/schema/mod.rs +20 -0
- package/dist-engine-src/src/schema/seed.rs +14 -0
- package/dist-engine-src/src/schema/tests.rs +780 -0
- package/dist-engine-src/src/session/context.rs +364 -0
- package/dist-engine-src/src/session/create_version.rs +88 -0
- package/dist-engine-src/src/session/execute.rs +478 -0
- package/dist-engine-src/src/session/merge/analysis.rs +102 -0
- package/dist-engine-src/src/session/merge/apply.rs +23 -0
- package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
- package/dist-engine-src/src/session/merge/mod.rs +11 -0
- package/dist-engine-src/src/session/merge/stats.rs +65 -0
- package/dist-engine-src/src/session/merge/version.rs +427 -0
- package/dist-engine-src/src/session/mod.rs +27 -0
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +109 -0
- package/dist-engine-src/src/sql2/change_provider.rs +331 -0
- package/dist-engine-src/src/sql2/classify.rs +182 -0
- package/dist-engine-src/src/sql2/context.rs +311 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
- package/dist-engine-src/src/sql2/error.rs +216 -0
- package/dist-engine-src/src/sql2/execute.rs +3440 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
- package/dist-engine-src/src/sql2/history_projection.rs +56 -0
- package/dist-engine-src/src/sql2/history_provider.rs +412 -0
- package/dist-engine-src/src/sql2/history_route.rs +657 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
- package/dist-engine-src/src/sql2/mod.rs +46 -0
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +63 -0
- package/dist-engine-src/src/sql2/record_batch.rs +17 -0
- package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
- package/dist-engine-src/src/sql2/runtime.rs +60 -0
- package/dist-engine-src/src/sql2/session.rs +132 -0
- package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
- package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
- package/dist-engine-src/src/sql2/version_scope.rs +394 -0
- package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
- package/dist-engine-src/src/storage/context.rs +356 -0
- package/dist-engine-src/src/storage/mod.rs +14 -0
- package/dist-engine-src/src/storage/read_scope.rs +88 -0
- package/dist-engine-src/src/storage/types.rs +501 -0
- package/dist-engine-src/src/storage_bench.rs +4863 -0
- package/dist-engine-src/src/test_support.rs +228 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
- package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
- package/dist-engine-src/src/tracked_state/context.rs +1867 -0
- package/dist-engine-src/src/tracked_state/diff.rs +686 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +492 -0
- package/dist-engine-src/src/tracked_state/mod.rs +32 -0
- package/dist-engine-src/src/tracked_state/storage.rs +375 -0
- package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
- package/dist-engine-src/src/tracked_state/types.rs +231 -0
- package/dist-engine-src/src/transaction/commit.rs +1484 -0
- package/dist-engine-src/src/transaction/context.rs +1548 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
- package/dist-engine-src/src/transaction/mod.rs +13 -0
- package/dist-engine-src/src/transaction/normalization.rs +890 -0
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
- package/dist-engine-src/src/transaction/staging.rs +1731 -0
- package/dist-engine-src/src/transaction/types.rs +460 -0
- package/dist-engine-src/src/transaction/validation.rs +5830 -0
- package/dist-engine-src/src/untracked_state/codec.rs +307 -0
- package/dist-engine-src/src/untracked_state/context.rs +98 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
- package/dist-engine-src/src/untracked_state/mod.rs +15 -0
- package/dist-engine-src/src/untracked_state/storage.rs +396 -0
- package/dist-engine-src/src/untracked_state/types.rs +146 -0
- package/dist-engine-src/src/version/context.rs +40 -0
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +13 -0
- package/dist-engine-src/src/version/refs.rs +330 -0
- package/dist-engine-src/src/version/stage_rows.rs +67 -0
- package/dist-engine-src/src/version/types.rs +21 -0
- package/dist-engine-src/src/wasm/mod.rs +60 -0
- package/package.json +68 -64
package/SKILL.md
CHANGED
|
@@ -1,64 +1,97 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lix-js-sdk
|
|
3
|
-
description: Use this skill when building examples, demos, tests, or applications with @lix-js/sdk
|
|
3
|
+
description: Use this skill when building examples, demos, tests, or applications with @lix-js/sdk: opening a Lix, registering schemas, writing entities through generated SQL tables, creating named versions, merging, and querying change history.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Lix JS SDK Skill
|
|
7
7
|
|
|
8
8
|
## What Is Lix
|
|
9
9
|
|
|
10
|
-
Lix is an embeddable version control system
|
|
10
|
+
Lix is an embeddable version control system for structured application state. It gives apps named versions, merge, and an immutable SQL-queryable change journal without asking the app to build those systems from scratch.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Current `@lix-js/sdk` capabilities:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- Register JSON schemas as tracked entity tables.
|
|
15
|
+
- Read and write entities through generated SQL tables.
|
|
16
|
+
- Create named versions of state and write/read across versions.
|
|
17
|
+
- Merge one version into the active version.
|
|
18
|
+
- Query `lix_change` for history, audit, activity feeds, and undo-style features.
|
|
19
|
+
- Store files as bytes with `lix_file` and version them like other entities.
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
Product direction:
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
- Lix is designed to version files of any kind by parsing them into typed entities on write.
|
|
24
|
+
- Parser plugins that turn file contents into app entities are not shipped through the JS SDK yet. Do not promise this behavior in demos. Today, `lix_file` versions bytes, while app entities are modeled directly through registered schemas.
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
Every row in every registered schema is a tracked entity. Merge granularity is currently per-entity, not per-field: two versions editing different rows merge cleanly; two versions editing the same row conflict, even if the fields are disjoint. Model collaborative domains as many small entities, such as sections, blocks, paragraphs, message keys, or line items.
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
Use Lix vocabulary in user-facing copy. What Git calls a branch is called a **version** in Lix because that language makes sense to non-developers.
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
## When To Use This Skill
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
- **AI agent sandboxes.** Spawn an agent on its own version, let it mutate freely, the human reviews the diff and merges or discards. Cheap rollback, no shadow tables.
|
|
28
|
-
- **Real-time and local-first multiplayer.** Lix already journals every change with author, timestamp, and entity identity — that is exactly what a sync protocol needs. Each peer is a version; sync is merge. Conflicts surface as exceptions instead of silent last-writer-wins.
|
|
29
|
-
- **Scenario / what-if branching** in spreadsheets, budgets, OKR planners, pricing tables.
|
|
30
|
-
- **Translation / localization workflows.** Each translator works on a version of the message catalog; merges flow back to main.
|
|
31
|
-
- **Auditable records** (compliance, clinical, legal). `lix_change` is an immutable journal queryable as SQL — replaces a hand-rolled audit log.
|
|
32
|
+
Use this skill when you need to write or debug consumer code using `@lix-js/sdk`:
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
- Opening a persistent `.lix` file.
|
|
35
|
+
- Registering schemas.
|
|
36
|
+
- Writing and reading generated SQL entity tables.
|
|
37
|
+
- Reading `execute()` results.
|
|
38
|
+
- Creating, switching, previewing, and merging versions.
|
|
39
|
+
- Querying history through `lix_change`.
|
|
40
|
+
- Building app demos, examples, smoke tests, or product flows around the SDK.
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
Do not use this skill for raw SQLite access, private engine/wasm internals, SDK publishing, SDK build pipelines, or unreleased file-parser plugin behavior.
|
|
43
|
+
|
|
44
|
+
## Agent Quick Start
|
|
45
|
+
|
|
46
|
+
1. Install `@lix-js/sdk` and `better-sqlite3`.
|
|
47
|
+
2. Open with `createBetterSqlite3Backend({ path })`; do not open `.lix` with raw SQLite.
|
|
48
|
+
3. Register a schema with `x-lix-key`, `x-lix-primary-key`, and `additionalProperties: false`.
|
|
49
|
+
4. Write rows through the generated table named by `x-lix-key`.
|
|
50
|
+
5. Use `<schema>_by_version` plus `lixcol_version_id` for side-by-side version reads/writes.
|
|
51
|
+
6. Query `lix_change` for audit/history instead of hand-rolling audit tables.
|
|
52
|
+
7. Wrap `mergeVersion()` in `try/catch` whenever conflicts are possible.
|
|
53
|
+
|
|
54
|
+
## Core Rules
|
|
55
|
+
|
|
56
|
+
- Use the public `@lix-js/sdk` API only.
|
|
57
|
+
- Use `createBetterSqlite3Backend()` for persistent apps, demos, and tests.
|
|
58
|
+
- Use numbered SQL placeholders: `$1`, `$2`, `$3`; bare `?` is rejected.
|
|
59
|
+
- Use `lix_json($1)` when inserting JSON text into JSON-typed columns.
|
|
60
|
+
- Use scalar SQL functions `SELECT lix_uuid_v7()` and `SELECT lix_timestamp()` when consumer code needs Lix-generated UUID v7 ids or ISO timestamps. Do not call them as table functions with `SELECT * FROM ...`.
|
|
61
|
+
- Use stable, namespaced, lowercase schema keys like `acme_section`, not generic names like `task`.
|
|
62
|
+
- Always include `x-lix-primary-key` and `additionalProperties: false` on app schemas.
|
|
63
|
+
- Use version names from the user's vocabulary, such as `"Marketing edit"` or `"Q3 pricing draft"`.
|
|
64
|
+
- Model concurrent-edit domains as collections of small rows because merge is per-row today.
|
|
65
|
+
- Prefer `_by_version` tables for demos, sync, agent inspection, and side-by-side diffs.
|
|
66
|
+
- Close handles in scripts and tests with `await lix.close()`.
|
|
67
|
+
|
|
68
|
+
## Install And Open
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm i @lix-js/sdk better-sqlite3
|
|
72
|
+
```
|
|
36
73
|
|
|
37
74
|
```ts
|
|
38
75
|
import { openLix } from "@lix-js/sdk";
|
|
39
76
|
import { createBetterSqlite3Backend } from "@lix-js/sdk/sqlite";
|
|
40
77
|
|
|
41
78
|
const lix = await openLix({
|
|
42
|
-
backend: createBetterSqlite3Backend({ path: "/path/to/
|
|
79
|
+
backend: createBetterSqlite3Backend({ path: "/path/to/app.lix" }),
|
|
43
80
|
});
|
|
44
81
|
```
|
|
45
82
|
|
|
46
|
-
`better-sqlite3` is an optional peer dependency
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
npm i @lix-js/sdk better-sqlite3
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Use the version of this skill that ships with the installed `@lix-js/sdk` package; do not copy version-specific snippets from older releases.
|
|
83
|
+
`better-sqlite3` is an optional peer dependency. Install it in projects that import `@lix-js/sdk/sqlite`.
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
`openLix()` without a backend is in-memory and dies with the process. For anything that should persist, pass a real `.lix` path. Reopening the same path picks up existing state.
|
|
55
86
|
|
|
56
|
-
For tests and demos, use an isolated temp
|
|
87
|
+
For tests and demos, use an isolated temp directory per run:
|
|
57
88
|
|
|
58
89
|
```ts
|
|
59
90
|
import { mkdtempSync } from "node:fs";
|
|
60
91
|
import { tmpdir } from "node:os";
|
|
61
92
|
import path from "node:path";
|
|
93
|
+
import { openLix } from "@lix-js/sdk";
|
|
94
|
+
import { createBetterSqlite3Backend } from "@lix-js/sdk/sqlite";
|
|
62
95
|
|
|
63
96
|
const dir = mkdtempSync(path.join(tmpdir(), "lix-"));
|
|
64
97
|
const lix = await openLix({
|
|
@@ -66,36 +99,23 @@ const lix = await openLix({
|
|
|
66
99
|
});
|
|
67
100
|
```
|
|
68
101
|
|
|
69
|
-
|
|
102
|
+
Use the version of this skill that ships with the installed `@lix-js/sdk` package. If behavior is unclear, inspect the installed package before guessing. The npm package bundles matching engine source under `node_modules/@lix-js/sdk/dist-engine-src/`.
|
|
70
103
|
|
|
71
|
-
|
|
72
|
-
type Lix = {
|
|
73
|
-
execute(sql: string, params?: readonly unknown[]): Promise<ExecuteResult>;
|
|
74
|
-
activeVersionId(): Promise<string>;
|
|
75
|
-
createVersion(options: { id?: string; name: string }): Promise<{ versionId: string }>;
|
|
76
|
-
switchVersion(options: { versionId: string }): Promise<{ versionId: string }>;
|
|
77
|
-
mergeVersion(options: { sourceVersionId: string }): Promise<MergeVersionResult>;
|
|
78
|
-
close(): Promise<void>;
|
|
79
|
-
};
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Use the public `@lix-js/sdk` API only. Do not import from `engine-wasm`, do not call `initLix` / `createWasmSqliteBackend`, do not open SQLite directly against a `.lix` file.
|
|
104
|
+
Useful installed-package references:
|
|
83
105
|
|
|
84
|
-
|
|
106
|
+
- `dist-engine-src/src/sql2/entity_provider.rs` - registered schema SQL surfaces.
|
|
107
|
+
- `dist-engine-src/src/sql2/change_provider.rs` - `lix_change` projection.
|
|
108
|
+
- `dist-engine-src/src/sql2/version_provider.rs` - writable `lix_version` surface.
|
|
109
|
+
- `dist-engine-src/src/transaction/validation.rs` - primary-key, unique, foreign-key, and shape validation.
|
|
110
|
+
- `dist-engine-src/src/schema/definition.json` - Lix schema-definition meta-schema.
|
|
111
|
+
- `dist-engine-src/src/schema/builtin/` - built-in entity table shapes.
|
|
112
|
+
- `dist-engine-src/src/sql2/udfs/` - registered SQL functions.
|
|
85
113
|
|
|
86
|
-
-
|
|
87
|
-
- [packages/js-sdk/src/open-lix.test.ts](https://github.com/opral/lix/blob/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/js-sdk/src/open-lix.test.ts) — canonical end-to-end flow.
|
|
88
|
-
- [packages/js-sdk/src/sqlite/index.ts](https://github.com/opral/lix/blob/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/js-sdk/src/sqlite/index.ts) — `better-sqlite3` backend factory.
|
|
89
|
-
- [packages/engine/src/schema/builtin](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/schema/builtin) — built-in entity table shapes.
|
|
90
|
-
- [packages/engine/src/sql2/udfs](https://github.com/opral/lix/tree/561f92b5bc3fa68e48a863ed3a02129645a57011/packages/engine/src/sql2/udfs) — registered SQL functions.
|
|
114
|
+
Do not import from `@lix-js/sdk/engine-wasm`, do not call private wasm helpers, and do not open the `.lix` SQLite file directly.
|
|
91
115
|
|
|
92
|
-
##
|
|
116
|
+
## Minimal Entity Example
|
|
93
117
|
|
|
94
|
-
This is the
|
|
95
|
-
|
|
96
|
-
The schema models a brochure as a list of **section entities** (headline, body, disclaimer) — not one row with multiple fields. That matches Lix's per-row merge granularity: Marketing edits the headline section, Legal edits the disclaimer section, both merge cleanly because they touched different rows.
|
|
97
|
-
|
|
98
|
-
Note: every registered schema `X` automatically gets a sibling table `X_by_version` exposing a `lixcol_version_id` column. Use it for cross-version reads and writes — you almost never need `switchVersion` for demos or read paths.
|
|
118
|
+
This is the smallest useful consumer pattern: open, register a schema, write a row, read it back, and close.
|
|
99
119
|
|
|
100
120
|
```ts
|
|
101
121
|
import { mkdtempSync } from "node:fs";
|
|
@@ -108,376 +128,340 @@ const dir = mkdtempSync(path.join(tmpdir(), "lix-"));
|
|
|
108
128
|
const lix = await openLix({
|
|
109
129
|
backend: createBetterSqlite3Backend({ path: path.join(dir, "demo.lix") }),
|
|
110
130
|
});
|
|
111
|
-
const published = await lix.activeVersionId();
|
|
112
131
|
|
|
113
|
-
// 1. Register a section schema. A brochure is a *collection* of sections.
|
|
114
132
|
await lix.execute(
|
|
115
133
|
"INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
|
|
116
|
-
[JSON.stringify({
|
|
117
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
118
|
-
"x-lix-key": "acme_section",
|
|
119
|
-
"x-lix-version": "1",
|
|
120
|
-
"x-lix-primary-key": ["/id"],
|
|
121
|
-
type: "object",
|
|
122
|
-
required: ["id", "brochure_id", "kind", "text"],
|
|
123
|
-
properties: {
|
|
124
|
-
id: { type: "string" },
|
|
125
|
-
brochure_id: { type: "string" },
|
|
126
|
-
kind: { type: "string" }, // "headline" | "body" | "disclaimer"
|
|
127
|
-
text: { type: "string" },
|
|
128
|
-
},
|
|
129
|
-
additionalProperties: false,
|
|
130
|
-
})],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// 2. Seed three sections of the live brochure on Published.
|
|
134
|
-
await lix.execute(
|
|
135
|
-
`INSERT INTO acme_section (id, brochure_id, kind, text) VALUES
|
|
136
|
-
($1,$2,$3,$4),($5,$6,$7,$8),($9,$10,$11,$12)`,
|
|
137
134
|
[
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
137
|
+
"x-lix-key": "acme_note",
|
|
138
|
+
"x-lix-primary-key": ["/id"],
|
|
139
|
+
type: "object",
|
|
140
|
+
required: ["id", "title", "done"],
|
|
141
|
+
properties: {
|
|
142
|
+
id: { type: "string" },
|
|
143
|
+
title: { type: "string" },
|
|
144
|
+
done: { type: "boolean" },
|
|
145
|
+
},
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
}),
|
|
141
148
|
],
|
|
142
149
|
);
|
|
143
150
|
|
|
144
|
-
// 3. Create all three reviewer versions up front from the same Published base.
|
|
145
|
-
// (Forking later — after a merge — would change the merge base and hide the conflict.)
|
|
146
|
-
const marketing = await lix.createVersion({ name: "Marketing edit" });
|
|
147
|
-
const legal = await lix.createVersion({ name: "Legal review" });
|
|
148
|
-
const competing = await lix.createVersion({ name: "Competing headline" });
|
|
149
|
-
|
|
150
|
-
// Marketing rewrites the headline section.
|
|
151
|
-
await lix.execute(
|
|
152
|
-
`UPDATE acme_section_by_version
|
|
153
|
-
SET text = $1
|
|
154
|
-
WHERE id = $2 AND lixcol_version_id = $3`,
|
|
155
|
-
["The Acme X1 — built for your weekend", "s-headline", marketing.versionId],
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// Legal rewrites the disclaimer section. Different entity → won't conflict with Marketing.
|
|
159
151
|
await lix.execute(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
WHERE id = $2 AND lixcol_version_id = $3`,
|
|
163
|
-
["Specifications and pricing subject to change. See acme.com/legal.",
|
|
164
|
-
"s-disclaimer", legal.versionId],
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Competing rewrites the *same* headline section as Marketing. This will conflict on merge.
|
|
168
|
-
await lix.execute(
|
|
169
|
-
`UPDATE acme_section_by_version
|
|
170
|
-
SET text = $1
|
|
171
|
-
WHERE id = $2 AND lixcol_version_id = $3`,
|
|
172
|
-
["Acme X1: now with carbon fork", "s-headline", competing.versionId],
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// 4. THE AHA: one SELECT, four versions side by side.
|
|
176
|
-
const sideBySide = await lix.execute(
|
|
177
|
-
`SELECT v.name, s.kind, s.text
|
|
178
|
-
FROM acme_section_by_version s
|
|
179
|
-
JOIN lix_version v ON v.id = s.lixcol_version_id
|
|
180
|
-
WHERE s.brochure_id = $1
|
|
181
|
-
AND s.lixcol_version_id IN ($2, $3, $4, $5)
|
|
182
|
-
ORDER BY v.name, s.kind`,
|
|
183
|
-
["spring-2026", published, marketing.versionId, legal.versionId, competing.versionId],
|
|
152
|
+
"INSERT INTO acme_note (id, title, done) VALUES ($1, $2, $3)",
|
|
153
|
+
["n1", "Draft launch copy", false],
|
|
184
154
|
);
|
|
185
|
-
if (sideBySide.kind === "rows") {
|
|
186
|
-
console.log("Same brochure, four versions:");
|
|
187
|
-
let lastVersion = "";
|
|
188
|
-
for (const row of sideBySide.rows.rows) {
|
|
189
|
-
const v = row[0].asText()!;
|
|
190
|
-
if (v !== lastVersion) { console.log(` [${v}]`); lastVersion = v; }
|
|
191
|
-
console.log(` ${row[1].asText()!.padEnd(11)} → ${row[2].asText()}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 5. Merge Marketing and Legal into Published. Different entities → both succeed.
|
|
196
|
-
const m1 = await lix.mergeVersion({ sourceVersionId: marketing.versionId });
|
|
197
|
-
const m2 = await lix.mergeVersion({ sourceVersionId: legal.versionId });
|
|
198
|
-
console.log(`\nMarketing merge: ${m1.outcome} (+${m1.appliedChangeCount} changes)`);
|
|
199
|
-
console.log(`Legal merge: ${m2.outcome} (+${m2.appliedChangeCount} changes)`);
|
|
200
|
-
|
|
201
|
-
const finalState = await lix.execute(
|
|
202
|
-
`SELECT kind, text FROM acme_section
|
|
203
|
-
WHERE brochure_id = $1 ORDER BY kind`,
|
|
204
|
-
["spring-2026"],
|
|
205
|
-
);
|
|
206
|
-
if (finalState.kind === "rows") {
|
|
207
|
-
console.log("\nPublished, after both merges (Marketing's headline + Legal's disclaimer):");
|
|
208
|
-
for (const row of finalState.rows.rows) {
|
|
209
|
-
console.log(` ${row[0].asText()!.padEnd(11)} → ${row[1].asText()}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
155
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`SELECT created_at, entity_id, snapshot_content
|
|
217
|
-
FROM lix_change
|
|
218
|
-
WHERE schema_key = $1
|
|
219
|
-
ORDER BY created_at`,
|
|
220
|
-
["acme_section"],
|
|
156
|
+
const result = await lix.execute(
|
|
157
|
+
"SELECT title, done FROM acme_note WHERE id = $1",
|
|
158
|
+
["n1"],
|
|
221
159
|
);
|
|
222
|
-
if (history.kind === "rows") {
|
|
223
|
-
console.log(`\nAudit trail (${history.rows.rows.length} entries):`);
|
|
224
|
-
for (const row of history.rows.rows) {
|
|
225
|
-
const snap = row[2].asText();
|
|
226
|
-
const parsed = snap ? JSON.parse(snap) : null;
|
|
227
|
-
console.log(` ${row[0].asText()} ${row[1].asText()} ${JSON.stringify(parsed)}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
160
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
await lix.mergeVersion({ sourceVersionId: competing.versionId });
|
|
235
|
-
} catch (err) {
|
|
236
|
-
console.log(`\nConflict surfaced (expected): ${(err as Error).message}`);
|
|
237
|
-
}
|
|
161
|
+
const row = result.rows[0]!;
|
|
162
|
+
console.log(row.value("title").asText(), row.value("done").asBoolean());
|
|
238
163
|
|
|
239
164
|
await lix.close();
|
|
240
165
|
```
|
|
241
166
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- One SELECT shows the same brochure across four versions, each section with its own per-version text — versioning lives *in the query layer*.
|
|
245
|
-
- Two `mergeCommitted` outcomes with non-zero `appliedChangeCount`.
|
|
246
|
-
- A final Published brochure with **Marketing's headline AND Legal's disclaimer** — clean per-row merge.
|
|
247
|
-
- An audit trail straight out of `lix_change`, ordered by `created_at`.
|
|
248
|
-
- A caught conflict from the version that re-edited Marketing's section.
|
|
249
|
-
|
|
250
|
-
That output is the elevator pitch. Imitate this shape when building demos: model the domain as **collections of small entities** (sections, blocks, paragraphs, message keys, line items) so reviewers naturally edit different rows; create all versions up front from the same base; use `<schema>_by_version` for cross-version reads and writes (you almost never need `switchVersion`); name versions in the user's vocabulary (`"Marketing edit"`, `"Legal review"`, `"Editor's pass"`, `"Q3 pricing draft"` — never `"Draft"` or `"branch-1"`); always include a `SELECT FROM lix_change` to surface the audit trail; and a deliberate `try/catch` conflict path.
|
|
251
|
-
|
|
252
|
-
## Cross-Version Reads And Writes
|
|
253
|
-
|
|
254
|
-
Every registered schema `X` gets a sibling table `X_by_version` with a `lixcol_version_id` column. Use it whenever a query needs more than one version, or whenever you want to write to a non-active version without `switchVersion`.
|
|
167
|
+
## Reading Results
|
|
255
168
|
|
|
256
|
-
|
|
257
|
-
- **INSERTs** require `lixcol_version_id` — without it the engine errors with `INSERT into <key>_by_version requires lixcol_version_id`.
|
|
258
|
-
- **UPDATEs / DELETEs** must include `lixcol_version_id` in the WHERE clause; DELETEs without it error with `DELETE FROM <key>_by_version requires an explicit lixcol_version_id predicate`.
|
|
259
|
-
- The non-suffixed table (`acme_brochure`) is the **active-version view** — convenient for app code that always operates on the current version, but `_by_version` is the right tool for demos, sync, agent inspection, and side-by-side diffs.
|
|
169
|
+
`lix.execute()` returns one shape for every statement:
|
|
260
170
|
|
|
261
|
-
|
|
171
|
+
```ts
|
|
172
|
+
type ExecuteResult = {
|
|
173
|
+
columns: string[];
|
|
174
|
+
rows: Row[];
|
|
175
|
+
rowsAffected: number;
|
|
176
|
+
notices: LixNotice[];
|
|
177
|
+
};
|
|
178
|
+
```
|
|
262
179
|
|
|
263
|
-
|
|
180
|
+
There is no `result.kind`. `SELECT` fills `columns` and `rows`; `INSERT`, `UPDATE`, and `DELETE` usually return `rows: []` and set `rowsAffected`.
|
|
264
181
|
|
|
265
|
-
|
|
182
|
+
Each row is a `Row` object. Use `row.value("column")` or `row.valueAt(index)` to get a `Value`, then call typed accessors:
|
|
266
183
|
|
|
267
184
|
```ts
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Read it back.
|
|
275
|
-
const r = await lix.execute(
|
|
276
|
-
"SELECT path, data FROM lix_file WHERE id = $1",
|
|
277
|
-
["file-readme"],
|
|
278
|
-
);
|
|
279
|
-
if (r.kind === "rows") {
|
|
280
|
-
const [path, data] = r.rows.rows[0]!;
|
|
281
|
-
console.log(path.asText(), new TextDecoder().decode(data.asBlob()!));
|
|
185
|
+
const r = await lix.execute("SELECT id, title, done FROM acme_note");
|
|
186
|
+
for (const row of r.rows) {
|
|
187
|
+
const id = row.value("id").asText();
|
|
188
|
+
const title = row.value("title").asText();
|
|
189
|
+
const done = row.value("done").asBoolean();
|
|
282
190
|
}
|
|
283
191
|
```
|
|
284
192
|
|
|
285
|
-
|
|
193
|
+
| Method | Returns | Use for |
|
|
194
|
+
| ------------- | ------------------------- | ----------------------------------------- |
|
|
195
|
+
| `asText()` | `string \| undefined` | strings; note `asText`, not `asString` |
|
|
196
|
+
| `asBoolean()` | `boolean \| undefined` | booleans |
|
|
197
|
+
| `asInteger()` | `number \| undefined` | integer fields |
|
|
198
|
+
| `asReal()` | `number \| undefined` | decimal/real fields |
|
|
199
|
+
| `asJson()` | `JsonValue \| undefined` | objects and arrays |
|
|
200
|
+
| `asBlob()` | `Uint8Array \| undefined` | binary data |
|
|
286
201
|
|
|
287
|
-
|
|
288
|
-
|--------|------|------------|
|
|
289
|
-
| `id` | text | Stable identity for the file (you choose it). |
|
|
290
|
-
| `path` | text | Absolute path like `/docs/readme.md`. Parent directories are auto-created in `lix_directory`. |
|
|
291
|
-
| `data` | blob | File contents as bytes. |
|
|
292
|
-
| `hidden` | bool | UI hint; doesn't affect storage. |
|
|
293
|
-
| `lixcol_*` | various | Version metadata (`lixcol_version_id`, `lixcol_global`, `lixcol_untracked`, `lixcol_change_id`, `lixcol_commit_id`, ...). |
|
|
202
|
+
Accessors return `undefined` when the cell kind does not match. Branch on `value.kind` if a column can hold multiple types. Public kind strings are `"null"`, `"boolean"`, `"integer"`, `"real"`, `"text"`, `"json"`, and `"blob"`.
|
|
294
203
|
|
|
295
|
-
`
|
|
296
|
-
|
|
297
|
-
**Files-as-entities (upcoming).** A future plugin API will let a registered parser turn a file's contents into rows in your schema tables on write — so `messages.json` becomes per-key entities and two translators editing different keys merge cleanly. Not shipped through `@lix-js/sdk` yet; don't promise it in demos. Today, `lix_file` versions bytes only.
|
|
204
|
+
`Row` also has convenience methods when native JS values are enough: `get(name)`, `tryGet(name)`, `getAt(index)`, `toObject()`, and `toValueMap()`.
|
|
298
205
|
|
|
299
206
|
## Registering Schemas
|
|
300
207
|
|
|
301
|
-
|
|
208
|
+
Register app schemas by inserting JSON into `lix_registered_schema.value`:
|
|
302
209
|
|
|
303
|
-
|
|
210
|
+
```ts
|
|
211
|
+
await lix.execute(
|
|
212
|
+
"INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
|
|
213
|
+
[JSON.stringify(schema)],
|
|
214
|
+
);
|
|
215
|
+
```
|
|
304
216
|
|
|
305
|
-
|
|
306
|
-
- `["/owner/email"]` — nested property `owner.email`.
|
|
307
|
-
- `["/owner", "/slug"]` — composite key over two top-level fields.
|
|
217
|
+
Schema basics:
|
|
308
218
|
|
|
309
|
-
|
|
219
|
+
- `x-lix-key` becomes the generated SQL table name.
|
|
220
|
+
- Compatible schema amendments are keyed by `x-lix-key`.
|
|
221
|
+
- `x-lix-primary-key` tells Lix how to derive entity identity.
|
|
222
|
+
- Primary-key entries are JSON Pointers with a leading slash, such as `["/id"]` or `["/owner/email"]`.
|
|
223
|
+
- Use `additionalProperties: false` so accidental fields fail fast.
|
|
310
224
|
|
|
311
|
-
|
|
225
|
+
Without `x-lix-primary-key`, table-style INSERTs fail with an error like `requires lixcol_entity_id because the schema has no x-lix-primary-key`.
|
|
312
226
|
|
|
313
|
-
|
|
227
|
+
Uniqueness is not inferred from ordinary JSON Schema fields. If a non-primary-key field must be unique, declare it explicitly:
|
|
314
228
|
|
|
315
229
|
```ts
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
230
|
+
const companyDomainSchema = {
|
|
231
|
+
"x-lix-key": "crm_company_domain",
|
|
232
|
+
"x-lix-primary-key": ["/id"],
|
|
233
|
+
"x-lix-unique": [["/domain"]],
|
|
234
|
+
type: "object",
|
|
235
|
+
required: ["id", "domain"],
|
|
236
|
+
properties: {
|
|
237
|
+
id: { type: "string" },
|
|
238
|
+
domain: { type: "string" },
|
|
239
|
+
},
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
};
|
|
319
242
|
```
|
|
320
243
|
|
|
321
|
-
|
|
244
|
+
Do not add generic `created_at` or `updated_at` fields by default. Lix already records lifecycle history through `lix_change` and `lixcol_*` metadata. Add timestamp fields only when they are domain data, such as `due_at`, `published_at`, or `occurred_at`.
|
|
322
245
|
|
|
323
|
-
|
|
246
|
+
Discover live schemas before guessing:
|
|
324
247
|
|
|
325
248
|
```ts
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const price = row[2].asReal(); // number | undefined
|
|
334
|
-
}
|
|
249
|
+
const schemas = await lix.execute(
|
|
250
|
+
"SELECT lixcol_entity_id, value FROM lix_registered_schema ORDER BY lixcol_entity_id",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
for (const row of schemas.rows) {
|
|
254
|
+
const schema = row.get("value") as { "x-lix-key"?: string };
|
|
255
|
+
console.log(schema["x-lix-key"]);
|
|
335
256
|
}
|
|
336
257
|
```
|
|
337
258
|
|
|
338
|
-
|
|
339
|
-
|--------|---------|---------|
|
|
340
|
-
| `asText()` | `string \| undefined` | `string` (note: `asText`, not `asString`) |
|
|
341
|
-
| `asBoolean()` | `boolean \| undefined` | `boolean` |
|
|
342
|
-
| `asInteger()` | `number \| undefined` | `integer` |
|
|
343
|
-
| `asReal()` | `number \| undefined` | `number` |
|
|
344
|
-
| `asJson()` | `JsonValue \| undefined` | `object` / `array` |
|
|
345
|
-
| `asBlob()` | `Uint8Array \| undefined` | binary |
|
|
346
|
-
| `kindValue()` | `"text" \| "bool" \| "int" \| "float" \| "json" \| "blob" \| "null"` | discriminator |
|
|
259
|
+
## Versions And `_by_version`
|
|
347
260
|
|
|
348
|
-
|
|
261
|
+
Capture the initial active version id instead of hardcoding `"main"`:
|
|
349
262
|
|
|
350
|
-
|
|
263
|
+
```ts
|
|
264
|
+
const published = await lix.activeVersionId();
|
|
265
|
+
```
|
|
351
266
|
|
|
352
|
-
|
|
267
|
+
Create versions with names from the user's domain:
|
|
353
268
|
|
|
354
269
|
```ts
|
|
355
|
-
const
|
|
270
|
+
const marketing = await lix.createVersion({ name: "Marketing edit" });
|
|
271
|
+
const legal = await lix.createVersion({ name: "Legal review" });
|
|
356
272
|
```
|
|
357
273
|
|
|
358
|
-
|
|
274
|
+
Every registered schema `X` gets a sibling table `X_by_version` with `lixcol_version_id`. Use it for side-by-side reads and for writes to non-active versions.
|
|
359
275
|
|
|
360
276
|
```ts
|
|
361
|
-
|
|
277
|
+
await lix.execute(
|
|
278
|
+
`UPDATE acme_note_by_version
|
|
279
|
+
SET title = $1
|
|
280
|
+
WHERE id = $2 AND lixcol_version_id = $3`,
|
|
281
|
+
["Sharper launch copy", "n1", marketing.id],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const sideBySide = await lix.execute(
|
|
285
|
+
`SELECT v.name, n.title
|
|
286
|
+
FROM acme_note_by_version n
|
|
287
|
+
JOIN lix_version v ON v.id = n.lixcol_version_id
|
|
288
|
+
WHERE n.id = $1
|
|
289
|
+
AND n.lixcol_version_id IN ($2, $3)
|
|
290
|
+
ORDER BY v.name`,
|
|
291
|
+
["n1", published, marketing.id],
|
|
292
|
+
);
|
|
362
293
|
```
|
|
363
294
|
|
|
364
|
-
|
|
295
|
+
Rules for `_by_version`:
|
|
296
|
+
|
|
297
|
+
- Reads filter by `lixcol_version_id`, or omit the filter to scan all versions.
|
|
298
|
+
- INSERTs require `lixcol_version_id`.
|
|
299
|
+
- UPDATEs and DELETEs must include `lixcol_version_id` in the WHERE clause.
|
|
300
|
+
- The non-suffixed table is the active-version view.
|
|
365
301
|
|
|
366
|
-
`switchVersion` is for app code
|
|
302
|
+
`switchVersion()` is for app code with a current working version concept. `mergeVersion()` always merges into the active version, so switch first if you need a different target.
|
|
367
303
|
|
|
368
304
|
## Merging
|
|
369
305
|
|
|
370
|
-
`mergeVersion()` merges the source version into the
|
|
306
|
+
`mergeVersion()` merges the source version into the currently active version:
|
|
371
307
|
|
|
372
308
|
```ts
|
|
373
|
-
|
|
309
|
+
try {
|
|
310
|
+
const merge = await lix.mergeVersion({ sourceVersionId: marketing.id });
|
|
311
|
+
console.log(merge.outcome, merge.changeStats.total);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error("Merge conflict", error);
|
|
314
|
+
}
|
|
374
315
|
```
|
|
375
316
|
|
|
376
|
-
|
|
317
|
+
Common outcomes:
|
|
377
318
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
targetHeadAfterCommitId: string;
|
|
388
|
-
createdMergeCommitId: string | null;
|
|
389
|
-
};
|
|
390
|
-
```
|
|
319
|
+
- `"alreadyUpToDate"` - source has no commits the target lacks.
|
|
320
|
+
- `"fastForward"` - target advanced to source without a merge commit.
|
|
321
|
+
- `"mergeCommitted"` - a new merge commit was created.
|
|
322
|
+
|
|
323
|
+
`mergeVersionPreview()` reports the same merge decision without advancing refs, staging changes, or creating commits. Merge conflicts are returned as preview data.
|
|
324
|
+
|
|
325
|
+
Conflicts throw from `mergeVersion()`. If both versions modified the same entity since their merge base, Lix raises a `LixError`. Conflict detection is row-level today, not field-level. To reproduce a conflict in a demo, fork all contending versions from the same base before merging any of them.
|
|
326
|
+
|
|
327
|
+
## Demo Pattern To Imitate
|
|
391
328
|
|
|
392
|
-
|
|
393
|
-
- `outcome: "mergeCommitted"` — a new merge commit was created; `appliedChangeCount > 0` and `createdMergeCommitId` is set.
|
|
329
|
+
For richer demos, show these four things:
|
|
394
330
|
|
|
395
|
-
|
|
331
|
+
1. Isolation: one SELECT against `<schema>_by_version` shows several versions side by side.
|
|
332
|
+
2. Clean parallel merges: two reviewers edit different entities and both land.
|
|
333
|
+
3. Audit history: `lix_change` is queryable SQL.
|
|
334
|
+
4. Conflict handling: two versions edit the same entity and `mergeVersion()` throws.
|
|
396
335
|
|
|
397
|
-
|
|
336
|
+
Shape the domain as a collection of small entities:
|
|
398
337
|
|
|
399
|
-
|
|
338
|
+
- Good: brochure sections, document blocks, paragraph rows, message keys, line items.
|
|
339
|
+
- Risky: one huge document row with many editable fields.
|
|
340
|
+
|
|
341
|
+
Demo recipe:
|
|
342
|
+
|
|
343
|
+
1. Register a schema such as `acme_section`.
|
|
344
|
+
2. Seed several rows in the published version.
|
|
345
|
+
3. Create all reviewer versions up front from the same base.
|
|
346
|
+
4. Write each reviewer's changes through `acme_section_by_version`.
|
|
347
|
+
5. Read side by side by joining `acme_section_by_version` to `lix_version`.
|
|
348
|
+
6. Merge non-overlapping row edits successfully.
|
|
349
|
+
7. Query `lix_change`.
|
|
350
|
+
8. Catch the deliberate same-row conflict.
|
|
351
|
+
|
|
352
|
+
## Files With `lix_file`
|
|
353
|
+
|
|
354
|
+
`lix_file` stores files as versioned bytes. Parent directories are created automatically.
|
|
400
355
|
|
|
401
356
|
```ts
|
|
402
|
-
await lix.execute("
|
|
403
|
-
|
|
357
|
+
await lix.execute("INSERT INTO lix_file (id, path, data) VALUES ($1, $2, $3)", [
|
|
358
|
+
"file-readme",
|
|
359
|
+
"/docs/readme.md",
|
|
360
|
+
new TextEncoder().encode("# Hello\n"),
|
|
361
|
+
]);
|
|
404
362
|
|
|
405
|
-
|
|
363
|
+
const result = await lix.execute(
|
|
364
|
+
"SELECT path, data FROM lix_file WHERE id = $1",
|
|
365
|
+
["file-readme"],
|
|
366
|
+
);
|
|
406
367
|
|
|
407
|
-
|
|
368
|
+
const file = result.rows[0]!;
|
|
369
|
+
console.log(
|
|
370
|
+
file.value("path").asText(),
|
|
371
|
+
new TextDecoder().decode(file.value("data").asBlob()!),
|
|
372
|
+
);
|
|
373
|
+
```
|
|
408
374
|
|
|
409
|
-
|
|
375
|
+
Columns consumers usually need:
|
|
410
376
|
|
|
411
|
-
|
|
|
412
|
-
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
377
|
+
| Column | What it is |
|
|
378
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
379
|
+
| `id` | Stable identity for the file. |
|
|
380
|
+
| `path` | Absolute path like `/docs/readme.md`. |
|
|
381
|
+
| `data` | File contents as bytes. |
|
|
382
|
+
| `hidden` | UI hint; does not affect storage. |
|
|
383
|
+
| `lixcol_*` | Version/change metadata, including `lixcol_version_id` where exposed. |
|
|
417
384
|
|
|
418
|
-
|
|
385
|
+
`lix_file_by_version` exists for cross-version file reads and writes. Files-as-parsed-entities are product direction, not current JS SDK behavior.
|
|
419
386
|
|
|
420
|
-
## The Change Journal
|
|
387
|
+
## The Change Journal
|
|
421
388
|
|
|
422
|
-
`lix_change` is
|
|
389
|
+
`lix_change` is an immutable SQL table of changes across registered schemas and versions. Use it for audit logs, blame, history, activity feeds, and undo-style UI.
|
|
423
390
|
|
|
424
|
-
|
|
391
|
+
Important columns include `id`, `entity_id`, `schema_key`, `snapshot_content`, `created_at`, and `lixcol_*` metadata.
|
|
425
392
|
|
|
426
393
|
```ts
|
|
427
|
-
// Audit log
|
|
394
|
+
// Audit log for one entity, oldest to newest.
|
|
428
395
|
await lix.execute(
|
|
429
396
|
`SELECT created_at, snapshot_content
|
|
430
397
|
FROM lix_change
|
|
431
398
|
WHERE schema_key = $1 AND entity_id = $2
|
|
432
399
|
ORDER BY created_at`,
|
|
433
|
-
["
|
|
400
|
+
["acme_note", "n1"],
|
|
434
401
|
);
|
|
435
402
|
|
|
436
|
-
//
|
|
437
|
-
await lix.execute(
|
|
438
|
-
`SELECT created_at, snapshot_content
|
|
439
|
-
FROM lix_change
|
|
440
|
-
WHERE schema_key = $1 AND entity_id = $2
|
|
441
|
-
ORDER BY created_at DESC
|
|
442
|
-
LIMIT 1`,
|
|
443
|
-
["acme_section", "s-headline"],
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
// Activity feed: latest 20 changes across the whole schema.
|
|
403
|
+
// Latest activity across a schema.
|
|
447
404
|
await lix.execute(
|
|
448
405
|
`SELECT created_at, entity_id, snapshot_content
|
|
449
406
|
FROM lix_change
|
|
450
407
|
WHERE schema_key = $1
|
|
451
408
|
ORDER BY created_at DESC
|
|
452
409
|
LIMIT 20`,
|
|
453
|
-
["
|
|
410
|
+
["acme_note"],
|
|
454
411
|
);
|
|
455
412
|
```
|
|
456
413
|
|
|
457
|
-
|
|
414
|
+
`snapshot_content` can be null or absent for tombstones, removals, or rows where content was not materialized. In the JS SDK, read it with `row.value("snapshot_content").asJson()` or `row.get("snapshot_content")`, then handle null. Do not blindly `JSON.parse` it as text.
|
|
415
|
+
|
|
416
|
+
## Built-In Tables And UDFs
|
|
458
417
|
|
|
459
|
-
|
|
418
|
+
Common tables:
|
|
460
419
|
|
|
461
|
-
|
|
420
|
+
| Table | What it gives consumers |
|
|
421
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
422
|
+
| `lix_version` | Writable version surface: `id`, `name`, `hidden`, `commit_id`. |
|
|
423
|
+
| `lix_change` | Immutable change journal. |
|
|
424
|
+
| `lix_file` | Versioned byte storage for files. |
|
|
425
|
+
| `lix_registered_schema` | Registry of app schemas plus built-ins; also exposes the Lix schema-definition meta-schema at runtime. |
|
|
462
426
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
427
|
+
`lix_version` can be updated for admin flows:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
await lix.execute("UPDATE lix_version SET hidden = true WHERE id = $1", [
|
|
431
|
+
marketing.id,
|
|
432
|
+
]);
|
|
433
|
+
```
|
|
469
434
|
|
|
470
|
-
|
|
435
|
+
There is no documented `deleteVersion()` helper in this preview. If the product wants reversible cleanup, hide the version. If it wants removal, `DELETE FROM lix_version WHERE id = $1` is the SQL surface; the engine rejects deleting the global version and active version.
|
|
471
436
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
437
|
+
Use `lix_json($1)` to parse JSON text parameters when writing JSON-typed columns:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
await lix.execute(
|
|
441
|
+
"INSERT INTO lix_registered_schema (value) VALUES (lix_json($1))",
|
|
442
|
+
[JSON.stringify(schema)],
|
|
443
|
+
);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Other UDFs, such as `lix_json_get`, `lix_uuid_v7`, `lix_text_encode`, and `lix_empty_blob`, live in `dist-engine-src/src/sql2/udfs/` in the installed package.
|
|
447
|
+
|
|
448
|
+
## Do And Avoid
|
|
449
|
+
|
|
450
|
+
| Do | Avoid |
|
|
451
|
+
| --- | --- |
|
|
452
|
+
| Use `createBetterSqlite3Backend({ path })` for persistent state. | Opening `.lix` files with raw SQLite libraries. |
|
|
453
|
+
| Use public imports from `@lix-js/sdk` and `@lix-js/sdk/sqlite`. | Importing `engine-wasm` or private internals. |
|
|
454
|
+
| Use `$1`, `$2`, `$3` placeholders. | Bare `?` placeholders. |
|
|
455
|
+
| Use `lix_json($1)` for JSON parameters. | Inlining stringified JSON directly into SQL. |
|
|
456
|
+
| Use `_by_version` for cross-version reads/writes. | Switching versions just to render a side-by-side view. |
|
|
457
|
+
| Name versions in user vocabulary. | User-facing words like branch, branch-1, or generic Draft. |
|
|
458
|
+
| Model collaborative data as small rows. | One giant row when multiple reviewers edit different parts. |
|
|
459
|
+
| Add `x-lix-unique` for non-primary unique fields. | Assuming JSON Schema property metadata creates uniqueness. |
|
|
460
|
+
| Read `snapshot_content` as JSON/native and handle null. | Blindly `JSON.parse(row.value(...).asText())`. |
|
|
461
|
+
| Wrap `mergeVersion()` in `try/catch`. | Assuming merges cannot conflict. |
|
|
480
462
|
|
|
481
463
|
## Reporting SDK Friction
|
|
482
464
|
|
|
483
|
-
If you encounter an SDK bug, missing API, confusing error, documentation gap, or large implementation friction while using this skill, pause and ask the user whether they want you to open a GitHub issue via the `gh` CLI installed on their computer. Do not file
|
|
465
|
+
If you encounter an SDK bug, missing API, confusing error, documentation gap, or large implementation friction while using this skill, pause and ask the user whether they want you to open a GitHub issue via the `gh` CLI installed on their computer. Do not file an issue without confirmation.
|
|
466
|
+
|
|
467
|
+
Before filing, scan existing issues to avoid duplicates. If the user approves a report, include a minimal reproduction, expected behavior, actual behavior, the installed `@lix-js/sdk` version, runtime details, and relevant error output. Do not include private data, customer content, credentials, tokens, local paths, database contents, or proprietary schemas.
|