flint-orm 0.2.0 → 0.4.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.
Files changed (5) hide show
  1. package/API.md +208 -211
  2. package/README.md +341 -0
  3. package/dist/index.js +288 -255
  4. package/package.json +46 -11
  5. package/src/cli.ts +103 -48
package/API.md CHANGED
@@ -1,60 +1,85 @@
1
1
  # Flint ORM — API Reference
2
2
 
3
- A minimal, SQLite/libSQL-only query builder. Type-safe, immutable, parameterized.
3
+ A type-safe, driver-agnostic SQLite ORM for JavaScript. One schema, any driver.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  bun add flint-orm
9
+ # or
10
+ npm install flint-orm
9
11
  ```
10
12
 
11
13
  ## Quick Start
12
14
 
13
15
  ```ts
14
- import { flint, table, text, integer, eq } from 'flint-orm';
16
+ import { flint } from 'flint-orm/bun-sqlite';
17
+ import { table, text, integer, date } from 'flint-orm/table';
18
+ import { eq } from 'flint-orm/expressions';
15
19
 
16
20
  // Define schema
17
21
  const users = table('users', {
18
- id: text('id').primaryKey(),
19
- name: text('name').notNull(),
20
- email: text('email').unique(),
21
- age: integer('age'),
22
+ id: text().primaryKey(),
23
+ name: text().notNull(),
24
+ email: text().unique(),
25
+ age: integer(),
26
+ createdAt: date().defaultNow(),
22
27
  });
23
28
 
24
29
  // Connect
25
- const db = flint({ url: 'app.db' });
30
+ const db = flint({ url: './app.db' });
26
31
 
27
32
  // Query
28
- const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute();
29
- // { id: "u1", name: "Alice", email: "alice@example.com", age: 30 }
33
+ const user = await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
34
+ // { id: "u1", name: "Alice", email: "alice@example.com", age: 30, createdAt: Date }
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Config
40
+
41
+ Create `flint.config.ts` in your project root:
42
+
43
+ ```ts
44
+ import { defineConfig } from 'flint-orm/config';
45
+
46
+ export default defineConfig({
47
+ driver: 'bun-sqlite', // 'bun-sqlite' | 'better-sqlite3' | 'libsql' | 'libsql-web' | 'turso' | 'turso-sync'
48
+ database: {
49
+ url: './app.db',
50
+ authToken: '...', // for libsql/libsql-web/turso-sync only
51
+ },
52
+ schema: './src/schema',
53
+ migrations: './flint',
54
+ });
30
55
  ```
31
56
 
32
57
  ---
33
58
 
34
59
  ## Schema
35
60
 
36
- ### `table(name, columns)`
61
+ ### `table(name, columns, indexFn?)`
37
62
 
38
63
  Define a table. Columns live as direct properties. SQL metadata is under `._`.
39
64
 
40
65
  ```ts
41
- import { table, text, integer, boolean } from 'flint-orm';
66
+ import { table, text, integer, boolean, index } from 'flint-orm/table';
42
67
 
43
68
  const users = table('users', {
44
- id: text('id').primaryKey(),
45
- name: text('name').notNull(),
46
- email: text('email').unique(),
47
- active: boolean('active').default(true),
48
- age: integer('age'),
69
+ id: text().primaryKey(),
70
+ name: text().notNull(),
71
+ email: text().unique(),
72
+ active: boolean().default(true),
73
+ age: integer(),
49
74
  });
50
75
  ```
51
76
 
52
- ### `snakeCase.table(name, columns)`
77
+ ### `snakeCase.table(name, columns, indexFn?)`
53
78
 
54
79
  Auto-converts camelCase keys to snake_case SQL names.
55
80
 
