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