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.
- package/API.md +151 -316
- package/README.md +237 -134
- package/dist/index.js +288 -255
- package/package.json +42 -11
- package/src/cli.ts +58 -11
package/API.md
CHANGED
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
# Flint ORM — API Reference
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
|
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(
|
|
19
|
-
name: text(
|
|
20
|
-
email: text(
|
|
21
|
-
age: integer(
|
|
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(
|
|
45
|
-
name: text(
|
|
46
|
-
email: text(
|
|
47
|
-
active: boolean(
|
|
48
|
-
age: integer(
|
|
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()`, `.
|
|
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(
|
|
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(
|
|
119
|
+
integer().autoIncrement(); // AUTOINCREMENT
|
|
95
120
|
```
|
|
96
121
|
|
|
97
122
|
**Date-only:**
|
|
98
123
|
|
|
99
124
|
```ts
|
|
100
|
-
date(
|
|
125
|
+
date()
|
|
101
126
|
.defaultNow() // DEFAULT (current epoch ms)
|
|
102
|
-
.
|
|
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(
|
|
136
|
-
email: text(
|
|
137
|
-
name: text(
|
|
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)
|
|
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
|
|
242
|
-
db
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
325
|
+
Return the updated row(s) instead of void.
|
|
316
326
|
|
|
317
327
|
```ts
|
|
318
|
-
|
|
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.
|
|
344
|
+
Return the deleted row(s) instead of void.
|
|
341
345
|
|
|
342
346
|
```ts
|
|
343
|
-
|
|
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
|
|
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(
|
|
363
|
-
userId: text(
|
|
364
|
-
total: integer(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
406
|
-
// Returns: { id: string; name: string;
|
|
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(
|
|
413
|
-
// Returns: { id: string; name: string;
|
|
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(
|
|
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
|
|
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);
|
|
516
|
-
db.count(users, eq(users.active, true));
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
###
|
|
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`
|
|
618
|
-
// { sql: "
|
|
534
|
+
const expr = sql`name = ${'Alice'} AND age > ${18}`;
|
|
535
|
+
// { sql: "name = ? AND age > ?", params: ["Alice", 18] }
|
|
619
536
|
|
|
620
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
646
|
-
flint
|
|
647
|
-
|
|
648
|
-
# Show
|
|
649
|
-
flint migrate --
|
|
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({
|
|
572
|
+
const result = await migrate(executor, { migrationsDir: './flint' });
|
|
687
573
|
|
|
688
574
|
// Check migration status
|
|
689
|
-
const status = getMigrationStatus(
|
|
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
|
|
761
|
-
|
|
|
762
|
-
| `TableDef<T>`
|
|
763
|
-
| `ColumnDef<T, S>`
|
|
764
|
-
| `InferRow<T>`
|
|
765
|
-
| `InsertRow<T>`
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
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()`) |
|