Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
dcbacfe
implement cache vtable insert logic
samuelstroschein Aug 12, 2025
c943c83
implement selects
samuelstroschein Aug 12, 2025
53e193b
implement update cache
samuelstroschein Aug 12, 2025
e490069
implement mutations
samuelstroschein Aug 12, 2025
546a84b
simplify jsonb
samuelstroschein Aug 12, 2025
ab40bdd
reduce bench to 10k
samuelstroschein Aug 12, 2025
251d213
fix: scoped lix instances
samuelstroschein Aug 12, 2025
3345789
feat: implement populateStateCacheV2 function and associated tests
samuelstroschein Aug 12, 2025
ac0e217
90% done migration to v2 cache api
samuelstroschein Aug 13, 2025
912d764
replace v1 cache with v2 cache
samuelstroschein Aug 13, 2025
036b18d
move transaction state into own folder
samuelstroschein Aug 13, 2025
21ac7d3
move cache state logic out of transaction state
samuelstroschein Aug 14, 2025
a0eb65b
remove insert into transaction state during commit phase
samuelstroschein Aug 14, 2025
c75430c
refactor commit function to improve version context handling and simp…
samuelstroschein Aug 14, 2025
a3abe47
move files
samuelstroschein Aug 14, 2025
14a7013
add logic to delete untracked state for committed changes to ensure d…
samuelstroschein Aug 14, 2025
d67bb83
move change author logic to commit
samuelstroschein Aug 15, 2025
bb99146
fix: use static object
samuelstroschein Aug 15, 2025
1011783
fix: restore mocks before each test
samuelstroschein Aug 15, 2025
e3da246
refactor: rename updateStateCacheV2 to updateStateCache across tests …
samuelstroschein Aug 15, 2025
260446e
refactor: rename internal_state_cache_v2 to internal_state_cache acro…
samuelstroschein Aug 15, 2025
531eca8
remove copy down logic from commit
samuelstroschein Aug 15, 2025
ac4845d
refactor: remove debug logging from updateStateCache and commit funct…
samuelstroschein Aug 15, 2025
bbd7934
fix: change author is global
samuelstroschein Aug 15, 2025
747a372
fix: make create comment deterministic
samuelstroschein Aug 16, 2025
2d435ae
filter test
samuelstroschein Aug 16, 2025
95fe477
test: semantic comparison
samuelstroschein Aug 16, 2025
0eba5ba
better wording
samuelstroschein Aug 17, 2025
e1d9eb4
fix: handle transitive inheritance
samuelstroschein Aug 18, 2025
502e1b4
fix: populate inherited state
samuelstroschein Aug 18, 2025
5198048
fix: create changes for change set elements
samuelstroschein Aug 18, 2025
b3254c3
test: fix sorting
samuelstroschein Aug 18, 2025
1781708
fix: enhance tests for opening Lix and ensure transaction table is em…
samuelstroschein Aug 18, 2025
45e739e
fix: remove outdated special mappings for active version and account …
samuelstroschein Aug 18, 2025
d25244e
test: add test for overwriting primary key in a single transaction an…
samuelstroschein Aug 19, 2025
d4621a2
require minimum node 22
samuelstroschein Aug 19, 2025
98d8942
handle transaction state too
samuelstroschein Aug 19, 2025
0a66770
test: add tests for untracked and tracked inserts/deletes within the …
samuelstroschein Aug 19, 2025
1cd012c
fix: dont coerse strings to numbers
samuelstroschein Aug 19, 2025
ac1980b
test: add unit tests ensuring that numbers are preserved
samuelstroschein Aug 19, 2025
2c4a215
add `transition()` api as replacement for create transition commit
samuelstroschein Aug 19, 2025
3304410
test: add more transition tests
samuelstroschein Aug 19, 2025
1f5f054
improve: make `createCheckpoint()` idempotent
samuelstroschein Aug 19, 2025
86c38af
fix: handle state hook unsubscribes
samuelstroschein Aug 19, 2025
b23dec1
test: add benchmark tests for transition operations
samuelstroschein Aug 19, 2025
360f395
refactor createVersion api to use from for a better DX
samuelstroschein Aug 21, 2025
f6a256d
add select version diff api
samuelstroschein Aug 21, 2025
7ab2847
up bench to 10
samuelstroschein Aug 21, 2025
f0b4dda
move views into own folder
samuelstroschein Aug 21, 2025
4091d2b
refactor: move vtable logic into own file
samuelstroschein Aug 22, 2025
c776db9
expose tombstones in state vtable
samuelstroschein Aug 22, 2025
ae29bd0
Enhance version diff logic to handle tombstones and clarify diff stat…
samuelstroschein Aug 22, 2025
7fbc05b
Add StateWithTombstones types and integrate into schema
samuelstroschein Aug 22, 2025
5424711
feat: implement mergeVersion function for handling version merges in …
samuelstroschein Aug 23, 2025
8b514a9
remove unsued old merge code
samuelstroschein Aug 24, 2025
19caa51
move vtable code into folder
samuelstroschein Aug 24, 2025
d00cc0c
formatting
samuelstroschein Aug 24, 2025
689c3f7
comment out expensive bench
samuelstroschein Aug 24, 2025
dafe63a
feat: restore and expand LLM rules documentation in AGENTS.md
samuelstroschein Aug 24, 2025
b9acb74
move table creation in own file
samuelstroschein Aug 24, 2025
10bea8e
perf: ≈5% faster change inserts
samuelstroschein Aug 24, 2025
e929760
bench: add multi commit benchmark
samuelstroschein Aug 24, 2025
e917ed4
bench: add bootup time bench
samuelstroschein Aug 24, 2025
34fcd0a
chore: format
samuelstroschein Aug 24, 2025
2b4bce6
faster hot path for depth 0 queries
samuelstroschein Aug 24, 2025
cd12b9d
fix: update test timeout to 60 seconds to prevent CI/CD issues
samuelstroschein Aug 24, 2025
d02bd15
feat: add vitest configuration with increased test timeout
samuelstroschein Aug 24, 2025
8f89da1
fix: increase test timeout to 120 seconds to prevent CI/CD issues
samuelstroschein Aug 24, 2025
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"packageManager": "[email protected]",
"engines": {
"node": ">=20",
"node": ">=22",
"pnpm": ">=10 <11"
},
"devDependencies": {
Expand All @@ -23,4 +23,4 @@
"nx-cloud": "16.5.2",
"vitest": "^3.1.1"
}
}
}
2 changes: 1 addition & 1 deletion packages/csv-app/src/layouts/OpenFileLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ const VersionDropdown = () => {
onClick={async () => {
const newversion = await createVersion({
lix,
commit_id: currentVersion.commit_id,
from: currentVersion,
});
await switchToversion(newversion);
}}
Expand Down
101 changes: 101 additions & 0 deletions packages/lix-docs/docs/guide/concepts/key-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Key-Value

A simple key-value store built into Lix with change control. Store any JSON value - from feature flags to UI preferences - and access it across your application.

## Common Use Cases

- **UI state persistence** - Sidebar positions, dismissed prompts, user preferences
- **App configuration** - Feature flags, environment settings, runtime toggles
- **Lix configuration** - Lix itself stores key values like `lix_id`, `lix_name`, or `lix_deterministic_mode`

## Quick Start

Always namespace your keys to avoid collisions:

```ts
// ✅ DO: Use namespaced keys
await lix.db
.insertInto("key_value")
.values({ key: "myapp_sidebar_collapsed", value: true })
.execute();

// Read it back
const sidebar = await lix.db
.selectFrom("key_value")
.where("key", "=", "myapp_sidebar_collapsed")
.executeTakeFirst();
```

## Do's and Don'ts

### ✅ DO

- **Always use namespaces**: `myapp_feature`, `ui_sidebar`, `config_theme`
- **Store UI state as untracked**: Use `lixcol_untracked: 1` for ephemeral data

### ❌ DON'T

- **Never use bare keys**: Avoid `"theme"`, use `"myapp_theme"` instead

## Real-World Examples

### UI State Pattern

```ts
// Store dismissed prompts per file (from md-app)
const key = `flashtype_prompt_dismissed_${activeFileId}`;
await lix.db
.insertInto("key_value")
.values({ key, value: true, lixcol_untracked: 1 })
.execute();
```

### Untracked Preferences

```ts
// UI preferences that don't create commits
await lix.db
.insertInto("key_value_all")
.values({
key: "ui_sidebar_width",
value: 240,
lixcol_untracked: 1,
lixcol_version_id: "global",
})
.execute();
```

## Views

- **`key_value`** - Active version only. Your default choice.
- **`key_value_all`** - All versions. Use for untracked values or cross-version operations.
- **`key_value_history`** - Read-only audit trail.

## Important: Booleans

Booleans are returned as integers because SQLite's `json_extract` function (used by the views) converts JSON booleans to integers:

- `true` → `1`
- `false` → `0`

```ts
// Store boolean
await lix.db
.insertInto("key_value")
.values({ key: "foo_enabled", value: true })
.execute();

// Read and convert
const result = await lix.db
.selectFrom("key_value")
.where("key", "=", "foo_enabled")
.executeTakeFirstOrThrow();

// Option 1: Use loose equality (simplest)
if (result.value == true) {
/* enabled */
}

// Option 2: Explicit conversion
const isEnabled = result.value === 1;
```
1 change: 1 addition & 0 deletions packages/lix-docs/rspress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export default defineConfig({
},
{ text: "Versions", link: "/guide/concepts/versions" },
{ text: "Discussions", link: "/guide/concepts/discussions" },
{ text: "Key-Value", link: "/guide/concepts/key-value" },
],
},
{
Expand Down
8 changes: 8 additions & 0 deletions packages/lix-docs/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
// increased default timeout to avoid ci/cd issues
testTimeout: 60000,
},
});
8 changes: 4 additions & 4 deletions packages/lix-file-manager/src/components/MergeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
SelectValue,
} from "@/components/ui/select.js";
import { useState, useEffect } from "react";
import { createMergeCommit } from "@lix-js/sdk";
import { transition } from "@lix-js/sdk";

