flint-orm 0.3.0 → 0.4.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.
Files changed (5) hide show
  1. package/API.md +158 -317
  2. package/README.md +239 -134
  3. package/dist/index.js +288 -255
  4. package/package.json +43 -12
  5. package/src/cli.ts +0 -314
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,41 +90,43 @@ 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
86
111
  .default('hello') // DEFAULT 'hello'
87
112
  .defaultFn(() => new Date()) // DEFAULT (computed at insert)
88
- .references(otherColumn); // REFERENCES
113
+ .references(otherColumn) // REFERENCES otherColumn
114
+ .onDelete('cascade') // ON DELETE CASCADE
115
+ .onUpdate('set null'); // ON UPDATE SET NULL
89
116
  ```
90
117
 
91
118
  **Integer-only:**
92
119
 
93
120
  ```ts
94
- integer('id').autoIncrement(); // AUTOINCREMENT
121
+ integer().autoIncrement(); // AUTOINCREMENT
95
122
  ```
96
123
 
97
124
  **Date-only:**
98
125
 
99
126
  ```ts
100
- date('created_at')
127
+ date()
101
128
  .defaultNow() // DEFAULT (current epoch ms)
102
- .onUpdate(); // Always set to now on UPDATE
129
+ .onUpdateTimestamp(); // Always set to now on UPDATE
103
130
  ```
104
131
 
105
132
  ### `InferRow<T>`
@@ -107,8 +134,10 @@ date('created_at')
107
134
  Derives the row type from a table definition.
108
135
 
109
136
  ```ts
137
+ import type { InferRow } from 'flint-orm/table';
138
+
110
139
  type UserRow = InferRow<typeof users>;
111
- // { id: string; name: string; email: string | null; active: boolean; age: number | null }
140
+ // { id: string; name: string; email: string | null; active: boolean; age: number | null; createdAt: Date }
112
141
  ```
113
142
 
114
143
  ### `InsertRow<T>`
@@ -116,8 +145,10 @@ type UserRow = InferRow<typeof users>;
116
145
  Row type for INSERT. Columns with defaults or autoIncrement are optional.
117
146
 
118
147
  ```ts
148
+ import type { InsertRow } from 'flint-orm/table';
149
+
119
150
  type UserInsert = InsertRow<typeof users>;
120
- // { id: string; name: string; email?: string; active?: boolean; age?: number }
151
+ // { id: string; name: string; email?: string; active?: boolean; age?: number; createdAt?: Date }
121
152
  ```
122
153
 
123
154
  ---
@@ -132,26 +163,26 @@ Define indexes via the table callback. Chainable API.
132
163
  const users = table(
133
164
  'users',
134
165
  {
135
- id: text('id').primaryKey(),
136
- email: text('email'),
137
- name: text('name'),
166
+ id: text().primaryKey(),
167
+ email: text(),
168
+ name: text(),
138
169
  },
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)],
170
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
140
171
  );
141
172
  ```
142
173
 
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
174
  ---
146
175
 
147
176
  ## Query Builder
148
177
 
178
+ All `execute()` methods return `Promise<T>` — always `await` regardless of driver.
179
+
149
180
  ### `db.select().from(table)`
150
181
 
151
182
  Start a SELECT query. Two-phase: `.from()` is required before anything else.
152
183
 
153
184
  ```ts
154
- db.select().from(users).execute();
185
+ await db.select().from(users).execute();
155
186
  // SELECT * FROM users
156
187
  ```
157
188
 
@@ -160,7 +191,7 @@ db.select().from(users).execute();
160
191
  Narrow which columns appear in the result.
161
192
 
162
193
  ```ts
163
- db.select().from(users).columns(['id', 'name']).execute();
194
+ await db.select().from(users).columns(['id', 'name']).execute();
164
195
  // SELECT id, name FROM users
165
196
  // Returns: { id: string; name: string }[]
166
197
  ```
@@ -170,7 +201,7 @@ db.select().from(users).columns(['id', 'name']).execute();
170
201
  Filter rows.
171
202
 
172
203
  ```ts
173
- db.select().from(users).where(eq(users.active, true)).execute();
204
+ await db.select().from(users).where(eq(users.active, true)).execute();
174
205
  // SELECT * FROM users WHERE active = 1
175
206
  ```
