flint-orm 0.3.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 +151 -316
  2. package/README.md +237 -134
  3. package/dist/index.js +288 -255
  4. package/package.json +42 -11
  5. package/src/cli.ts +58 -11
package/API.md CHANGED
@@ -1,32 +1,57 @@
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
  ---
@@ -38,14 +63,14 @@ const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute(
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, index } 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
 
@@ -54,7 +79,7 @@ const users = table('users', {
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,8 +143,10 @@ 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 }
121
150
  ```
122
151
 
123
152
  ---
@@ -132,26 +161,26 @@ Define indexes via the table callback. Chainable API.
132
161
  const users = table(
133
162
  'users',
134
163
  {
135
- id: text('id').primaryKey(),
136
- email: text('email'),
137
- name: text('name'),
164
+ id: text().primaryKey(),
165
+ email: text(),
166
+ name: text(),
138
167
  },
139
- (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name), index('idx_users_email_name').on(t.email, t.name)],
168
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
140
169
  );
141
170
  ```
142
171
 
143
- The callback receives the table definition and returns an array of `IndexBuilder` objects. Indexes are attached to the table automatically and serialized for migrations.
144
-
145
172
  ---
146
173
 
147
174
  ## Query Builder
148
175
 
176
+ All `execute()` methods return `Promise<T>` — always `await` regardless of driver.
177
+
149
178
  ### `db.select().from(table)`
150
179
 
151
180
  Start a SELECT query. Two-phase: `.from()` is required before anything else.
152
181
 
153
182
  ```ts
154
- db.select().from(users).execute();
183
+ await db.select().from(users).execute();
155
184
  // SELECT * FROM users
156
185
  ```
157
186
 
@@ -160,7 +189,7 @@ db.select().from(users).execute();
160
189
  Narrow which columns appear in the result.
161
190
 
162
191
  ```ts
163
- db.select().from(users).columns(['id', 'name']).execute();
192
+ await db.select().from(users).columns(['id', 'name']).execute();
164
193
  // SELECT id, name FROM users
165
194
  // Returns: { id: string; name: string }[]
166
195
  ```
@@ -170,7 +199,7 @@ db.select().from(users).columns(['id', 'name']).execute();
170
199
  Filter rows.
171
200
 
172
201
  ```ts
173
- db.select().from(users).where(eq(users.active, true)).execute();
202
+ await db.select().from(users).where(eq(users.active, true)).execute();
174
203
  // SELECT * FROM users WHERE active = 1
175
204
  ```
176
205
 
@@ -179,7 +208,7 @@ db.select().from(users).where(eq(users.active, true)).execute();
179
208
  Return one row or null instead of an array. Adds `LIMIT 1`.
180
209
 
181
210
  ```ts
182
- db.select().from(users).where(eq(users.id, 'u1')).single().execute();
211
+ await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
183
212
  // SELECT * FROM users WHERE id = ? LIMIT 1
184
213
  // Returns: UserRow | null
185
214
  ```
@@ -189,7 +218,7 @@ db.select().from(users).where(eq(users.id, 'u1')).single().execute();
189
218
  Sort results. Default direction is `"asc"`.
190
219
 
191
220
  ```ts
192
- db.select().from(users).orderBy('name', 'desc').execute();
221
+ await db.select().from(users).orderBy('name', 'desc').execute();
193
222
  // SELECT * FROM users ORDER BY name DESC
194
223
  ```
195
224
 
@@ -198,7 +227,7 @@ db.select().from(users).orderBy('name', 'desc').execute();
198
227
  Limit the number of results.
199
228
 
200
229
  ```ts
201
- db.select().from(users).limit(10).execute();
230
+ await db.select().from(users).limit(10).execute();
202
231
  // SELECT * FROM users LIMIT 10
203
232
  ```
204
233
 
@@ -207,7 +236,7 @@ db.select().from(users).limit(10).execute();
207
236
  Skip N rows.
208
237
 
209
238
  ```ts
210
- db.select().from(users).limit(10).offset(20).execute();
239
+ await db.select().from(users).limit(10).offset(20).execute();
211
240
  // SELECT * FROM users LIMIT 10 OFFSET 20
212
241
  ```
213
242
 
@@ -216,17 +245,10 @@ db.select().from(users).limit(10).offset(20).execute();
216
245
  Return unique rows.
217
246
 
218
247
  ```ts
219
- db.select().from(users).columns(['name']).distinct().execute();
248
+ await db.select().from(users).columns(['name']).distinct().execute();
220
249
  // SELECT DISTINCT name FROM users
221
250
  ```
222
251
 
223
- ### Full Example
224
-
225
- ```ts
226
- const results = db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name', 'asc').limit(10).offset(0).execute();
227
- // Returns: { id: string; name: string }[]
228
- ```
229
-
230
252
  ---
231
253
 
232
254
  ### `db.insert(table).values(row)`
@@ -235,23 +257,16 @@ Insert one or more rows. Two-phase: `.values()` is required before `.execute()`.
235
257
 
236
258
  ```ts
237
259
  // Single row
238
- db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
239
- // INSERT INTO users (id, name, email) VALUES (?, ?, ?)
260
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
240
261
 
241
- // Multiple rows (bulk insert)
242
- db.insert(users)
262
+ // Multiple rows
263
+ await db
264
+ .insert(users)
243
265
  .values([
244
266
  { id: 'u1', name: 'Alice' },
245
267
  { id: 'u2', name: 'Bob' },
246
268
  ])
247
269
  .execute();
248
- // INSERT INTO users (id, name) VALUES (?, ?), (?, ?)
249
- ```
250
-
251
- Columns with defaults can be omitted:
252
-
253
- ```ts
254
- db.insert(users).values({ id: 'u1', name: 'Alice' }).execute();
255
270
  ```
256
271
 
257
272
  ### `.returning()`
@@ -259,12 +274,10 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).execute();
259
274
  Return the inserted row(s) instead of void. Pass an array to narrow which columns are returned.