56
81
  ```ts
57
- import { snakeCase, text } from 'flint-orm';
82
+ import { snakeCase, text } from 'flint-orm/table';
58
83
 
59
84
  const users = snakeCase.table('users', {
60
85
  id: text().primaryKey(), // SQL: id
@@ -65,21 +90,21 @@ const users = snakeCase.table('users', {
65
90
 
66
91
  ### Column Types
67
92
 
68
- | Function | TS Type | SQLite Storage | Notes |
69
- | ----------- | --------- | ------------------ | --------------------------------------- |
70
- | `text()` | `string` | TEXT | |
71
- | `integer()` | `number` | INTEGER | Supports `.autoIncrement()` |
72
- | `boolean()` | `boolean` | INTEGER (0/1) | Encodes/decodes automatically |
73
- | `json<T>()` | `T` | TEXT (JSON) | Generic, encodes/decodes automatically |
74
- | `real()` | `number` | REAL | |
75
- | `date()` | `Date` | INTEGER (epoch ms) | Supports `.defaultNow()`, `.onUpdate()` |
93
+ | Function | TS Type | SQLite Storage | Notes |
94
+ | ----------- | --------- | ------------------ | ------------------------------------------------ |
95
+ | `text()` | `string` | TEXT | |
96
+ | `integer()` | `number` | INTEGER | Supports `.autoIncrement()` |
97
+ | `boolean()` | `boolean` | INTEGER (0/1) | Encodes/decodes automatically |
98
+ | `json<T>()` | `T` | TEXT (JSON) | Generic, encodes/decodes automatically |
99
+ | `real()` | `number` | REAL | |
100
+ | `date()` | `Date` | INTEGER (epoch ms) | Supports `.defaultNow()`, `.onUpdateTimestamp()` |
76
101
 
77
102
  ### Column Modifiers
78
103
 
79
104
  Every column supports chaining:
80
105
 
81
106
  ```ts
82
- text('name')
107
+ text()
83
108
  .primaryKey() // PRIMARY KEY
84
109
  .notNull() // NOT NULL
85
110
  .unique() // UNIQUE
@@ -91,15 +116,15 @@ text('name')
91
116
  **Integer-only:**
92
117
 
93
118
  ```ts
94
- integer('id').autoIncrement(); // AUTOINCREMENT
119
+ integer().autoIncrement(); // AUTOINCREMENT
95
120
  ```
96
121
 
97
122
  **Date-only:**
98
123
 
99
124
  ```ts
100
- date('created_at')
125
+ date()
101
126
  .defaultNow() // DEFAULT (current epoch ms)
102
- .onUpdate(); // Always set to now on UPDATE
127
+ .onUpdateTimestamp(); // Always set to now on UPDATE
103
128
  ```
104
129
 
105
130
  ### `InferRow<T>`
@@ -107,8 +132,10 @@ date('created_at')
107
132
  Derives the row type from a table definition.
108
133
 
109
134
  ```ts
135
+ import type { InferRow } from 'flint-orm/table';
136
+
110
137
  type UserRow = InferRow<typeof users>;
111
- // { id: string; name: string; email: string | null; active: boolean; age: number | null }
138
+ // { id: string; name: string; email: string | null; active: boolean; age: number | null; createdAt: Date }
112
139
  ```
113
140
 
114
141
  ### `InsertRow<T>`
@@ -116,20 +143,44 @@ type UserRow = InferRow<typeof users>;
116
143
  Row type for INSERT. Columns with defaults or autoIncrement are optional.
117
144
 
118
145
  ```ts
146
+ import type { InsertRow } from 'flint-orm/table';
147
+
119
148
  type UserInsert = InsertRow<typeof users>;
120
- // { id: string; name: string; email?: string; active?: boolean; age?: number }
149
+ // { id: string; name: string; email?: string; active?: boolean; age?: number; createdAt?: Date }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Indexes
155
+
156
+ ### `index(name).on(columns).unique()`
157
+
158
+ Define indexes via the table callback. Chainable API.
159
+
160
+ ```ts
161
+ const users = table(
162
+ 'users',
163
+ {
164
+ id: text().primaryKey(),
165
+ email: text(),
166
+ name: text(),
167
+ },
168
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
169
+ );
121
170
  ```
122
171
 
123
172
  ---
124
173
 
125
174
  ## Query Builder
126
175
 
176
+ All `execute()` methods return `Promise<T>` — always `await` regardless of driver.
177
+
127
178
  ### `db.select().from(table)`
128
179
 
129
180
  Start a SELECT query. Two-phase: `.from()` is required before anything else.
130
181
 
131
182
  ```ts
132
- db.select().from(users).execute();
183
+ await db.select().from(users).execute();
133
184
  // SELECT * FROM users
134
185
  ```
135
186
 
@@ -138,7 +189,7 @@ db.select().from(users).execute();
138
189
  Narrow which columns appear in the result.
139
190
 
140
191
  ```ts
141
- db.select().from(users).columns(['id', 'name']).execute();
192
+ await db.select().from(users).columns(['id', 'name']).execute();
142
193
  // SELECT id, name FROM users