176
207
 
@@ -179,7 +210,7 @@ db.select().from(users).where(eq(users.active, true)).execute();
179
210
  Return one row or null instead of an array. Adds `LIMIT 1`.
180
211
 
181
212
  ```ts
182
- db.select().from(users).where(eq(users.id, 'u1')).single().execute();
213
+ await db.select().from(users).where(eq(users.id, 'u1')).single().execute();
183
214
  // SELECT * FROM users WHERE id = ? LIMIT 1
184
215
  // Returns: UserRow | null
185
216
  ```
@@ -189,7 +220,7 @@ db.select().from(users).where(eq(users.id, 'u1')).single().execute();
189
220
  Sort results. Default direction is `"asc"`.
190
221
 
191
222
  ```ts
192
- db.select().from(users).orderBy('name', 'desc').execute();
223
+ await db.select().from(users).orderBy('name', 'desc').execute();
193
224
  // SELECT * FROM users ORDER BY name DESC
194
225
  ```
195
226
 
@@ -198,7 +229,7 @@ db.select().from(users).orderBy('name', 'desc').execute();
198
229
  Limit the number of results.
199
230
 
200
231
  ```ts
201
- db.select().from(users).limit(10).execute();
232
+ await db.select().from(users).limit(10).execute();
202
233
  // SELECT * FROM users LIMIT 10
203
234
  ```
204
235
 
@@ -207,7 +238,7 @@ db.select().from(users).limit(10).execute();
207
238
  Skip N rows.
208
239
 
209
240
  ```ts
210
- db.select().from(users).limit(10).offset(20).execute();
241
+ await db.select().from(users).limit(10).offset(20).execute();
211
242
  // SELECT * FROM users LIMIT 10 OFFSET 20
212
243
  ```
213
244
 
@@ -216,17 +247,10 @@ db.select().from(users).limit(10).offset(20).execute();
216
247
  Return unique rows.
217
248
 
218
249
  ```ts
219
- db.select().from(users).columns(['name']).distinct().execute();
250
+ await db.select().from(users).columns(['name']).distinct().execute();
220
251
  // SELECT DISTINCT name FROM users
221
252
  ```
222
253
 
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
254
  ---
231
255
 
232
256
  ### `db.insert(table).values(row)`
@@ -235,23 +259,16 @@ Insert one or more rows. Two-phase: `.values()` is required before `.execute()`.
235
259
 
236
260
  ```ts
237
261
  // Single row
238
- db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
239
- // INSERT INTO users (id, name, email) VALUES (?, ?, ?)
262
+ await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
240
263
 
241
- // Multiple rows (bulk insert)
242
- db.insert(users)
264
+ // Multiple rows
265
+ await db
266
+ .insert(users)
243
267
  .values([
244
268
  { id: 'u1', name: 'Alice' },
245
269
  { id: 'u2', name: 'Bob' },
246
270
  ])
247
271
  .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
272
  ```
256
273
 
257
274
  ### `.returning()`
@@ -259,12 +276,10 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).execute();
259
276
  Return the inserted row(s) instead of void. Pass an array to narrow which columns are returned.
260
277
 
261
278
  ```ts
262
- // Return all columns
263
- const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
279
+ const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
264
280
  // Returns: { id: string; name: string; ... }[]
265
281
 
266
- // Return specific columns
267
- const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute();
282
+ const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute();
268
283
  // Returns: { id: string; name: string }[]
269
284
  ```
270
285
 
@@ -273,8 +288,7 @@ const user = db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id
273
288
  Skip the insert if a row with the same primary key already exists.
274
289
 
275
290
  ```ts
276
- db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
277
- // INSERT OR IGNORE INTO users ...
291
+ await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
278
292
  ```
279
293
 
280
294
  ### `.onConflictDoUpdate()`
@@ -282,14 +296,14 @@ db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execu
282
296
  Update specific columns when a row with the same primary key already exists (upsert).
283
297
 
284
298
  ```ts
285
- db.insert(users)
299
+ await db
300
+ .insert(users)
286
301
  .values({ id: 'u1', name: 'Alice' })
287
302
  .onConflictDoUpdate({
288
303
  target: users.id,
289
304
  set: { name: 'Alice Updated' },
290
305
  })
291
306
  .execute();
292
- // INSERT INTO users ... ON CONFLICT (id) DO UPDATE SET name = excluded.name
293
307
  ```