260
275
 
261
276
  ```ts
262
- // Return all columns
263
- 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();
264
278
  // Returns: { id: string; name: string; ... }[]
265
279
 
266
- // Return specific columns
267
- 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();
268
281
  // Returns: { id: string; name: string }[]
269
282
  ```
270
283
 
@@ -273,8 +286,7 @@ const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id
273
286
  Skip the insert if a row with the same primary key already exists.
274
287
 
275
288
  ```ts
276
- db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
277
- // INSERT OR IGNORE INTO users ...
289
+ await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
278
290
  ```
279
291
 
280
292
  ### `.onConflictDoUpdate()`
@@ -282,14 +294,14 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execu
282
294
  Update specific columns when a row with the same primary key already exists (upsert).
283
295
 
284
296
  ```ts
285
- db.insert(users)
297
+ await db
298
+ .insert(users)
286
299
  .values({ id: 'u1', name: 'Alice' })
287
300
  .onConflictDoUpdate({
288
301
  target: users.id,
289
302
  set: { name: 'Alice Updated' },
290
303
  })
291
304
  .execute();
292
- // INSERT INTO users ... ON CONFLICT (id) DO UPDATE SET name = excluded.name
293
305
  ```
294
306
 
295
307
  ---
@@ -299,29 +311,22 @@ db.insert(users)
299
311
  Update rows. Two-phase: `.set()` is required before `.execute()`.
300
312
 
301
313
  ```ts
302
- db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
303
- // UPDATE users SET name = ? WHERE id = ?
314
+ await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
304
315
  ```
305
316
 
306
317
  Multiple `.set()` calls merge:
307
318
 
308
319
  ```ts
309
- db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute();
310
- // 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();
311
321
  ```
312
322
 
313
323
  ### `.returning()`
314
324
 
315
- 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.
316
326
 
317
327
  ```ts
318
- // Return all columns
319
- 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();
320
329
  // Returns: { id: string; name: string; ... }[]
321
-
322
- // Return specific columns
323
- const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning(['id', 'name']).execute();
324
- // Returns: { id: string; name: string }[]
325
330
  ```
326
331
 
327
332
  ---
@@ -331,22 +336,16 @@ const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).
331
336
  Delete rows.
332
337
 