143
194
  // Returns: { id: string; name: string }[]
144
195
  ```
@@ -148,7 +199,7 @@ db.select().from(users).columns(['id', 'name']).execute();
148
199
  Filter rows.
149
200
 
150
201
  ```ts
151
- db.select().from(users).where(eq(users.active, true)).execute();
202
+ await db.select().from(users).where(eq(users.active, true)).execute();
152
203
  // SELECT * FROM users WHERE active = 1
153
204
  ```
154
205
 
@@ -157,7 +208,7 @@ db.select().from(users).where(eq(users.active, true)).execute();
157
208
  Return one row or null instead of an array. Adds `LIMIT 1`.
158
209
 
159
210
  ```ts
160
- db.select().from(users).where(eq(users.id, 'u1')).single().execute();
211
+ await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
161
212
  // SELECT * FROM users WHERE id = ? LIMIT 1
162
213
  // Returns: UserRow | null
163
214
  ```
@@ -167,7 +218,7 @@ db.select().from(users).where(eq(users.id, 'u1')).single().execute();
167
218
  Sort results. Default direction is `"asc"`.
168
219
 
169
220
  ```ts
170
- db.select().from(users).orderBy('name', 'desc').execute();
221
+ await db.select().from(users).orderBy('name', 'desc').execute();
171
222
  // SELECT * FROM users ORDER BY name DESC
172
223
  ```
173
224
 
@@ -176,7 +227,7 @@ db.select().from(users).orderBy('name', 'desc').execute();
176
227
  Limit the number of results.
177
228
 
178
229
  ```ts
179
- db.select().from(users).limit(10).execute();
230
+ await db.select().from(users).limit(10).execute();
180
231
  // SELECT * FROM users LIMIT 10
181
232
  ```
182
233
 
@@ -185,7 +236,7 @@ db.select().from(users).limit(10).execute();
185
236
  Skip N rows.
186
237
 
187
238
  ```ts
188
- db.select().from(users).limit(10).offset(20).execute();
239
+ await db.select().from(users).limit(10).offset(20).execute();
189
240
  // SELECT * FROM users LIMIT 10 OFFSET 20
190
241
  ```
191
242
 
@@ -194,17 +245,10 @@ db.select().from(users).limit(10).offset(20).execute();
194
245
  Return unique rows.
195
246
 
196
247
  ```ts
197
- db.select().from(users).columns(['name']).distinct().execute();
248
+ await db.select().from(users).columns(['name']).distinct().execute();
198
249
  // SELECT DISTINCT name FROM users
199
250
  ```
200
251
 
201
- ### Full Example
202
-
203
- ```ts
204
- const results = db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name', 'asc').limit(10).offset(0).execute();
205
- // Returns: { id: string; name: string }[]
206
- ```
207
-
208
252
  ---
209
253
 
210
254
  ### `db.insert(table).values(row)`
@@ -213,23 +257,16 @@ Insert one or more rows. Two-phase: `.values()` is required before `.execute()`.
213
257
 
214
258
  ```ts
215
259
  // Single row
216
- db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
217
- // INSERT INTO users (id, name, email) VALUES (?, ?, ?)
260
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
218
261
 
219
- // Multiple rows (bulk insert)
220
- db.insert(users)
262
+ // Multiple rows
263
+ await db
264
+ .insert(users)
221
265
  .values([
222
266
  { id: 'u1', name: 'Alice' },
223
267
  { id: 'u2', name: 'Bob' },
224
268
  ])
225
269
  .execute();
226
- // INSERT INTO users (id, name) VALUES (?, ?), (?, ?)
227
- ```
228
-
229
- Columns with defaults can be omitted:
230
-
231
- ```ts
232
- db.insert(users).values({ id: 'u1', name: 'Alice' }).execute();
233
270
  ```
234
271
 
235
272
  ### `.returning()`
@@ -237,12 +274,10 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).execute();
237
274
  Return the inserted row(s) instead of void. Pass an array to narrow which columns are returned.
238
275
 
239
276
  ```ts
240
- // Return all columns
241
- const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
277
+ const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
242
278
  // Returns: { id: string; name: string; ... }[]
243
279
 
244
- // Return specific columns
245
- const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute();
280
+ const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute();
246
281
  // Returns: { id: string; name: string }[]