interface MergeDialogProps {
open: boolean;
Expand Down Expand Up @@ -62,10 +62,10 @@ export function MergeDialog({

if (!source || !target) return;

await createMergeCommit({
await transition({
lix,
source: { id: source.commit_id },
target: { id: target.commit_id },
to: { id: source.commit_id },
version: { id: target.id },
});

onMergeComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function VersionDropdown() {

const newVersion = await createVersion({
lix,
commit_id: activeVersion.commit_id,
from: activeVersion,
});

await switchToVersion(newVersion);
Expand Down
22 changes: 9 additions & 13 deletions packages/lix-file-manager/src/state-active-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import {
threadSearchParamsAtom,
} from "./state.ts";
import {
changeSetElementIsLeafOf,
commitIsAncestorOf,
jsonArrayFrom,
Lix,
sql,
UiDiffComponentProps,
ebEntity,
commitIsAncestorOf,
jsonArrayFrom,
Lix,
sql,
UiDiffComponentProps,
ebEntity,
} from "@lix-js/sdk";
import { redirect } from "react-router-dom";

Expand Down Expand Up @@ -130,8 +129,7 @@ export const intermediateChangesAtom = atom<
"change_set_element.change_id",
"change.id"
)
.where(changeSetElementIsLeafOf([{ id: workingChangeSetId }])) // Only get leaf changes
.where("change_set_element.change_set_id", "=", workingChangeSetId)
.where("change_set_element.change_set_id", "=", workingChangeSetId)
.where("change.file_id", "!=", "lix_own_change_control")
.select([
"change.id",
Expand Down Expand Up @@ -296,8 +294,7 @@ export const getChangeDiffs = async (
"change.id"
)
.where("change_set_element.change_set_id", "=", changeSetId)
.where(changeSetElementIsLeafOf([{ id: changeSetId }])) // Only get leaf changes
.where(ebEntity("change").hasLabel({ name: "checkpoint" }))
.where(ebEntity("change").hasLabel({ name: "checkpoint" }))
.selectAll("change")
.select(sql`json(snapshot.content)`.as("snapshot_content_after"));

Expand Down Expand Up @@ -326,8 +323,7 @@ export const getChangeDiffs = async (
"change_set_element.change_id",
"change.id"
)
.where(changeSetElementIsLeafOf([{ id: changeSetBeforeId }]))
.where("change.entity_id", "=", change.entity_id)
.where("change.entity_id", "=", change.entity_id)
.where("change.schema_key", "=", change.schema_key)
.where(ebEntity("change").hasLabel({ name: "checkpoint" }))
.select(sql`json(snapshot.content)`.as("snapshot_content_before"))
Expand Down
10 changes: 4 additions & 6 deletions packages/lix-sdk/llm.md → packages/lix-sdk/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Rules for LLMs

- Do not mock lix. Lix is a local SQLite database that does not need mocking. Test cases should always use the real lix.
- Do not mock lix. Lix is a local SQLite database that does not need mocking. Test cases should always use the real lix.

- Lix uses Kysely to expose the the SQL API in a typesafe way https://kysely-org.github.io/kysely-apidoc/.
- Lix uses Kysely to expose the the SQL API in a typesafe way https://kysely-org.github.io/kysely-apidoc/.

- The api reference for lix can be found in [./api-docs/README.md](./api-docs/README.md)

- tests for the lix sdk can be run with `pnpm exec vitest run ...`
- tests for the lix sdk can be run with `pnpm exec vitest run ...`

- validate the types AFTER the tests pass with `pnpm exec tsc --noEmit`

- always start with implementing test cases that reproduce bugs before implementing a fix to validate if the test captures the bug

- do not create getter functions. isntead query sql directly via kysely. otherwise, we end up with a huge pile of wrapper functions
- do not create getter functions. isntead query sql directly via kysely. otherwise, we end up with a huge pile of wrapper functions
4 changes: 2 additions & 2 deletions packages/lix-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"_comment": "Required for tree-shaking https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free",
"sideEffects": false,
"engines": {
"node": ">=18"
"node": ">=22"
},
"dependencies": {
"@codspeed/vitest-plugin": "^4.0.1",
Expand All @@ -50,4 +50,4 @@
"typescript-eslint": "^8.9.0",
"vitest": "3.2.4"
}
}
}
3 changes: 2 additions & 1 deletion packages/lix-sdk/src/change-author/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ test("should allow same author for multiple changes", async () => {
]);
});

test("change authors are accessible during a transaction", async () => {
// disabled because change author logic was moved into the commit phase
test.skip("change authors are accessible during a transaction", async () => {
// Create a lix instance with an active account
const lix = await openLix({
account: {
Expand Down
35 changes: 28 additions & 7 deletions packages/lix-sdk/src/change-proposal/create-change-proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { LixChangeSet } from "../change-set/schema.js";
import type { Lix } from "../lix/open-lix.js";
import type { ChangeProposal } from "./database-schema.js";
import { createChangeSet } from "../change-set/create-change-set.js";
import { changeSetElementInSymmetricDifference } from "../query-filter/change-set-element-in-symmetric-difference.js";

/**
* Creates a change proposal that represents the symmetric difference
Expand All @@ -20,14 +19,36 @@ export async function createChangeProposal(args: {
targetChangeSet: Pick<LixChangeSet, "id">;
}): Promise<ChangeProposal> {
const executeInTransaction = async (trx: Lix["db"]) => {
// Get the changes that are in the symmetric difference between the two change sets
// Compute symmetric difference of change_ids between the two change sets (inline stub)
const symmetricDifferenceChanges = await trx
.selectFrom("change_set_element")
.where(
changeSetElementInSymmetricDifference(
args.sourceChangeSet,
args.targetChangeSet
)
.where((eb) =>
eb.or([
eb("change_set_element.change_id", "in", (sub) =>
sub
.selectFrom("change_set_element as A")
.leftJoin("change_set_element as B", (join) =>
join
.onRef("A.change_id", "=", "B.change_id")
.on("B.change_set_id", "=", args.targetChangeSet.id)
)
.where("A.change_set_id", "=", args.sourceChangeSet.id)
.where("B.change_id", "is", null)
.select("A.change_id")
),
eb("change_set_element.change_id", "in", (sub) =>
sub
.selectFrom("change_set_element as B")
.leftJoin("change_set_element as A", (join) =>
join
.onRef("B.change_id", "=", "A.change_id")
.on("A.change_set_id", "=", args.sourceChangeSet.id)
)
.where("B.change_set_id", "=", args.targetChangeSet.id)
.where("A.change_id", "is", null)
.select("B.change_id")
),
])
)
.select(["change_id as id", "entity_id", "schema_key", "file_id"])
.execute();
Expand Down
2 changes: 1 addition & 1 deletion packages/lix-sdk/src/change-set/apply-change-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "../plugin/mock-json-plugin.js";
import type { LixChange } from "../change/schema.js";
import type { LixKeyValue } from "../key-value/schema.js";
import { createCheckpoint } from "../commit/create-checkpoint.js";
import { createCheckpoint } from "../state/create-checkpoint.js";

test("it applies lix own entity changes", async () => {
const lix = await openLix({});
Expand Down
4 changes: 2 additions & 2 deletions packages/lix-sdk/src/change-set/apply-change-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ export async function applyChangeSet(args: {
.execute();

// Write-through cache: populate internal_state_cache for all applied changes
const changesForCache = changesResult.map(change => ({
const changesForCache = changesResult.map((change) => ({
...change,
snapshot_content: change.snapshot_content
? JSON.stringify(change.snapshot_content)
: null,
}));

updateStateCache({
lix: args.lix,
changes: changesForCache,
Expand Down
Loading
Loading