333
338
  ```ts
334
- db.delete(users).where(eq(users.id, 'u1')).execute();
335
- // DELETE FROM users WHERE id = ?
339
+ await db.delete(users).where(eq(users.id, 'u1')).execute();
336
340
  ```
337
341
 
338
342
  ### `.returning()`
339
343
 
340
- 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.
341
345
 
342
346
  ```ts
343
- // Return all columns
344
- 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();
345
348
  // Returns: { id: string; name: string; ... }[]
346
-
347
- // Return specific columns
348
- const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'name']).execute();
349
- // Returns: { id: string; name: string }[]
350
349
  ```
351
350
 
352
351
  ---
@@ -355,20 +354,20 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
355
354
 
356
355
  ### `db.leftJoin(parent).on(child, condition?)`
357
356
 
358
- LEFT JOIN. Returns all parent rows, with matching child data nested under `__children`.
357
+ LEFT JOIN. Returns all parent rows, with matching child data nested under the child table name.
359
358
 
360
359
  ```ts
361
360
  const orders = table('orders', {
362
- id: text('id').primaryKey(),
363
- userId: text('userId').notNull().references(users.id),
364
- total: integer('total').notNull(),
361
+ id: text().primaryKey(),
362
+ userId: text().notNull().references(users.id),
363
+ total: integer().notNull(),
365
364
  });
366
365
 
367
366
  // With explicit condition
368
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).execute();
367
+ await db.leftJoin(users).on(orders, eq(orders.userId, users.id)).execute();
369
368
 
370
369
  // Auto-join from foreign key (if .references() is defined)
371
- db.leftJoin(orders).on(users).execute();
370
+ await db.leftJoin(users).on(orders).execute();
372
371
  ```
373
372
 
374
373
  ### `db.innerJoin(parent).on(child, condition?)`
@@ -376,7 +375,7 @@ db.leftJoin(orders).on(users).execute();
376
375
  INNER JOIN. Returns only rows where both tables match.
377
376
 
378
377
  ```ts
379
- db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
378
+ await db.innerJoin(users).on(orders, eq(orders.userId, users.id)).execute();
380
379
  ```
381
380
 
382
381
  ### Join Result Shape
@@ -384,12 +383,11 @@ db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
384
383
  Joins return **nested** results, not flat-merged:
385
384
 
386
385
  ```ts
387
- // One-to-many: one user with multiple orders
388
386
  [
389
387
  {
390
388
  id: 'u1',
391
389
  name: 'Alice',
392
- __children: [
390
+ orders: [
393
391
  { id: 'o1', userId: 'u1', total: 100 },
394
392
  { id: 'o2', userId: 'u1', total: 200 },
395
393
  ],
@@ -402,15 +400,15 @@ Joins return **nested** results, not flat-merged:
402
400
  Narrow parent columns with `.columns()`:
403
401
 
404
402
  ```ts
405
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
406
- // Returns: { id: string; name: string; __children: OrderRow[] }[]
403
+ await db.leftJoin(users).on(orders).columns(['id', 'name']).execute();
404
+ // Returns: { id: string; name: string; orders: OrderRow[] }[]
407
405
  ```
408
406
 
409
407
  ### `.single()` on Joins
410
408
 
411
409
  ```ts
412
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
413
- // Returns: { id: string; name: string; __children: OrderRow[] } | null
410
+ await db.leftJoin(users).on(orders).single().execute();
411
+ // Returns: { id: string; name: string; orders: OrderRow[] } | null
414
412
  ```
415
413
 
416
414
  ### Multi-Join
@@ -418,19 +416,20 @@ db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
418
416
  Chain multiple joins:
419
417
 
420
418
  ```ts
421
- db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
422
- // Returns nested: { ...userFields, __children: [{ ...orderFields, __children: [...items] }] }
419
+ await db.leftJoin(users).on(orders).leftJoin(orders).on(orderItems).execute();
423
420
  ```
424
421
 
425
422
  ---
426
423
 
427
424
  ## Conditions
428
425
 
429
- All conditions are imported from `flint-orm`.
426
+ All conditions are imported from `flint-orm/expressions`.
430
427
 
431
428
  ### Comparison
432
429
 
433
430
  ```ts
