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.
- package/API.md +158 -317
- package/README.md +239 -134
- package/dist/index.js +288 -255
- package/package.json +43 -12
- package/src/cli.ts +0 -314
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,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()`, `.
|
|
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
|
|
86
111
|
.default('hello') // DEFAULT 'hello'
|
|
87
112
|
.defaultFn(() => new Date()) // DEFAULT (computed at insert)
|
|
88
|
-
.references(otherColumn)
|
|
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(
|
|
121
|
+
integer().autoIncrement(); // AUTOINCREMENT
|
|
95
122
|
```
|
|
96
123
|
|
|
97
124
|
**Date-only:**
|
|
98
125
|
|
|
99
126
|
```ts
|
|
100
|
-
date(
|
|
127
|
+
date()
|
|
101
128
|
.defaultNow() // DEFAULT (current epoch ms)
|
|
102
|
-
.
|
|
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(
|
|
136
|
-
email: text(
|
|
137
|
-
name: text(
|
|
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)
|
|
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
|
|
242
|
-
db
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
327
|
+
Return the updated row(s) instead of void.
|
|
316
328
|
|
|
317
329
|
```ts
|
|
318
|
-
|
|
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.
|
|
346
|
+
Return the deleted row(s) instead of void.
|
|
341
347
|
|
|
342
348
|
```ts
|
|
343
|
-
|
|
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
|
|
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(
|
|
363
|
-
userId: text(
|
|
364
|
-
total: integer(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
406
|
-
// Returns: { id: string; name: string;
|
|
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(
|
|
413
|
-
// Returns: { id: string; name: string;
|
|
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(
|
|
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
|
|
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);
|
|
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)]);
|
|
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
|
-
|
|
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'))]);
|
|
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
|
-
###
|
|
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`
|
|
618
|
-
// { sql: "
|
|
536
|
+
const expr = sql`name = ${'Alice'} AND age > ${18}`;
|
|
537
|
+
// { sql: "name = ? AND age > ?", params: ["Alice", 18] }
|
|
619
538
|
|
|
620
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
});
|
|
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({
|
|
574
|
+
const result = await migrate(executor, { migrationsDir: './flint' });
|
|
687
575
|
|
|
688
576
|
// Check migration status
|
|
689
|
-
const status = getMigrationStatus(
|
|
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
|
-
|
|
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
|
|
761
|
-
|
|
|
762
|
-
| `TableDef<T>`
|
|
763
|
-
| `ColumnDef<T, S>`
|
|
764
|
-
| `InferRow<T>`
|
|
765
|
-
| `InsertRow<T>`
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
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()`) |
|