247
282
  ```
248
283
 
@@ -251,8 +286,7 @@ const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id
251
286
  Skip the insert if a row with the same primary key already exists.
252
287
 
253
288
  ```ts
254
- db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
255
- // INSERT OR IGNORE INTO users ...
289
+ await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
256
290
  ```
257
291
 
258
292
  ### `.onConflictDoUpdate()`
@@ -260,14 +294,14 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execu
260
294
  Update specific columns when a row with the same primary key already exists (upsert).
261
295
 
262
296
  ```ts
263
- db.insert(users)
297
+ await db
298
+ .insert(users)
264
299
  .values({ id: 'u1', name: 'Alice' })
265
300
  .onConflictDoUpdate({
266
301
  target: users.id,
267
302
  set: { name: 'Alice Updated' },
268
303
  })
269
304
  .execute();
270
- // INSERT INTO users ... ON CONFLICT (id) DO UPDATE SET name = excluded.name
271
305
  ```
272
306
 
273
307
  ---
@@ -277,29 +311,22 @@ db.insert(users)
277
311
  Update rows. Two-phase: `.set()` is required before `.execute()`.
278
312
 
279
313
  ```ts
280
- db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
281
- // UPDATE users SET name = ? WHERE id = ?
314
+ await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
282
315
  ```
283
316
 
284
317
  Multiple `.set()` calls merge:
285
318
 
286
319
  ```ts
287
- db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute();
288
- // UPDATE users SET name = ?, email = ? WHERE id = ?
320
+ await db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute();
289
321
  ```
290
322
 
291
323
  ### `.returning()`
292
324
 
293
- Return the updated row(s) instead of void. Pass an array to narrow which columns are returned.
325
+ Return the updated row(s) instead of void.
294
326
 
295
327
  ```ts
296
- // Return all columns
297
- const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
328
+ const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
298
329
  // Returns: { id: string; name: string; ... }[]
299
-
300
- // Return specific columns
301
- const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning(['id', 'name']).execute();
302
- // Returns: { id: string; name: string }[]
303
330
  ```
304
331
 
305
332
  ---
@@ -309,22 +336,16 @@ const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).
309
336
  Delete rows.
310
337
 
311
338
  ```ts
312
- db.delete(users).where(eq(users.id, 'u1')).execute();
313
- // DELETE FROM users WHERE id = ?
339
+ await db.delete(users).where(eq(users.id, 'u1')).execute();
314
340
  ```
315
341
 
316
342
  ### `.returning()`
317
343
 
318
- Return the deleted row(s) instead of void. Pass an array to narrow which columns are returned.
344
+ Return the deleted row(s) instead of void.
319
345
 
320
346
  ```ts
321
- // Return all columns
322
- const deleted = db.delete(users).where(eq(users.id, 'u1')).returning().execute();
347
+ const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute();
323
348
  // Returns: { id: string; name: string; ... }[]
324
-
325
- // Return specific columns
326
- const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'name']).execute();
327
- // Returns: { id: string; name: string }[]
328
349
  ```
329
350
 
330
351
  ---
@@ -333,20 +354,20 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
333
354
 
334
355
  ### `db.leftJoin(parent).on(child, condition?)`
335
356
 
336
- LEFT JOIN. Returns all parent rows, with matching child data or null.
357
+ LEFT JOIN. Returns all parent rows, with matching child data nested under the child table name.
337
358
 
338
359
  ```ts
339
360
  const orders = table('orders', {
340
- id: text('id').primaryKey(),
341
- userId: text('userId').notNull().references(users.id),
342
- total: integer('total').notNull(),
361
+ id: text().primaryKey(),
362
+ userId: text().notNull().references(users.id),
363
+ total: integer().notNull(),
343
364
  });
344
365
 
345
366
  // With explicit condition
346
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).execute();
367
+ await db.leftJoin(users).on(orders, eq(orders.userId, users.id)).execute();
347
368
 
348
369
  // Auto-join from foreign key (if .references() is defined)
349
- db.leftJoin(orders).on(users).execute();
370
+ await db.leftJoin(users).on(orders).execute();
350
371
  ```
351
372
 
352
373
  ### `db.innerJoin(parent).on(child, condition?)`
@@ -354,7 +375,7 @@ db.leftJoin(orders).on(users).execute();
354
375
  INNER JOIN. Returns only rows where both tables match.
355
376
 
356
377
  ```ts