431
+ import { eq, neq, gt, gte, lt, lte } from 'flint-orm/expressions';
432
+
434
433
  eq(column, value); // column = value
435
434
  eq(left, right); // left = right (column-to-column)
436
435
  neq(column, value); // column != value
@@ -443,12 +442,16 @@ lte(column, value); // column <= value
443
442
  ### Range
444
443
 
445
444
  ```ts
445
+ import { between } from 'flint-orm/expressions';
446
+
446
447
  between(column, low, high); // column BETWEEN low AND high
447
448
  ```
448
449
 
449
450
  ### Null Checks
450
451
 
451
452
  ```ts
453
+ import { isNull, isNotNull } from 'flint-orm/expressions';
454
+
452
455
  isNull(column); // column IS NULL
453
456
  isNotNull(column); // column IS NOT NULL
454
457
  ```
@@ -456,6 +459,8 @@ isNotNull(column); // column IS NOT NULL
456
459
  ### Array
457
460
 
458
461
  ```ts
462
+ import { isIn, isNotIn } from 'flint-orm/expressions';
463
+
459
464
  isIn(column, values); // column IN (?, ?, ...)
460
465
  isNotIn(column, values); // column NOT IN (?, ?, ...)
461
466
  ```
@@ -463,6 +468,8 @@ isNotIn(column, values); // column NOT IN (?, ?, ...)
463
468
  ### Pattern Matching
464
469
 
465
470
  ```ts
471
+ import { like, glob } from 'flint-orm/expressions';
472
+
466
473
  like(column, pattern); // column LIKE ? (% and _ wildcards, case-insensitive)
467
474
  glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
468
475
  ```
@@ -470,102 +477,25 @@ glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
470
477
  ### Logical
471
478
 
472
479
  ```ts
480
+ import { and, or } from 'flint-orm/expressions';
481
+
473
482
  and(...conditions); // cond1 AND cond2 AND ...
474
483
  or(...conditions); // (cond1 OR cond2 OR ...)
475
484
  ```
476
485
 
477
- ### Examples
478
-
479
- ```ts
480
- import { eq, and, or, gt, isIn, like, between } from "flint-orm";
481
-
482
- // Simple equality
483
- .where(eq(users.name, "Alice"))
484
-
485
- // Column-to-column
486
- .where(eq(orders.userId, users.id))
487
-
488
- // Multiple conditions
489
- .where(and(eq(users.active, true), gt(users.age, 18)))
490
-
491
- // OR
492
- .where(or(eq(users.name, "Alice"), eq(users.name, "Bob")))
493
-
494
- // IN
495
- .where(isIn(users.status, ["active", "pending"]))
496
-
497
- // LIKE
498
- .where(like(users.email, "%@example.com"))
499
-
500
- // BETWEEN
501
- .where(between(users.age, 18, 65))
502
- ```
503
-
504
486
  ---
505
487
 
506
488
  ## Aggregates
507
489
 
508
- Aggregate functions are methods on the `db` object. They execute immediately and return a value.
509
-
510
- ### `db.count(table, condition?)`
511
-
512
- Count all rows.
490
+ Aggregate functions are methods on the `db` object. They return `Promise<T>`.
513
491
 
