latticesql 3.1.0 → 3.2.1
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 +4 -0
- package/dist/cli.js +2174 -1016
- package/dist/index.cjs +393 -190
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +393 -190
- package/docs/api-reference.md +1370 -0
- package/docs/architecture.md +331 -0
- package/docs/assistant.md +138 -0
- package/docs/cli.md +515 -0
- package/docs/cloud.md +675 -0
- package/docs/collaboration.md +85 -0
- package/docs/configuration.md +416 -0
- package/docs/entity-context.md +510 -0
- package/docs/examples/agent-system.md +313 -0
- package/docs/examples/cms.md +366 -0
- package/docs/examples/ticket-tracker.md +313 -0
- package/docs/migrations.md +272 -0
- package/docs/templates.md +338 -0
- package/docs/workspaces.md +81 -0
- package/package.json +3 -2
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Example: Ticket Tracker
|
|
2
|
+
|
|
3
|
+
A complete example showing how to build a simple ticket tracker with `latticesql` — users, tickets, comments, and automatic Markdown context generation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Scenario
|
|
8
|
+
|
|
9
|
+
A small engineering team wants:
|
|
10
|
+
|
|
11
|
+
- A SQLite-backed ticket system
|
|
12
|
+
- Automatic Markdown snapshots of the ticket board for LLM context
|
|
13
|
+
- TypeScript types generated from the schema
|
|
14
|
+
- Query support for filtering by status and assignee
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Project structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
ticket-tracker/
|
|
22
|
+
├── lattice.config.yml
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── db.ts
|
|
25
|
+
│ └── tickets.ts
|
|
26
|
+
├── context/
|
|
27
|
+
│ ├── USERS.md
|
|
28
|
+
│ ├── TICKETS.md
|
|
29
|
+
│ └── COMMENTS.md
|
|
30
|
+
├── data/
|
|
31
|
+
│ └── tickets.db
|
|
32
|
+
└── generated/
|
|
33
|
+
├── types.ts
|
|
34
|
+
└── migration.sql
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Schema
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
# lattice.config.yml
|
|
43
|
+
db: ./data/tickets.db
|
|
44
|
+
|
|
45
|
+
entities:
|
|
46
|
+
user:
|
|
47
|
+
fields:
|
|
48
|
+
id: { type: uuid, primaryKey: true }
|
|
49
|
+
name: { type: text, required: true }
|
|
50
|
+
email: { type: text, required: true }
|
|
51
|
+
team: { type: text }
|
|
52
|
+
render: default-table
|
|
53
|
+
outputFile: context/USERS.md
|
|
54
|
+
|
|
55
|
+
ticket:
|
|
56
|
+
fields:
|
|
57
|
+
id: { type: uuid, primaryKey: true }
|
|
58
|
+
title: { type: text, required: true }
|
|
59
|
+
description: { type: text }
|
|
60
|
+
status: { type: text, default: open }
|
|
61
|
+
priority: { type: integer, default: 2 }
|
|
62
|
+
reporter_id: { type: uuid, ref: user }
|
|
63
|
+
assignee_id: { type: uuid, ref: user }
|
|
64
|
+
created_at: { type: datetime }
|
|
65
|
+
closed_at: { type: datetime }
|
|
66
|
+
render:
|
|
67
|
+
template: default-list
|
|
68
|
+
formatRow: '[{{status}}] P{{priority}} {{title}} — {{assignee.name}}'
|
|
69
|
+
outputFile: context/TICKETS.md
|
|
70
|
+
|
|
71
|
+
comment:
|
|
72
|
+
fields:
|
|
73
|
+
id: { type: uuid, primaryKey: true }
|
|
74
|
+
body: { type: text, required: true }
|
|
75
|
+
ticket_id: { type: uuid, ref: ticket }
|
|
76
|
+
author_id: { type: uuid, ref: user }
|
|
77
|
+
created_at: { type: datetime }
|
|
78
|
+
render:
|
|
79
|
+
template: default-list
|
|
80
|
+
formatRow: '{{author.name}} ({{created_at}}): {{body}}'
|
|
81
|
+
outputFile: context/COMMENTS.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 2. Generate types and migration
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx lattice generate --out src/generated
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`src/generated/types.ts`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export interface User {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
email: string;
|
|
99
|
+
team?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Ticket {
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
priority?: number;
|
|
108
|
+
reporter_id?: string; // → user
|
|
109
|
+
assignee_id?: string; // → user
|
|
110
|
+
created_at?: string;
|
|
111
|
+
closed_at?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface Comment {
|
|
115
|
+
id: string;
|
|
116
|
+
body: string;
|
|
117
|
+
ticket_id?: string; // → ticket
|
|
118
|
+
author_id?: string; // → user
|
|
119
|
+
created_at?: string;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 3. Database setup
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// src/db.ts
|
|
129
|
+
import { Lattice } from 'latticesql';
|
|
130
|
+
|
|
131
|
+
export const db = new Lattice({
|
|
132
|
+
config: './lattice.config.yml',
|
|
133
|
+
options: { wal: true },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await db.init({
|
|
137
|
+
migrations: [
|
|
138
|
+
// v1: add tags column after initial release
|
|
139
|
+
{
|
|
140
|
+
version: 1,
|
|
141
|
+
sql: "ALTER TABLE ticket ADD COLUMN tags TEXT DEFAULT '[]'",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 4. Core ticket operations
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/tickets.ts
|
|
153
|
+
import { db } from './db.js';
|
|
154
|
+
|
|
155
|
+
// --- Users ---
|
|
156
|
+
|
|
157
|
+
export async function createUser(name: string, email: string, team?: string) {
|
|
158
|
+
return db.insert('user', {
|
|
159
|
+
name,
|
|
160
|
+
email,
|
|
161
|
+
team: team ?? null,
|
|
162
|
+
id: undefined, // auto-generated
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Tickets ---
|
|
167
|
+
|
|
168
|
+
export async function openTicket(opts: {
|
|
169
|
+
title: string;
|
|
170
|
+
description?: string;
|
|
171
|
+
priority?: number;
|
|
172
|
+
reporterId: string;
|
|
173
|
+
assigneeId?: string;
|
|
174
|
+
}) {
|
|
175
|
+
return db.insert('ticket', {
|
|
176
|
+
title: opts.title,
|
|
177
|
+
description: opts.description ?? null,
|
|
178
|
+
status: 'open',
|
|
179
|
+
priority: opts.priority ?? 2,
|
|
180
|
+
reporter_id: opts.reporterId,
|
|
181
|
+
assignee_id: opts.assigneeId ?? null,
|
|
182
|
+
created_at: new Date().toISOString(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function closeTicket(id: string) {
|
|
187
|
+
await db.update('ticket', id, {
|
|
188
|
+
status: 'closed',
|
|
189
|
+
closed_at: new Date().toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function assignTicket(id: string, assigneeId: string) {
|
|
194
|
+
await db.update('ticket', id, { assignee_id: assigneeId });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function getOpenTickets(assigneeId?: string) {
|
|
198
|
+
if (assigneeId) {
|
|
199
|
+
return db.query('ticket', {
|
|
200
|
+
where: { status: 'open', assignee_id: assigneeId },
|
|
201
|
+
orderBy: 'priority',
|
|
202
|
+
orderDir: 'desc',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return db.query('ticket', {
|
|
206
|
+
where: { status: 'open' },
|
|
207
|
+
orderBy: 'priority',
|
|
208
|
+
orderDir: 'desc',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function searchTickets(titleFragment: string) {
|
|
213
|
+
return db.query('ticket', {
|
|
214
|
+
filters: [
|
|
215
|
+
{ col: 'title', op: 'like', val: `%${titleFragment}%` },
|
|
216
|
+
{ col: 'status', op: 'ne', val: 'archived' },
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function getHighPriorityOpen() {
|
|
222
|
+
return db.query('ticket', {
|
|
223
|
+
filters: [
|
|
224
|
+
{ col: 'status', op: 'eq', val: 'open' },
|
|
225
|
+
{ col: 'priority', op: 'gte', val: 3 },
|
|
226
|
+
],
|
|
227
|
+
orderBy: 'priority',
|
|
228
|
+
orderDir: 'desc',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Comments ---
|
|
233
|
+
|
|
234
|
+
export async function addComment(ticketId: string, authorId: string, body: string) {
|
|
235
|
+
return db.insert('comment', {
|
|
236
|
+
body,
|
|
237
|
+
ticket_id: ticketId,
|
|
238
|
+
author_id: authorId,
|
|
239
|
+
created_at: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function getComments(ticketId: string) {
|
|
244
|
+
return db.query('comment', {
|
|
245
|
+
where: { ticket_id: ticketId },
|
|
246
|
+
orderBy: 'created_at',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 5. Reports and counts
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Dashboard stats
|
|
257
|
+
const openCount = await db.count('ticket', { where: { status: 'open' } });
|
|
258
|
+
const closedCount = await db.count('ticket', { where: { status: 'closed' } });
|
|
259
|
+
|
|
260
|
+
// Tickets assigned to no-one
|
|
261
|
+
const unassigned = await db.query('ticket', {
|
|
262
|
+
filters: [
|
|
263
|
+
{ col: 'assignee_id', op: 'isNull' },
|
|
264
|
+
{ col: 'status', op: 'eq', val: 'open' },
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
console.log(`Open: ${openCount}, Closed: ${closedCount}, Unassigned: ${unassigned.length}`);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 6. Context sync
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// Keep context files fresh — sync every 30 seconds
|
|
277
|
+
const stop = await db.watch('./context', {
|
|
278
|
+
interval: 30_000,
|
|
279
|
+
onRender: (r) => console.log(`[sync] ${r.filesWritten.length} files updated`),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
process.on('SIGTERM', () => {
|
|
283
|
+
stop();
|
|
284
|
+
db.close();
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 7. Sample context output
|
|
291
|
+
|
|
292
|
+
**`context/TICKETS.md`:**
|
|
293
|
+
|
|
294
|
+
```markdown
|
|
295
|
+
# ticket
|
|
296
|
+
|
|
297
|
+
- [open] P3 Auth tokens expire too early — Alice
|
|
298
|
+
- [open] P2 Slow search on large datasets — (unassigned)
|
|
299
|
+
- [closed] P2 Fix CSV export encoding — Bob
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**`context/USERS.md`:**
|
|
303
|
+
|
|
304
|
+
```markdown
|
|
305
|
+
# user
|
|
306
|
+
|
|
307
|
+
| id | name | email | team |
|
|
308
|
+
| --- | ----- | ----------------- | -------- |
|
|
309
|
+
| u-1 | Alice | alice@example.com | Backend |
|
|
310
|
+
| u-2 | Bob | bob@example.com | Frontend |
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
An LLM reading the ticket context immediately sees what's open, who owns what, and at what priority — without needing to query the database directly.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Migration Guide
|
|
2
|
+
|
|
3
|
+
How to evolve your database schema over time with Lattice.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [How migrations work](#how-migrations-work)
|
|
11
|
+
- [Writing your first migration](#writing-your-first-migration)
|
|
12
|
+
- [Workflow: YAML config + codegen](#workflow-yaml-config--codegen)
|
|
13
|
+
- [Migration best practices](#migration-best-practices)
|
|
14
|
+
- [Common migration patterns](#common-migration-patterns)
|
|
15
|
+
- [Rollback strategy](#rollback-strategy)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Lattice uses a version-tracked migration system. You provide an array of `Migration` objects when calling `init()`. Each migration runs exactly once and is tracked in a `_lattice_migrations` table inside your database.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
await db.init({
|
|
25
|
+
migrations: [
|
|
26
|
+
{ version: 1, sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1' },
|
|
27
|
+
{ version: 2, sql: 'CREATE INDEX idx_tasks_status ON tasks (status)' },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## How migrations work
|
|
35
|
+
|
|
36
|
+
1. On `init()`, Lattice creates `_lattice_migrations (version INTEGER PRIMARY KEY)` if it doesn't exist.
|
|
37
|
+
2. For each migration in the array, Lattice checks whether its `version` is already in `_lattice_migrations`.
|
|
38
|
+
3. If not present, the migration's SQL is executed and the version is recorded.
|
|
39
|
+
4. Migrations are run in the order they appear in the array, not necessarily by version number — but using ascending version numbers is strongly recommended.
|
|
40
|
+
|
|
41
|
+
**Key properties:**
|
|
42
|
+
|
|
43
|
+
- Each migration runs at most once per database (idempotent across restarts)
|
|
44
|
+
- Migrations run inside implicit transactions (SQLite auto-commit per statement)
|
|
45
|
+
- If a migration fails, the error is thrown and the version is not recorded — the migration will be retried on the next startup
|
|
46
|
+
- `CREATE TABLE IF NOT EXISTS` in `init()` always runs (not a migration) — `columns` specs are for initial schema creation
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Writing your first migration
|
|
51
|
+
|
|
52
|
+
**Before (initial schema — `define()`):**
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
db.define('tasks', {
|
|
56
|
+
columns: {
|
|
57
|
+
id: 'TEXT PRIMARY KEY',
|
|
58
|
+
title: 'TEXT NOT NULL',
|
|
59
|
+
status: "TEXT DEFAULT 'open'",
|
|
60
|
+
},
|
|
61
|
+
render: 'default-list',
|
|
62
|
+
outputFile: 'context/TASKS.md',
|
|
63
|
+
});
|
|
64
|
+
await db.init();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**After (add a column in v0.2 of your app):**
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
db.define('tasks', {
|
|
71
|
+
columns: {
|
|
72
|
+
id: 'TEXT PRIMARY KEY',
|
|
73
|
+
title: 'TEXT NOT NULL',
|
|
74
|
+
status: "TEXT DEFAULT 'open'",
|
|
75
|
+
priority: 'INTEGER DEFAULT 1', // New column
|
|
76
|
+
},
|
|
77
|
+
render: 'default-list',
|
|
78
|
+
outputFile: 'context/TASKS.md',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await db.init({
|
|
82
|
+
migrations: [
|
|
83
|
+
{
|
|
84
|
+
version: 1,
|
|
85
|
+
sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `columns` spec reflects the _current_ desired schema (used for new databases). The migration handles the _delta_ for existing databases. Both code paths result in the same final schema.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Workflow: YAML config + codegen
|
|
96
|
+
|
|
97
|
+
When using `lattice.config.yml`, the recommended workflow is:
|
|
98
|
+
|
|
99
|
+
### Step 1: Update the YAML config
|
|
100
|
+
|
|
101
|
+
Add the new field to your entity:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
# lattice.config.yml
|
|
105
|
+
entities:
|
|
106
|
+
task:
|
|
107
|
+
fields:
|
|
108
|
+
id: { type: uuid, primaryKey: true }
|
|
109
|
+
title: { type: text, required: true }
|
|
110
|
+
status: { type: text, default: open }
|
|
111
|
+
priority: { type: integer, default: 1 } # ← added
|
|
112
|
+
render: default-list
|
|
113
|
+
outputFile: context/TASKS.md
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Step 2: Regenerate
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
lattice generate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This regenerates `generated/types.ts` (with `priority?: number` on the `Task` interface) and `generated/migration.sql` (with `"priority" INTEGER DEFAULT 1` in the `CREATE TABLE` statement).
|
|
123
|
+
|
|
124
|
+
### Step 3: Write the migration
|
|
125
|
+
|
|
126
|
+
The generated `migration.sql` is for fresh databases. For existing databases, write a numbered migration in your application startup code:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
await db.init({
|
|
130
|
+
migrations: [{ version: 1, sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1' }],
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Keep migrations in a dedicated file so they accumulate over time:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// src/migrations.ts
|
|
138
|
+
import type { Migration } from 'latticesql';
|
|
139
|
+
|
|
140
|
+
export const migrations: Migration[] = [
|
|
141
|
+
{
|
|
142
|
+
version: 1,
|
|
143
|
+
sql: 'ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 1',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
version: 2,
|
|
147
|
+
sql: "ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT '[]'",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
version: 3,
|
|
151
|
+
sql: 'CREATE INDEX idx_tasks_priority ON tasks (priority DESC)',
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { migrations } from './migrations.js';
|
|
158
|
+
|
|
159
|
+
await db.init({ migrations });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Migration best practices
|
|
165
|
+
|
|
166
|
+
**Number migrations sequentially.** Use `1, 2, 3...` — never skip numbers. If two developers add migrations simultaneously, coordinate version numbers before merging.
|
|
167
|
+
|
|
168
|
+
**Never change a committed migration.** Once a migration is deployed and has run on any database, treat it as immutable. If you need to fix it, write a new migration that corrects the previous one.
|
|
169
|
+
|
|
170
|
+
**Keep migrations small.** One change per migration. This makes failures easy to diagnose and rollbacks straightforward.
|
|
171
|
+
|
|
172
|
+
**Test migrations.** Run your full migration sequence against a copy of production data in CI. Lattice's test utilities (`':memory:'` db) are useful for unit tests, but test against real data for confidence.
|
|
173
|
+
|
|
174
|
+
**Document each migration.** Use a comment in the SQL or the `migrations.ts` file explaining why the change was made:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
{
|
|
178
|
+
version: 4,
|
|
179
|
+
// Add soft-delete support — marketing wants to recover deleted tasks
|
|
180
|
+
sql: 'ALTER TABLE tasks ADD COLUMN deleted_at TEXT',
|
|
181
|
+
},
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Common migration patterns
|
|
187
|
+
|
|
188
|
+
### Add a nullable column
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
{ version: 5, sql: 'ALTER TABLE tasks ADD COLUMN notes TEXT' }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
SQLite `ALTER TABLE ADD COLUMN` supports nullable columns with no default. All existing rows get `NULL`.
|
|
195
|
+
|
|
196
|
+
### Add a column with a default
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
{ version: 6, sql: "ALTER TABLE tasks ADD COLUMN status TEXT DEFAULT 'open'" }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Add an index
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
{ version: 7, sql: 'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks (status)' }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Use `IF NOT EXISTS` to make the migration idempotent even if manually applied.
|
|
209
|
+
|
|
210
|
+
### Rename a column (SQLite limitation)
|
|
211
|
+
|
|
212
|
+
SQLite does not support `ALTER TABLE RENAME COLUMN` in older versions. The standard approach is a table copy:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
{
|
|
216
|
+
version: 8,
|
|
217
|
+
sql: `
|
|
218
|
+
CREATE TABLE tasks_new AS SELECT
|
|
219
|
+
id, title, status, priority,
|
|
220
|
+
due_at AS deadline,
|
|
221
|
+
created_at
|
|
222
|
+
FROM tasks;
|
|
223
|
+
DROP TABLE tasks;
|
|
224
|
+
ALTER TABLE tasks_new RENAME TO tasks;
|
|
225
|
+
`.trim(),
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> **Warning:** This drops and recreates the table. All indexes are lost and must be recreated in a subsequent migration.
|
|
230
|
+
|
|
231
|
+
### Add a NOT NULL column to an existing table
|
|
232
|
+
|
|
233
|
+
SQLite requires a `DEFAULT` on `ALTER TABLE ADD COLUMN` when `NOT NULL` is specified:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
{
|
|
237
|
+
version: 9,
|
|
238
|
+
sql: "ALTER TABLE tasks ADD COLUMN category TEXT NOT NULL DEFAULT 'general'",
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Backfill data after adding a column
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
{
|
|
246
|
+
version: 10,
|
|
247
|
+
sql: `
|
|
248
|
+
ALTER TABLE tasks ADD COLUMN slug TEXT;
|
|
249
|
+
UPDATE tasks SET slug = LOWER(REPLACE(title, ' ', '-')) WHERE slug IS NULL;
|
|
250
|
+
`.trim(),
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Rollback strategy
|
|
257
|
+
|
|
258
|
+
SQLite does not support transactional DDL (Data Definition Language) across multiple statements in the same way Postgres does. In practice, this means:
|
|
259
|
+
|
|
260
|
+
- **Test first.** Run migrations against a staging database before production.
|
|
261
|
+
- **Back up before migrating.** Lattice does not snapshot your database — do it yourself:
|
|
262
|
+
```sh
|
|
263
|
+
cp data/app.db data/app.db.bak-$(date +%Y%m%d%H%M%S)
|
|
264
|
+
```
|
|
265
|
+
- **Simple column additions are safe.** `ALTER TABLE ADD COLUMN` is non-destructive. Rollback = add nothing.
|
|
266
|
+
- **Table rebuilds are risky.** If a rename migration fails halfway, the database may be in an inconsistent state. Restore from backup.
|
|
267
|
+
|
|
268
|
+
For production deployments, always:
|
|
269
|
+
|
|
270
|
+
1. Back up the database
|
|
271
|
+
2. Run migrations in a pre-start hook before the app opens the connection
|
|
272
|
+
3. Have a tested restore procedure
|