latticesql 1.4.0 → 1.6.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # latticesql
2
2
 
3
- **Persistent memory for AI agents.** Keeps a SQLite database and a set of context files in sync — so every agent session starts with accurate state, and agent output becomes permanent data.
3
+ **Persistent memory for AI agents.** Keeps a SQLite **or Postgres** database and a set of context files in sync — so every agent session starts with accurate state, and agent output becomes permanent data.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/latticesql.svg)](https://www.npmjs.com/package/latticesql)
6
6
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
@@ -59,6 +59,7 @@ Lattice has no opinions about your schema, your agents, or your file format. You
59
59
  - [CLI — lattice generate](#cli--lattice-generate)
60
60
  - [Schema migrations](#schema-migrations)
61
61
  - [Security](#security)
62
+ - [Pluggable backends (v1.6+)](#pluggable-backends-v16)
62
63
  - [Architecture](#architecture)
63
64
  - [Examples](#examples)
64
65
  - [Staying up to date](#staying-up-to-date)
@@ -74,7 +75,24 @@ Lattice has no opinions about your schema, your agents, or your file format. You
74
75
  npm install latticesql
75
76
  ```
76
77
 
77
- Requires **Node.js 18+**. Uses `better-sqlite3` — no external database process needed.
78
+ Requires **Node.js 18+**. The default backend is SQLite (`better-sqlite3`) — no external database process needed.
79
+
80
+ To use the Postgres backend (for Supabase, Neon, RDS, or any other Postgres-compatible database), install the optional dependencies:
81
+
82
+ ```bash
83
+ npm install latticesql pg synckit
84
+ ```
85
+
86
+ Then pass a connection string instead of a file path:
87
+
88
+ ```ts
89
+ import { Lattice } from 'latticesql';
90
+
91
+ const lattice = new Lattice('postgres://user:pass@host:5432/db');
92
+ // rest of your setup is identical to the SQLite path
93
+ ```
94
+
95
+ See [Pluggable backends](#pluggable-backends-v16) below for full details.
78
96
 
79
97
  ---
80
98
 
@@ -861,22 +879,6 @@ Migrations are idempotent — each `version` number is applied exactly once, tra
861
879
 
862
880
  `close()` closes the SQLite connection. Call it when the process shuts down.
863
881
 
864
- #### Migration validation (v1.4+)
865
-
866
- Pass a `validateMigrationSQL` function in `InitOptions` to validate migration SQL before any migrations execute. If validation fails, no migrations run and an error is thrown.
867
-
868
- ```typescript
869
- await db.init({
870
- migrations: [{ version: 1, sql: 'ALTER TABLE tasks ADD COLUMN due_date TEXT' }],
871
- validateMigrationSQL: (sql) => {
872
- if (sql.trim().length === 0) return { valid: false, errors: ['Empty SQL'] };
873
- return { valid: true };
874
- },
875
- });
876
- ```
877
-
878
- Multi-statement migrations are fully supported — each migration's SQL can contain multiple semicolon-separated statements, all of which are executed.
879
-
880
882
  ### `migrate()` (v0.17+)
881
883
 
882
884
  ```typescript
@@ -1068,30 +1070,6 @@ await db.count(table: string, opts?: CountOptions): Promise<number>
1068
1070
  const n = await db.count('tasks', { where: { status: 'open' } });
1069
1071
  ```
1070
1072
 
1071
- #### `isDirty()` / `markDirty()` (v1.4+)
1072
-
1073
- ```typescript
1074
- db.isDirty(): boolean
1075
- db.markDirty(table?: string): this
1076
- ```
1077
-
1078
- Lattice tracks a per-table write version counter. `isDirty()` returns `true` when any table has been written to since the last `render()` call — useful for consumers implementing custom polling loops to skip redundant render cycles.
1079
-
1080
- `markDirty()` forces one or all tables to be considered changed. Call it after writing directly via the `db` escape hatch.
1081
-
1082
- ```typescript
1083
- // Custom watch loop that skips redundant renders
1084
- setInterval(async () => {
1085
- if (db.isDirty()) {
1086
- await db.render(outputDir);
1087
- }
1088
- }, 5000);
1089
-
1090
- // After direct DB writes, mark dirty so next render picks up changes
1091
- db.db.prepare('UPDATE tasks SET status = ? WHERE id = ?').run('done', taskId);
1092
- db.markDirty('tasks');
1093
- ```
1094
-
1095
1073
  ---
1096
1074
 
1097
1075
  ### Query operators
@@ -1304,18 +1282,6 @@ const rows = db.db
1304
1282
  .all('open');
1305
1283
  ```
1306
1284
 
1307
- After direct writes via the escape hatch, call `db.markDirty('table')` so `isDirty()` reflects the change.
1308
-
1309
- ---
1310
-
1311
- ### Performance (v1.4+)
1312
-
1313
- Lattice v1.4 includes two internal performance improvements that require no API changes:
1314
-
1315
- - **Prepared statement cache** — `SQLiteAdapter` caches compiled `better-sqlite3` statements. Repeated calls with the same SQL string reuse the compiled statement instead of recompiling. DDL statements bypass the cache. The cache clears automatically after schema changes and on `close()`.
1316
-
1317
- - **Batch entity query resolution** — Entity context rendering pre-fetches related rows for all entities in a single `WHERE IN (...)` query per source, replacing the previous per-entity query pattern. `hasMany`, `manyToMany`, and `belongsTo` sources are batched automatically. `custom` and `enriched` sources fall back to per-entity resolution.
1318
-
1319
1285
  ---
1320
1286
 
1321
1287
  ### Context optimization (v1.3+)
@@ -2153,6 +2119,100 @@ const db = new Lattice('./app.db', {
2153
2119
 
2154
2120
  ---
2155
2121
 
2122
+ ## Pluggable backends (v1.6+)
2123
+
2124
+ Lattice ships with two storage adapters and a pluggable interface so you can bring your own.
2125
+
2126
+ ### Picking a backend by connection string
2127
+
2128
+ The `Lattice` constructor inspects the first argument and picks the right adapter:
2129
+
2130
+ | First argument | Adapter | When to use |
2131
+ |---|---|---|
2132
+ | `'/abs/path/to/db.sqlite'` (or any plain path) | `SQLiteAdapter` | Default. Local file, no server. |
2133
+ | `':memory:'` | `SQLiteAdapter` | In-memory SQLite. Great for tests. |
2134
+ | `'file:/abs/path/to/db.sqlite'` | `SQLiteAdapter` | Same as the plain path form, with the scheme spelled out. |
2135
+ | `'postgres://user:pass@host:5432/db'` | `PostgresAdapter` | Postgres-compatible cloud DB (Supabase, Neon, RDS, …). |
2136
+ | `'postgresql://user:pass@host:5432/db'` | `PostgresAdapter` | Same as `postgres://`. |
2137
+ | any string + `{ adapter: myAdapter }` | your adapter | Bring your own implementation. |
2138
+
2139
+ ```ts
2140
+ import { Lattice } from 'latticesql';
2141
+
2142
+ // SQLite (default)
2143
+ const local = new Lattice('./data/lattice.db');
2144
+
2145
+ // Postgres (Supabase / Neon / RDS / etc.)
2146
+ const cloud = new Lattice('postgres://postgres:secret@db.example.com:5432/agent');
2147
+
2148
+ // Bring your own
2149
+ const custom = new Lattice('ignored', { adapter: new MyCustomAdapter() });
2150
+ ```
2151
+
2152
+ The rest of the API — `define()`, `init()`, `query()`, `insert()`, `render()`, `migrate()`, `watch()`, `reverseSync()`, `reverseSeed()` — is unchanged across both backends.
2153
+
2154
+ ### Postgres setup
2155
+
2156
+ `PostgresAdapter` depends on `pg` and `synckit`. Both are listed as `optionalDependencies`, so SQLite-only consumers don't pay the install cost. Install them when you actually use Postgres:
2157
+
2158
+ ```bash
2159
+ npm install pg synckit
2160
+ ```
2161
+
2162
+ Then point Lattice at any Postgres-compatible database that speaks the standard wire protocol on port 5432:
2163
+
2164
+ ```ts
2165
+ const lattice = new Lattice('postgres://user:pass@host:5432/db');
2166
+ await lattice.init();
2167
+ ```
2168
+
2169
+ **Connection pooler note:** if you put a pooler (e.g. PgBouncer, Supabase pooler) in front of your database, prefer **session-mode pooling**. Transaction-mode poolers do not support prepared statements across transactions, which would break Lattice's `adapter.prepare()` pattern.
2170
+
2171
+ **Why a worker thread under the hood:** the `StorageAdapter` interface is synchronous because `better-sqlite3` is sync. Every Node Postgres client is async. `PostgresAdapter` runs `pg` inside a `synckit` worker thread and blocks the main thread on `Atomics.wait` until the worker posts its reply. Each query pays ~1–3 ms of message-passing overhead — fine for Lattice's batch-insert + periodic-render workload. If you ever need OLTP-grade throughput, the interface can grow an async variant without breaking SQLite consumers.
2172
+
2173
+ **Schema portability:** Lattice's table definitions are mostly portable SQL. The adapter handles the few dialect differences automatically:
2174
+
2175
+ - `?` placeholders are translated to `$1, $2, …` for Postgres. Single-quoted strings, double-quoted identifiers, and SQL comments are skipped — `?` characters inside those are left alone.
2176
+ - `BLOB` column types are translated to `BYTEA` inside `addColumn`. Use `BLOB` in your `TableDefinition` and it works on both backends.
2177
+ - `datetime('now')` and `RANDOM()` defaults are translated to `NOW()` and `random()` for Postgres.
2178
+ - Use `TEXT PRIMARY KEY` (UUIDs) for portable primary keys. `INTEGER PRIMARY KEY` auto-increments on SQLite but not Postgres — if you need it on Postgres, use a sequence or `GENERATED ALWAYS AS IDENTITY`.
2179
+
2180
+ ### Bring your own adapter
2181
+
2182
+ The interface is small enough to implement against any backend:
2183
+
2184
+ ```ts
2185
+ export interface StorageAdapter {
2186
+ run(sql: string, params?: unknown[]): void;
2187
+ get(sql: string, params?: unknown[]): Row | undefined;
2188
+ all(sql: string, params?: unknown[]): Row[];
2189
+ prepare(sql: string): PreparedStatement;
2190
+ open(): void;
2191
+ close(): void;
2192
+ introspectColumns(table: string): string[];
2193
+ addColumn(table: string, column: string, typeSpec: string): void;
2194
+ }
2195
+ ```
2196
+
2197
+ Pass your implementation via `options.adapter`:
2198
+
2199
+ ```ts
2200
+ import { Lattice } from 'latticesql';
2201
+ import type { StorageAdapter } from 'latticesql';
2202
+
2203
+ class MyMySQLAdapter implements StorageAdapter { /* … */ }
2204
+
2205
+ const lattice = new Lattice('ignored', { adapter: new MyMySQLAdapter() });
2206
+ ```
2207
+
2208
+ ### Limitations
2209
+
2210
+ - `PreparedStatement.run()` returns `lastInsertRowid: 0` on the Postgres path. SQLite consumers that rely on `lastInsertRowid` should switch to `TEXT PRIMARY KEY` (UUIDs) for portability, or write `INSERT … RETURNING id` queries explicitly.
2211
+ - Two SQLite-only paths remain: `fixSchemaConflicts(db)` (the lifecycle helper that takes a raw `Database.Database` argument) and the writeback session-apply machinery. Postgres consumers shouldn't call them.
2212
+ - A built-in migration tool (SQLite → Postgres) is not included. Use a generic SQLite → Postgres migration tool, or `INSERT … SELECT` row-by-row.
2213
+
2214
+ ---
2215
+
2156
2216
  ## Architecture
2157
2217
 
2158
2218
  ```
@@ -2172,7 +2232,7 @@ const db = new Lattice('./app.db', {
2172
2232
  │ │ entity contexts │ │
2173
2233
  ├──────────────────┴──────────────────┴───────────────────────────────┤
2174
2234
  │ SQLiteAdapter │
2175
- (better-sqlite3 — synchronous I/O, statement cache)
2235
+ (better-sqlite3 — synchronous I/O)
2176
2236
  └──────────────────────────────────────────────────────────────────────┘
2177
2237
  │ │
2178
2238
  │ compileRender() │ lifecycle/
@@ -2190,7 +2250,7 @@ const db = new Lattice('./app.db', {
2190
2250
 
2191
2251
  **Key design decisions:**
2192
2252
 
2193
- - **Synchronous SQLite** — `better-sqlite3` gives synchronous reads; all Lattice CRUD methods return Promises for API consistency but resolve synchronously under the hood. Prepared statements are cached and reused automatically (v1.4+).
2253
+ - **Synchronous SQLite** — `better-sqlite3` gives synchronous reads; all Lattice CRUD methods return Promises for API consistency but resolve synchronously under the hood.
2194
2254
  - **Compile-time render** — `RenderSpec` is compiled to a plain `(rows: Row[]) => string` function at `define()`-time, not at render-time. `RenderEngine` stays unchanged.
2195
2255
  - **Atomic writes** — files are written to a `.tmp` sibling then renamed. No partial writes, no reader sees incomplete content.
2196
2256
  - **Schema-additive only** — Lattice never drops tables or columns automatically; it only adds missing ones.