357
- db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
378
+ await db.innerJoin(users).on(orders, eq(orders.userId, users.id)).execute();
358
379
  ```
359
380
 
360
381
  ### Join Result Shape
@@ -362,7 +383,6 @@ db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
362
383
  Joins return **nested** results, not flat-merged:
363
384
 
364
385
  ```ts
365
- // One-to-many: one user with multiple orders
366
386
  [
367
387
  {
368
388
  id: 'u1',
@@ -380,26 +400,36 @@ Joins return **nested** results, not flat-merged:
380
400
  Narrow parent columns with `.columns()`:
381
401
 
382
402
  ```ts
383
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
403
+ await db.leftJoin(users).on(orders).columns(['id', 'name']).execute();
384
404
  // Returns: { id: string; name: string; orders: OrderRow[] }[]
385
405
  ```
386
406
 
387
407
  ### `.single()` on Joins
388
408
 
389
409
  ```ts
390
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
410
+ await db.leftJoin(users).on(orders).single().execute();
391
411
  // Returns: { id: string; name: string; orders: OrderRow[] } | null
392
412
  ```
393
413
 
414
+ ### Multi-Join
415
+
416
+ Chain multiple joins:
417
+
418
+ ```ts
419
+ await db.leftJoin(users).on(orders).leftJoin(orders).on(orderItems).execute();
420
+ ```
421
+
394
422
  ---
395
423
 
396
424
  ## Conditions
397
425
 
398
- All conditions are imported from `flint-orm`.
426
+ All conditions are imported from `flint-orm/expressions`.
399
427
 
400
428
  ### Comparison
401
429
 
402
430
  ```ts
431
+ import { eq, neq, gt, gte, lt, lte } from 'flint-orm/expressions';
432
+
403
433
  eq(column, value); // column = value
404
434
  eq(left, right); // left = right (column-to-column)
405
435
  neq(column, value); // column != value
@@ -412,12 +442,16 @@ lte(column, value); // column <= value
412
442
  ### Range
413
443
 
414
444
  ```ts
445
+ import { between } from 'flint-orm/expressions';
446
+
415
447
  between(column, low, high); // column BETWEEN low AND high
416
448
  ```
417
449
 
418
450
  ### Null Checks
419
451
 
420
452
  ```ts
453
+ import { isNull, isNotNull } from 'flint-orm/expressions';
454
+
421
455
  isNull(column); // column IS NULL
422
456
  isNotNull(column); // column IS NOT NULL
423
457
  ```
@@ -425,6 +459,8 @@ isNotNull(column); // column IS NOT NULL
425
459
  ### Array
426
460
 
427
461
  ```ts
462
+ import { isIn, isNotIn } from 'flint-orm/expressions';
463
+
428
464
  isIn(column, values); // column IN (?, ?, ...)
429
465
  isNotIn(column, values); // column NOT IN (?, ?, ...)
430
466
  ```
@@ -432,6 +468,8 @@ isNotIn(column, values); // column NOT IN (?, ?, ...)
432
468
  ### Pattern Matching
433
469
 
434
470
  ```ts
471
+ import { like, glob } from 'flint-orm/expressions';
472
+
435
473
  like(column, pattern); // column LIKE ? (% and _ wildcards, case-insensitive)
436
474
  glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
437
475
  ```
@@ -439,181 +477,140 @@ glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
439
477
  ### Logical
440
478
 
441
479
  ```ts
480
+ import { and, or } from 'flint-orm/expressions';
481
+
442
482
  and(...conditions); // cond1 AND cond2 AND ...
443
483
  or(...conditions); // (cond1 OR cond2 OR ...)
444
484
  ```
445
485
 
446
- ### Examples
447
-
448
- ```ts
449
- import { eq, and, or, gt, isIn, like, between } from "flint-orm";
450
-
451
- // Simple equality
452
- .where(eq(users.name, "Alice"))
453
-
454
- // Column-to-column
455
- .where(eq(orders.userId, users.id))
456
-
457
- // Multiple conditions
458
- .where(and(eq(users.active, true), gt(users.age, 18)))
459
-
460
- // OR
461
- .where(or(eq(users.name, "Alice"), eq(users.name, "Bob")))
462
-
463
- // IN
464
- .where(isIn(users.status, ["active", "pending"]))
465
-
466
- // LIKE
467
- .where(like(users.email, "%@example.com"))
468
-
469
- // BETWEEN
470
- .where(between(users.age, 18, 65))
471
- ```
472
-
473
486
  ---
474
487
 
475
488
  ## Aggregates
476
489
 
477
- Aggregate functions are methods on the `db` object. They execute immediately and return a value.
478
-
479
- ### `db.count(table, condition?)`
480
-
481
- Count all rows.
490
+ Aggregate functions are methods on the `db` object. They return `Promise<T>`.
482
491
 
483
492
  ```ts
484
- db.count(users); // 150
485
- db.count(users, eq(users.active, true)); // 120
493
+ const total = await db.count(users);
494
+ const active = await db.count(users, eq(users.active, true));
495
+ const totalViews = await db.sum(posts, posts.views);
496
+ const avgAge = await db.avg(users, users.age);
497
+ const minAge = await db.min(users, users.age);
498
+ const maxAge = await db.max(users, users.age);
486
499
  ```
487
500
 
488
- ### `db.countColumn(table, column, condition?)`
489
-
490
- Count non-null values in a column.
491
-
492
- ```ts
493
- db.countColumn(users, users.email); // 145 (5 users have no email)
494
- ```
501
+ ---
495
502
 
496
- ### `db.sum(table, column, condition?)`
503
+ ## Batch
497
504
 
498
- Sum of values. Returns `null` if no rows match.
505
+ Run multiple queries atomically in a single transaction.
499
506
 
500
507
  ```ts
501
- db.sum(orders, orders.total); // 45000
502
- db.sum(orders, orders.total, eq(orders.userId, 'u1')); // 1500
508
+ await db.batch([
509
+ db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }),
510
+ db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1')),
511
+ ]);
503
512
  ```
504
513
 
505
- ### `db.avg(table, column, condition?)`
506
-
507
- Average of values. Returns `null` if no rows match.
514
+ ---
508
515
 
509
- ```ts
510
- db.avg(orders, orders.total); // 300
511
- db.avg(orders, orders.total, eq(orders.userId, 'u1')); // 500
512
- ```
516
+ ## Raw SQL
513
517
 
514
- ### `db.min(table, column, condition?)`
518
+ ### `db.$run(sql, ...params)`
515
519
 
516
- Minimum value. Returns `null` if no rows match.
520
+ Execute raw SQL directly against the database.
517
521
 
518
522
  ```ts