294
308
 
295
309
  ---
@@ -299,29 +313,22 @@ db.insert(users)
299
313
  Update rows. Two-phase: `.set()` is required before `.execute()`.
300
314
 
301
315
  ```ts
302
- db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
303
- // UPDATE users SET name = ? WHERE id = ?
316
+ await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
304
317
  ```
305
318
 
306
319
  Multiple `.set()` calls merge:
307
320
 
308
321
  ```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 = ?
322
+ await db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute();
311
323
  ```
312
324
 
313
325
  ### `.returning()`
314
326
 
315
- Return the updated row(s) instead of void. Pass an array to narrow which columns are returned.
327
+ Return the updated row(s) instead of void.
316
328
 
317
329
  ```ts
318
- // Return all columns
319
- const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
330
+ const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
320
331
  // 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
332
  ```
326
333
 
327
334
  ---
@@ -331,22 +338,16 @@ const updated = db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).
331
338
  Delete rows.
332
339
 
333
340
  ```ts
334
- db.delete(users).where(eq(users.id, 'u1')).execute();
335
- // DELETE FROM users WHERE id = ?
341
+ await db.delete(users).where(eq(users.id, 'u1')).execute();
336
342
  ```
337
343
 
338
344
  ### `.returning()`
339
345
 
340
- Return the deleted row(s) instead of void. Pass an array to narrow which columns are returned.
346
+ Return the deleted row(s) instead of void.
341
347
 
342
348
  ```ts
343
- // Return all columns
344
- const deleted = db.delete(users).where(eq(users.id, 'u1')).returning().execute();
349
+ const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute();
345
350
  // 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
351
  ```
351
352
 
352
353
  ---
@@ -355,20 +356,20 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
355
356
 
356
357
  ### `db.leftJoin(parent).on(child, condition?)`
357
358
 
358
- LEFT JOIN. Returns all parent rows, with matching child data nested under `__children`.
359
+ LEFT JOIN. Returns all parent rows, with matching child data nested under the child table name.
359
360
 
360
361
  ```ts
361
362
  const orders = table('orders', {
362
- id: text('id').primaryKey(),
363
- userId: text('userId').notNull().references(users.id),
364
- total: integer('total').notNull(),
363
+ id: text().primaryKey(),
364
+ userId: text().notNull().references(users.id),
365
+ total: integer().notNull(),
365
366
  });
366
367
 
367
368
  // With explicit condition
368
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).execute();
369
+ await db.leftJoin(users).on(orders, eq(orders.userId, users.id)).execute();
369
370
 
370
371
  // Auto-join from foreign key (if .references() is defined)
371
- db.leftJoin(orders).on(users).execute();
372
+ await db.leftJoin(users).on(orders).execute();
372
373
  ```
373
374
 
374
375
  ### `db.innerJoin(parent).on(child, condition?)`
@@ -376,7 +377,7 @@ db.leftJoin(orders).on(users).execute();
376
377
  INNER JOIN. Returns only rows where both tables match.
377
378
 
378
379
  ```ts
379
- db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
380
+ await db.innerJoin(users).on(orders, eq(orders.userId, users.id)).execute();
380
381
  ```
381
382
 
382
383
  ### Join Result Shape
@@ -384,12 +385,11 @@ db.innerJoin(orders).on(users, eq(orders.userId, users.id)).execute();
384
385
  Joins return **nested** results, not flat-merged:
385
386
 
386
387
  ```ts
387
- // One-to-many: one user with multiple orders
388
388
  [
389
389
  {
390
390
  id: 'u1',
391
391
  name: 'Alice',
392
- __children: [
392
+ orders: [
393
393
  { id: 'o1', userId: 'u1', total: 100 },
394
394
  { id: 'o2', userId: 'u1', total: 200 },
395
395
  ],
@@ -402,15 +402,15 @@ Joins return **nested** results, not flat-merged:
402
402
  Narrow parent columns with `.columns()`:
403
403
 
404
404
  ```ts