514
492
  ```ts
515
- db.count(users); // 150
516
- db.count(users, eq(users.active, true)); // 120
517
- ```
518
-
519
- ### `db.countColumn(table, column, condition?)`
520
-
521
- Count non-null values in a column.
522
-
523
- ```ts
524
- db.countColumn(users, users.email); // 145 (5 users have no email)
525
- ```
526
-
527
- ### `db.sum(table, column, condition?)`
528
-
529
- Sum of values. Returns `null` if no rows match.
530
-
531
- ```ts
532
- db.sum(orders, orders.total); // 45000
533
- db.sum(orders, orders.total, eq(orders.userId, 'u1')); // 1500
534
- ```
535
-
536
- ### `db.avg(table, column, condition?)`
537
-
538
- Average of values. Returns `null` if no rows match.
539
-
540
- ```ts
541
- db.avg(orders, orders.total); // 300
542
- db.avg(orders, orders.total, eq(orders.userId, 'u1')); // 500
543
- ```
544
-
545
- ### `db.min(table, column, condition?)`
546
-
547
- Minimum value. Returns `null` if no rows match.
548
-
549
- ```ts
550
- db.min(orders, orders.total); // 10
551
- db.min(orders, orders.total, eq(orders.userId, 'u1')); // 50
552
- ```
553
-
554
- ### `db.max(table, column, condition?)`
555
-
556
- Maximum value. Returns `null` if no rows match.
557
-
558
- ```ts
559
- db.max(orders, orders.total); // 1000
560
- db.max(orders, orders.total, eq(orders.userId, 'u1')); // 800
561
- ```
562
-
563
- ### Multiple Aggregates
564
-
565
- Use `Promise.all` when you need multiple aggregates:
566
-
567
- ```ts
568
- const [total, revenue, avgOrder] = await Promise.all([db.count(orders), db.sum(orders, orders.total), db.avg(orders, orders.total)]);
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);
569
499
  ```
570
500
 
571
501
  ---