519
- db.min(orders, orders.total); // 10
520
- db.min(orders, orders.total, eq(orders.userId, 'u1')); // 50
523
+ await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
524
+ await db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
521
525
  ```
522
526
 
523
- ### `db.max(table, column, condition?)`
527
+ ### Tagged Template SQL
524
528
 
525
- Maximum value. Returns `null` if no rows match.
529
+ Build parameterized SQL expressions with automatic placeholder handling.
526
530
 
527
531
  ```ts
528
- db.max(orders, orders.total); // 1000
529
- db.max(orders, orders.total, eq(orders.userId, 'u1')); // 800
530
- ```
531
-
532
- ### Multiple Aggregates
532
+ import { sql } from 'flint-orm';
533
533
 
534
- Use `Promise.all` when you need multiple aggregates:
534
+ const expr = sql`name = ${'Alice'} AND age > ${18}`;
535
+ // { sql: "name = ? AND age > ?", params: ["Alice", 18] }
535
536
 
536
- ```ts
537
- const [total, revenue, avgOrder] = await Promise.all([db.count(orders), db.sum(orders, orders.total), db.avg(orders, orders.total)]);
537
+ const result = await db.select().from(users).where(expr).execute();
538
538
  ```
539
539
 
540
540
  ---
541
541
 
542
- ## Batch
543
-
544
- Run multiple queries atomically in a single transaction.
545
-
546
- ```ts
547
- import { flint, table, text, eq } from 'flint-orm';
542
+ ## Migration System
548
543
 
549
- const db = flint({ url: 'app.db' });
544
+ ### CLI
550
545
 