405
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
406
- // Returns: { id: string; name: string; __children: OrderRow[] }[]
405
+ await db.leftJoin(users).on(orders).columns(['id', 'name']).execute();
406
+ // Returns: { id: string; name: string; orders: OrderRow[] }[]
407
407
  ```
408
408
 
409
409
  ### `.single()` on Joins
410
410
 
411
411
  ```ts
412
- db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
413
- // Returns: { id: string; name: string; __children: OrderRow[] } | null
412
+ await db.leftJoin(users).on(orders).single().execute();
413
+ // Returns: { id: string; name: string; orders: OrderRow[] } | null
414
414
  ```
415
415
 
416
416
  ### Multi-Join
@@ -418,19 +418,20 @@ db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
418
418
  Chain multiple joins:
419
419
 
420
420
  ```ts
421
- db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
422
- // Returns nested: { ...userFields, __children: [{ ...orderFields, __children: [...items] }] }
421
+ await db.leftJoin(users).on(orders).leftJoin(orders).on(orderItems).execute();
423
422
  ```
424
423
 
425
424
  ---
426
425
 
427
426
  ## Conditions
428
427
 
429
- All conditions are imported from `flint-orm`.
428
+ All conditions are imported from `flint-orm/expressions`.
430
429
 
431
430
  ### Comparison
432
431
 
433
432
  ```ts
433
+ import { eq, neq, gt, gte, lt, lte } from 'flint-orm/expressions';
434
+
434
435
  eq(column, value); // column = value
435
436
  eq(left, right); // left = right (column-to-column)
436
437
  neq(column, value); // column != value
@@ -443,12 +444,16 @@ lte(column, value); // column <= value
443
444
  ### Range
444
445
 
445
446
  ```ts
447
+ import { between } from 'flint-orm/expressions';
448
+
446
449
  between(column, low, high); // column BETWEEN low AND high
447
450
  ```
448
451
 
449
452
  ### Null Checks
450
453
 
451
454
  ```ts
455
+ import { isNull, isNotNull } from 'flint-orm/expressions';
456
+
452
457
  isNull(column); // column IS NULL
453
458
  isNotNull(column); // column IS NOT NULL
454
459
  ```
@@ -456,6 +461,8 @@ isNotNull(column); // column IS NOT NULL
456
461
  ### Array
457
462
 
458
463
  ```ts
464
+ import { isIn, isNotIn } from 'flint-orm/expressions';
465
+
459
466
  isIn(column, values); // column IN (?, ?, ...)
460
467
  isNotIn(column, values); // column NOT IN (?, ?, ...)
461
468
  ```
@@ -463,6 +470,8 @@ isNotIn(column, values); // column NOT IN (?, ?, ...)
463
470
  ### Pattern Matching
464
471
 
465
472
  ```ts
473
+ import { like, glob } from 'flint-orm/expressions';
474
+
466
475
  like(column, pattern); // column LIKE ? (% and _ wildcards, case-insensitive)
467
476
  glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
468
477
  ```
@@ -470,102 +479,25 @@ glob(column, pattern); // column GLOB ? (* and ? wildcards, case-sensitive)
470
479
  ### Logical
471
480
 
472
481
  ```ts
482
+ import { and, or } from 'flint-orm/expressions';
483
+
473
484
  and(...conditions); // cond1 AND cond2 AND ...
474
485
  or(...conditions); // (cond1 OR cond2 OR ...)
475
486
  ```
476
487
 
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
488
  ---
505
489
 
506
490
  ## Aggregates
507
491
 
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.
492
+ Aggregate functions are methods on the `db` object. They return `Promise<T>`.
513
493
 
514
494
  ```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)]);
495
+ const total = await db.count(users);
496
+ const active = await db.count(users, eq(users.active, true));
497
+ const totalViews = await db.sum(posts, posts.views);
498
+ const avgAge = await db.avg(users, users.age);
499
+ const minAge = await db.min(users, users.age);
500
+ const maxAge = await db.max(users, users.age);
569
501
  ```
570
502
 
571
503
  ---