@@ -575,15 +505,12 @@ const [total, revenue, avgOrder] = await Promise.all([db.count(orders), db.sum(o
575
505
  Run multiple queries atomically in a single transaction.
576
506
 
577
507
  ```ts
578
- import { flint, table, text, eq } from 'flint-orm';
579
-
580
- const db = flint({ url: 'app.db' });
581
-
582
- db.batch([db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1'))]);
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
+ ]);
583
512
  ```
584
513
 
585
- All queries succeed or all roll back.
586
-
587
514
  ---
588
515
 
589
516
  ## Raw SQL
@@ -593,76 +520,35 @@ All queries succeed or all roll back.
593
520
  Execute raw SQL directly against the database.
594
521
 
595
522
  ```ts
596
- db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
597
- db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
523
+ await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
524
+ await db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
598
525
  ```
599
526
 
600
- ### `db.$client`
601
-
602
- Direct access to the underlying `bun:sqlite` client.
603
-
604
- ```ts
605
- const rows = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
606
- ```
607
-
608
- ---
609
-
610
- ## Tagged Template SQL
527
+ ### Tagged Template SQL
611
528
 
612
529
  Build parameterized SQL expressions with automatic placeholder handling.
613
530
 
614
531
  ```ts
615
532
  import { sql } from 'flint-orm';
616
533
 
617
- const expr = sql`SELECT * FROM users WHERE name = ${'Alice'} AND age > ${18}`;
618
- // { sql: "SELECT * FROM users WHERE name = ? AND age > ?", params: ["Alice", 18] }
534
+ const expr = sql`name = ${'Alice'} AND age > ${18}`;
535
+ // { sql: "name = ? AND age > ?", params: ["Alice", 18] }
619
536
 
620
- // Use with db.$client
621
- const rows = db.$client.prepare(expr.sql).all(...expr.params);
537
+ const result = await db.select().from(users).where(expr).execute();
622
538
  ```
623
539
 
624
540
  ---
625
541
 
626
542
  ## Migration System
627
543
 
628
- ### `flint generate` (CLI)
629
-
630
- Generate a migration from schema changes.
631
-
632
- ```bash
633
- # Generate migration
634
- flint generate --name init_schema
635
-
636
- # Preview SQL without writing files
637
- flint generate --name init_schema --preview
638
- ```
639
-
640
- ### `flint migrate` (CLI)
641
-
642
- Apply pending migrations to the database.
544
+ ### CLI
643
545
 
644
546
  ```bash
645
- # Apply all pending migrations
646
- flint migrate
647
-
648
- # Show which migrations are pending/applied
649
- flint migrate --status
650
- ```
651
-
652
- ### Config
653
-
654
- Create `flint.config.ts` in your project root:
655
-
656
- ```ts
657
- import { defineConfig } from 'flint-orm/config';
658
-
659
- export default defineConfig({
660
- schema: './src/schema', // folder or file with table() definitions
661
- migrations: './flint', // where migration folders are stored
662
- database: {
663
- url: './app.db', // SQLite database path
664
- },
665
- });
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
666
552
  ```
667
553
 
668
554
  ### Programmatic API
@@ -680,19 +566,17 @@ const operations = diffSchemas(previousState, currentState);
680
566
  const sql = generateSQL(operations);
681
567
 
682
568
  // Generate a migration folder
683
- const result = generate([users, orders], './flint', 'init_schema');
569
+ const result = await generate([users, orders], './flint', { name: 'init_schema', interactive: true });
684
570
 
685
571
  // Apply pending migrations
686
- const result = migrate({ url: './app.db' }, './flint');
572
+ const result = await migrate(executor, { migrationsDir: './flint' });
687
573
 
688
574
  // Check migration status
689
- const status = getMigrationStatus(client, './flint');
575
+ const status = await getMigrationStatus(executor, './flint');
690
576
  ```
691
577
 
692
578
  ### Migration Operations
693
579
 
694
- Named, pre-vetted operations:
695
-
696
580
  ```ts
697
581
  import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
698
582
  ```
@@ -708,74 +592,25 @@ import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn,
708
592
  | `createIndex` | `CREATE [UNIQUE] INDEX ...` |
709
593
  | `dropIndex` | `DROP INDEX ...` |
710
594
 
711
- ### Migration File Shape
712
-
713
- ```ts
714
- import { defineMigration } from 'flint-orm/migration';
715
- import { addTable, addColumn } from 'flint-orm/migration';
716
-
717
- export default defineMigration({
718
- name: 'init_schema',
719
- operations: [
720
- addTable({ name: 'users', columns: [...], indexes: [...] }),
721
- addColumn('users', { name: 'email', sqlType: 'text', ... }),
722
- ],
723
- });
724
- ```
725
-
726
- ### Tracking
727
-
728
- Applied migrations are recorded in `__flint_migrations` (per-database). The table is created on first `migrate()` call — never with `IF NOT EXISTS`.
729
-
730
- ### FK Ordering
731
-
732
- Tables are topologically sorted (Kahn's algorithm) before generating `CREATE TABLE` statements. Referenced tables are created first.
733
-
734
- ---
735
-
736
- ## SQLite Introspection
737
-
738
- ### `introspectSchema(client)`
739
-
740
- Read the live database schema and return a `SchemaState` that can be diffed against code-defined tables.
741
-
742
- ```ts
743
- import { introspectSchema } from 'flint-orm/sqlite';
744
- import { diffSchemas } from 'flint-orm/migration';
745
-
746
- const client = new Database('./app.db');
747
- const liveState = introspectSchema(client);
748
-
749
- // Compare live DB against code schema
750
- const codeState = serializeSchema([users, orders]);
751
- const operations = diffSchemas(liveState, codeState);
752
- ```
753
-
754
- Normalizes SQLite type aliases (VARCHAR → TEXT, BIGINT → INTEGER, etc.) and parses default values.
755
-
756
595
  ---
757
596
 
758
597
  ## Types
759
598
 
760
- | Type | Description |
761
- | ------------------- | ------------------------------------------------------------- |
762
- | `TableDef<T>` | Table definition with hidden `._` metadata |
763
- | `ColumnDef<T, S>` | Column definition with phantom type `T` and storage class `S` |
764
- | `InferRow<T>` | Derives row type from table definition |
765
- | `InsertRow<T>` | Derives insert type (defaults are optional) |
766
- | `IndexDef` | Index definition: `{ name, columns, unique }` |
767
- | `IndexBuilder` | Chainable index builder: `.on(cols).unique()` |
768
- | `Condition` | Condition node for WHERE clauses |
769
- | `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
770
- | `ConnectionDetails` | `{ url: string }` |
771
- | `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'` |
772
609
 
773
610
  ---
774
611
 
775
612
  ## Error Classes
776
613
 
777
- Prefixed with `Flint` to avoid collisions in consumer codebases.
778
-
779
614
  | Class | When |
780
615
  | ---------------------- | ----------------------------------------------------------------- |
781
616
  | `FlintValidationError` | Invalid query construction (e.g., no primary key for `.single()`) |