551
- db.batch([db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1'))]);
546
+ ```bash
547
+ flint generate --name init_schema # Generate migration
548
+ flint generate --preview # Preview SQL without writing
549
+ flint migrate # Apply pending migrations
550
+ flint migrate --status # Show applied vs pending
551
+ flint migrate --dry-run # Preview without executing
552
552
  ```
553
553
 
554
- All queries succeed or all roll back.
555
-
556
- ---
557
-
558
- ## Raw SQL
559
-
560
- Access the underlying `bun:sqlite` client directly for raw queries.
554
+ ### Programmatic API
561
555
 
562
556
  ```ts
563
- // Simple query
564
- const users = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
565
-
566
- // With type annotation
567
- const rows = db.$client.prepare('SELECT count(*) as cnt FROM users').all() as { cnt: number }[];
568
- ```
557
+ import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
569
558
 
570
- ---
559
+ // Serialize table definitions to JSON
560
+ const state = serializeSchema([users, orders]);
571
561
 
572
- ## Tagged Template SQL
562
+ // Diff two schema states
563
+ const operations = diffSchemas(previousState, currentState);
573
564
 
574
- Build parameterized SQL expressions with automatic placeholder handling.
565
+ // Generate SQL from operations
566
+ const sql = generateSQL(operations);
575
567
 
576
- ```ts
577
- import { sql } from 'flint-orm';
568
+ // Generate a migration folder
569
+ const result = await generate([users, orders], './flint', { name: 'init_schema', interactive: true });
578
570
 
579
- const expr = sql`SELECT * FROM users WHERE name = ${'Alice'} AND age > ${18}`;
580
- // { sql: "SELECT * FROM users WHERE name = ? AND age > ?", params: ["Alice", 18] }
571
+ // Apply pending migrations
572
+ const result = await migrate(executor, { migrationsDir: './flint' });
581
573
 
582
- // Execute with db.$client
583
- const rows = db.$client.prepare(expr.sql).all(...expr.params);
574
+ // Check migration status
575
+ const status = await getMigrationStatus(executor, './flint');
584
576
  ```
585
577
 
586
- ---
587
-
588
- ## Escape Hatch
589
-
590
- Access the underlying `bun:sqlite` client directly.
578
+ ### Migration Operations
591
579
 
592
580
  ```ts
593
- db.$client; // Database instance
581
+ import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
594
582
  ```
595
583
 
584
+ | Operation | SQL Generated |
585
+ | -------------- | -------------------------------------- |
586
+ | `addTable` | `CREATE TABLE ...` |
587
+ | `dropTable` | `DROP TABLE ...` |
588
+ | `renameTable` | `ALTER TABLE ... RENAME TO ...` |
589
+ | `addColumn` | `ALTER TABLE ... ADD COLUMN ...` |
590
+ | `dropColumn` | `ALTER TABLE ... DROP COLUMN ...` |
591
+ | `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
592
+ | `createIndex` | `CREATE [UNIQUE] INDEX ...` |
593
+ | `dropIndex` | `DROP INDEX ...` |
594
+
596
595
  ---
597
596
 
598
597
  ## Types
599
598
 
600
- | Type | Description |
601
- | ------------------- | ------------------------------------------------------------- |
602
- | `TableDef<T>` | Table definition with hidden `._` metadata |
603
- | `ColumnDef<T, S>` | Column definition with phantom type `T` and storage class `S` |
604
- | `InferRow<T>` | Derives row type from table definition |
605
- | `InsertRow<T>` | Derives insert type (defaults are optional) |
606
- | `Condition` | Condition node for WHERE clauses |
607
- | `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
608
- | `ConnectionDetails` | `{ url: string }` |
609
- | `SQLExpression` | `{ sql: string; params: unknown[] }` |
599
+ | Type | Description |
600
+ | ----------------- | ----------------------------------------------------------------------------------------- |
601
+ | `TableDef<T>` | Table definition with hidden `._` metadata |
602
+ | `ColumnDef<T, S>` | Column definition with phantom types |
603
+ | `InferRow<T>` | Derives row type from table definition |
604
+ | `InsertRow<T>` | Derives insert type (defaults are optional) |
605
+ | `Executor` | Database executor interface (all, get, run, transaction) |
606
+ | `SQLExpression` | `{ sql: string; params: unknown[] }` |
607
+ | `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
608
+ | `Driver` | `'bun-sqlite' \| 'better-sqlite3' \| 'libsql' \| 'libsql-web' \| 'turso' \| 'turso-sync'` |
610
609
 
611
610
  ---
612
611
 
613
612
  ## Error Classes
614
613
 
615
- Prefixed with `Flint` to avoid collisions in consumer codebases.
616
-
617
614
  | Class | When |
618
615
  | ---------------------- | ----------------------------------------------------------------- |
619
616
  | `FlintValidationError` | Invalid query construction (e.g., no primary key for `.single()`) |