@@ -575,15 +507,12 @@ const [total, revenue, avgOrder] = await Promise.all([db.count(orders), db.sum(o
575
507
  Run multiple queries atomically in a single transaction.
576
508
 
577
509
  ```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'))]);
510
+ await db.batch([
511
+ db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }),
512
+ db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1')),
513
+ ]);
583
514
  ```
584
515
 
585
- All queries succeed or all roll back.
586
-
587
516
  ---
588
517
 
589
518
  ## Raw SQL
@@ -593,76 +522,35 @@ All queries succeed or all roll back.
593
522
  Execute raw SQL directly against the database.
594
523
 
595
524
  ```ts
596
- db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
597
- db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
525
+ await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
526
+ await db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
598
527
  ```
599
528
 
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
529
+ ### Tagged Template SQL
611
530
 
612
531
  Build parameterized SQL expressions with automatic placeholder handling.
613
532
 
614
533
  ```ts
615
534
  import { sql } from 'flint-orm';
616
535
 
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] }
536
+ const expr = sql`name = ${'Alice'} AND age > ${18}`;
537
+ // { sql: "name = ? AND age > ?", params: ["Alice", 18] }
619
538
 
620
- // Use with db.$client
621
- const rows = db.$client.prepare(expr.sql).all(...expr.params);
539
+ const result = await db.select().from(users).where(expr).execute();
622
540
  ```
623
541
 
624
542
  ---
625
543
 
626
544
  ## Migration System
627
545
 
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.
546
+ ### CLI
643
547
 
644
548
  ```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
- });
549
+ flint generate --name init_schema # Generate migration
550
+ flint generate --preview # Preview SQL without writing
551
+ flint migrate # Apply pending migrations
552
+ flint migrate --status # Show applied vs pending
553
+ flint migrate --dry-run # Preview without executing
666
554
  ```
667
555
 
668
556
  ### Programmatic API
@@ -680,19 +568,17 @@ const operations = diffSchemas(previousState, currentState);
680
568
  const sql = generateSQL(operations);
681
569
 
682
570
  // Generate a migration folder
683
- const result = generate([users, orders], './flint', 'init_schema');
571
+ const result = await generate([users, orders], './flint', { name: 'init_schema', interactive: true });
684
572
 
685
573
  // Apply pending migrations
686
- const result = migrate({ url: './app.db' }, './flint');
574
+ const result = await migrate(executor, { migrationsDir: './flint' });
687
575
 
688
576
  // Check migration status
689
- const status = getMigrationStatus(client, './flint');
577
+ const status = await getMigrationStatus(executor, './flint');
690
578
  ```
691
579
 
692
580
  ### Migration Operations
693
581
 
694
- Named, pre-vetted operations:
695
-
696
582
  ```ts
697
583
  import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
698
584
  ```
@@ -707,75 +593,30 @@ import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn,
707
593
  | `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
708
594
  | `createIndex` | `CREATE [UNIQUE] INDEX ...` |
709
595
  | `dropIndex` | `DROP INDEX ...` |
710
-
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.
596
+ | `modifyColumn` | `ALTER TABLE ... ALTER COLUMN ...` |
597
+ | `modifyIndex` | `DROP INDEX IF EXISTS ...; CREATE ...` |
598
+ | `rebuildTable` | Temp table → copy → drop → rename |
755
599
 
756
600
  ---
757
601
 
758
602
  ## Types
759
603
 
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[] }` |
604
+ | Type | Description |
605
+ | ----------------- | ----------------------------------------------------------------------------------------- |
606
+ | `TableDef<T>` | Table definition with hidden `._` metadata |
607
+ | `ColumnDef<T, S>` | Column definition with phantom types |
608
+ | `InferRow<T>` | Derives row type from table definition |
609
+ | `InsertRow<T>` | Derives insert type (defaults are optional) |
610
+ | `Executor` | Database executor interface (all, get, run, transaction) |
611
+ | `SQLExpression` | `{ sql: string; params: unknown[] }` |
612
+ | `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
613
+ | `Driver` | `'bun-sqlite' \| 'better-sqlite3' \| 'libsql' \| 'libsql-web' \| 'turso' \| 'turso-sync'` |
614
+ | `RebuildTableOp` | Migration op that recreates a table with a new schema |
772
615
 
773
616
  ---
774
617
 
775
618
  ## Error Classes
776
619
 
777
- Prefixed with `Flint` to avoid collisions in consumer codebases.
778
-
779
620
  | Class | When |
780
621
  | ---------------------- | ----------------------------------------------------------------- |
781
622
  | `FlintValidationError` | Invalid query construction (e.g., no primary key for `.single()`) |