flint-orm 0.1.0 → 0.3.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 +178 -16
- package/README.md +238 -0
- package/dist/index.js +147 -4
- package/package.json +17 -2
- package/src/cli.ts +314 -0
- package/dist/entries/expressions.d.ts +0 -5
- package/dist/entries/expressions.js +0 -1184
- package/dist/entries/table.d.ts +0 -4
- package/dist/entries/table.js +0 -302
- package/dist/errors.d.ts +0 -13
- package/dist/flint.d.ts +0 -128
- package/dist/index.d.ts +0 -9
- package/dist/query/aggregates.d.ts +0 -47
- package/dist/query/builder.d.ts +0 -256
- package/dist/query/conditions.d.ts +0 -176
- package/dist/schema/columns.d.ts +0 -137
- package/dist/schema/table.d.ts +0 -44
- package/dist/src/entries/expressions.d.ts +0 -5
- package/dist/src/entries/table.d.ts +0 -4
- package/dist/src/errors.d.ts +0 -13
- package/dist/src/flint.d.ts +0 -128
- package/dist/src/index.d.ts +0 -9
- package/dist/src/query/aggregates.d.ts +0 -47
- package/dist/src/query/builder.d.ts +0 -256
- package/dist/src/query/conditions.d.ts +0 -176
- package/dist/src/schema/columns.d.ts +0 -137
- package/dist/src/schema/table.d.ts +0 -44
package/API.md
CHANGED
|
@@ -33,12 +33,12 @@ const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute(
|
|
|
33
33
|
|
|
34
34
|
## Schema
|
|
35
35
|
|
|
36
|
-
### `table(name, columns)`
|
|
36
|
+
### `table(name, columns, indexFn?)`
|
|
37
37
|
|
|
38
38
|
Define a table. Columns live as direct properties. SQL metadata is under `._`.
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
|
-
import { table, text, integer, boolean } from 'flint-orm';
|
|
41
|
+
import { table, text, integer, boolean, index } from 'flint-orm';
|
|
42
42
|
|
|
43
43
|
const users = table('users', {
|
|
44
44
|
id: text('id').primaryKey(),
|
|
@@ -49,7 +49,7 @@ const users = table('users', {
|
|
|
49
49
|
});
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
### `snakeCase.table(name, columns)`
|
|
52
|
+
### `snakeCase.table(name, columns, indexFn?)`
|
|
53
53
|
|
|
54
54
|
Auto-converts camelCase keys to snake_case SQL names.
|
|
55
55
|
|
|
@@ -122,6 +122,28 @@ type UserInsert = InsertRow<typeof users>;
|
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
|
+
## Indexes
|
|
126
|
+
|
|
127
|
+
### `index(name).on(columns).unique()`
|
|
128
|
+
|
|
129
|
+
Define indexes via the table callback. Chainable API.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const users = table(
|
|
133
|
+
'users',
|
|
134
|
+
{
|
|
135
|
+
id: text('id').primaryKey(),
|
|
136
|
+
email: text('email'),
|
|
137
|
+
name: text('name'),
|
|
138
|
+
},
|
|
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)],
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
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
|
+
---
|
|
146
|
+
|
|
125
147
|
## Query Builder
|
|
126
148
|
|
|
127
149
|
### `db.select().from(table)`
|
|
@@ -333,7 +355,7 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
|
|
|
333
355
|
|
|
334
356
|
### `db.leftJoin(parent).on(child, condition?)`
|
|
335
357
|
|
|
336
|
-
LEFT JOIN. Returns all parent rows, with matching child data
|
|
358
|
+
LEFT JOIN. Returns all parent rows, with matching child data nested under `__children`.
|
|
337
359
|
|
|
338
360
|
```ts
|
|
339
361
|
const orders = table('orders', {
|
|
@@ -367,7 +389,7 @@ Joins return **nested** results, not flat-merged:
|
|
|
367
389
|
{
|
|
368
390
|
id: 'u1',
|
|
369
391
|
name: 'Alice',
|
|
370
|
-
|
|
392
|
+
__children: [
|
|
371
393
|
{ id: 'o1', userId: 'u1', total: 100 },
|
|
372
394
|
{ id: 'o2', userId: 'u1', total: 200 },
|
|
373
395
|
],
|
|
@@ -381,14 +403,23 @@ Narrow parent columns with `.columns()`:
|
|
|
381
403
|
|
|
382
404
|
```ts
|
|
383
405
|
db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
|
|
384
|
-
// Returns: { id: string; name: string;
|
|
406
|
+
// Returns: { id: string; name: string; __children: OrderRow[] }[]
|
|
385
407
|
```
|
|
386
408
|
|
|
387
409
|
### `.single()` on Joins
|
|
388
410
|
|
|
389
411
|
```ts
|
|
390
412
|
db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
|
|
391
|
-
// Returns: { id: string; name: string;
|
|
413
|
+
// Returns: { id: string; name: string; __children: OrderRow[] } | null
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Multi-Join
|
|
417
|
+
|
|
418
|
+
Chain multiple joins:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
|
|
422
|
+
// Returns nested: { ...userFields, __children: [{ ...orderFields, __children: [...items] }] }
|
|
392
423
|
```
|
|
393
424
|
|
|
394
425
|
---
|
|
@@ -557,14 +588,21 @@ All queries succeed or all roll back.
|
|
|
557
588
|
|
|
558
589
|
## Raw SQL
|
|
559
590
|
|
|
560
|
-
|
|
591
|
+
### `db.$run(sql, ...params)`
|
|
592
|
+
|
|
593
|
+
Execute raw SQL directly against the database.
|
|
561
594
|
|
|
562
595
|
```ts
|
|
563
|
-
|
|
564
|
-
|
|
596
|
+
db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
|
|
597
|
+
db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### `db.$client`
|
|
565
601
|
|
|
566
|
-
|
|
567
|
-
|
|
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');
|
|
568
606
|
```
|
|
569
607
|
|
|
570
608
|
---
|
|
@@ -579,20 +617,142 @@ import { sql } from 'flint-orm';
|
|
|
579
617
|
const expr = sql`SELECT * FROM users WHERE name = ${'Alice'} AND age > ${18}`;
|
|
580
618
|
// { sql: "SELECT * FROM users WHERE name = ? AND age > ?", params: ["Alice", 18] }
|
|
581
619
|
|
|
582
|
-
//
|
|
620
|
+
// Use with db.$client
|
|
583
621
|
const rows = db.$client.prepare(expr.sql).all(...expr.params);
|
|
584
622
|
```
|
|
585
623
|
|
|
586
624
|
---
|
|
587
625
|
|
|
588
|
-
##
|
|
626
|
+
## Migration System
|
|
627
|
+
|
|
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.
|
|
643
|
+
|
|
644
|
+
```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
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Programmatic API
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
|
|
672
|
+
|
|
673
|
+
// Serialize table definitions to JSON
|
|
674
|
+
const state = serializeSchema([users, orders]);
|
|
675
|
+
|
|
676
|
+
// Diff two schema states
|
|
677
|
+
const operations = diffSchemas(previousState, currentState);
|
|
678
|
+
|
|
679
|
+
// Generate SQL from operations
|
|
680
|
+
const sql = generateSQL(operations);
|
|
681
|
+
|
|
682
|
+
// Generate a migration folder
|
|
683
|
+
const result = generate([users, orders], './flint', 'init_schema');
|
|
684
|
+
|
|
685
|
+
// Apply pending migrations
|
|
686
|
+
const result = migrate({ url: './app.db' }, './flint');
|
|
687
|
+
|
|
688
|
+
// Check migration status
|
|
689
|
+
const status = getMigrationStatus(client, './flint');
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Migration Operations
|
|
693
|
+
|
|
694
|
+
Named, pre-vetted operations:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
| Operation | SQL Generated |
|
|
701
|
+
| -------------- | -------------------------------------- |
|
|
702
|
+
| `addTable` | `CREATE TABLE ...` |
|
|
703
|
+
| `dropTable` | `DROP TABLE ...` |
|
|
704
|
+
| `renameTable` | `ALTER TABLE ... RENAME TO ...` |
|
|
705
|
+
| `addColumn` | `ALTER TABLE ... ADD COLUMN ...` |
|
|
706
|
+
| `dropColumn` | `ALTER TABLE ... DROP COLUMN ...` |
|
|
707
|
+
| `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
|
|
708
|
+
| `createIndex` | `CREATE [UNIQUE] INDEX ...` |
|
|
709
|
+
| `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
|
|
589
737
|
|
|
590
|
-
|
|
738
|
+
### `introspectSchema(client)`
|
|
739
|
+
|
|
740
|
+
Read the live database schema and return a `SchemaState` that can be diffed against code-defined tables.
|
|
591
741
|
|
|
592
742
|
```ts
|
|
593
|
-
|
|
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);
|
|
594
752
|
```
|
|
595
753
|
|
|
754
|
+
Normalizes SQLite type aliases (VARCHAR → TEXT, BIGINT → INTEGER, etc.) and parses default values.
|
|
755
|
+
|
|
596
756
|
---
|
|
597
757
|
|
|
598
758
|
## Types
|
|
@@ -603,6 +763,8 @@ db.$client; // Database instance
|
|
|
603
763
|
| `ColumnDef<T, S>` | Column definition with phantom type `T` and storage class `S` |
|
|
604
764
|
| `InferRow<T>` | Derives row type from table definition |
|
|
605
765
|
| `InsertRow<T>` | Derives insert type (defaults are optional) |
|
|
766
|
+
| `IndexDef` | Index definition: `{ name, columns, unique }` |
|
|
767
|
+
| `IndexBuilder` | Chainable index builder: `.on(cols).unique()` |
|
|
606
768
|
| `Condition` | Condition node for WHERE clauses |
|
|
607
769
|
| `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
|
|
608
770
|
| `ConnectionDetails` | `{ url: string }` |
|
package/README.md
CHANGED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# flint-orm
|
|
2
|
+
|
|
3
|
+
A minimal, type-safe SQLite ORM. One dialect, one runtime, no abstraction overhead.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Drizzle is great, but its execution layer (`drizzle-kit migrate`) wasn't built for dynamic, multi-tenant, per-request-connection contexts. Flint is narrower by design: SQLite/libSQL only, no multi-dialect abstraction, no pluggable drivers — just a query builder that does exactly what SQLite supports.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install flint-orm
|
|
13
|
+
bun add flint-orm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { flint, table, text, integer, eq } from 'flint-orm';
|
|
20
|
+
|
|
21
|
+
const users = table('users', {
|
|
22
|
+
id: text('id').primaryKey(),
|
|
23
|
+
name: text('name').notNull(),
|
|
24
|
+
email: text('email').unique(),
|
|
25
|
+
age: integer('age'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const db = flint({ url: 'app.db' });
|
|
29
|
+
|
|
30
|
+
// Insert
|
|
31
|
+
db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
|
|
32
|
+
|
|
33
|
+
// Query
|
|
34
|
+
const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute();
|
|
35
|
+
// { id: "u1", name: "Alice", email: "alice@example.com", age: null }
|
|
36
|
+
|
|
37
|
+
// Update
|
|
38
|
+
db.update(users).set({ name: 'Alice Updated' }).where(eq(users.id, 'u1')).execute();
|
|
39
|
+
|
|
40
|
+
// Delete
|
|
41
|
+
db.delete(users).where(eq(users.id, 'u1')).execute();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
### Schema
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { table, text, integer, boolean, json, date, real, index } from 'flint-orm';
|
|
50
|
+
|
|
51
|
+
const users = table(
|
|
52
|
+
'users',
|
|
53
|
+
{
|
|
54
|
+
id: text('id').primaryKey(),
|
|
55
|
+
name: text('name').notNull(),
|
|
56
|
+
email: text('email').unique(),
|
|
57
|
+
active: boolean('active').default(true),
|
|
58
|
+
metadata: json('metadata'),
|
|
59
|
+
createdAt: date('createdAt').defaultNow().onUpdate(),
|
|
60
|
+
score: real('score'),
|
|
61
|
+
age: integer('age'),
|
|
62
|
+
orgId: text('orgId').references(orgs.id),
|
|
63
|
+
},
|
|
64
|
+
(t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_org').on(t.orgId)],
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Type | TS Type | SQLite | Modifiers |
|
|
69
|
+
| --------- | --------- | ------- | ------------------------------------------------------------------------- |
|
|
70
|
+
| `text` | `string` | TEXT | `.primaryKey()`, `.notNull()`, `.unique()`, `.default()`, `.references()` |
|
|
71
|
+
| `integer` | `number` | INTEGER | `.autoIncrement()` |
|
|
72
|
+
| `boolean` | `boolean` | INTEGER | Encodes 0/1 |
|
|
73
|
+
| `json<T>` | `T` | TEXT | JSON encode/decode |
|
|
74
|
+
| `date` | `Date` | INTEGER | `.defaultNow()`, `.onUpdate()` |
|
|
75
|
+
| `real` | `number` | REAL | |
|
|
76
|
+
|
|
77
|
+
### Query Builder
|
|
78
|
+
|
|
79
|
+
Two-phase builders prevent invalid queries at compile time:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// SELECT
|
|
83
|
+
db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name').limit(10).execute();
|
|
84
|
+
|
|
85
|
+
// INSERT
|
|
86
|
+
db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
|
|
87
|
+
|
|
88
|
+
// UPSERT
|
|
89
|
+
db.insert(users)
|
|
90
|
+
.values({ id: 'u1', name: 'Alice' })
|
|
91
|
+
.onConflictDoUpdate({ target: users.id, set: { name: 'Bob' } })
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
// UPDATE
|
|
95
|
+
db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
|
|
96
|
+
|
|
97
|
+
// DELETE
|
|
98
|
+
db.delete(users).where(eq(users.id, 'u1')).returning().execute();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Joins
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const orders = table('orders', {
|
|
105
|
+
id: text('id').primaryKey(),
|
|
106
|
+
userId: text('userId').references(users.id),
|
|
107
|
+
total: integer('total').notNull(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Left join — nested result shape
|
|
111
|
+
const result = db.leftJoin(orders).on(users).execute();
|
|
112
|
+
// [{ id: "u1", name: "Alice", __children: [{ id: "o1", total: 100 }] }]
|
|
113
|
+
|
|
114
|
+
// Multi-join
|
|
115
|
+
db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Aggregates
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
db.count(users);
|
|
122
|
+
db.sum(orders, orders.total);
|
|
123
|
+
db.avg(orders, orders.total, eq(orders.userId, 'u1'));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Batch
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
db.batch([db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1'))]);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Raw SQL
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Conditions
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm';
|
|
142
|
+
|
|
143
|
+
db.select()
|
|
144
|
+
.from(users)
|
|
145
|
+
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
146
|
+
.execute();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Types
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type { InferRow, InsertRow } from 'flint-orm';
|
|
153
|
+
|
|
154
|
+
type UserRow = InferRow<typeof users>;
|
|
155
|
+
type UserInsert = InsertRow<typeof users>; // defaults are optional
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Migration System
|
|
159
|
+
|
|
160
|
+
### CLI
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Generate migration from schema changes
|
|
164
|
+
flint generate --name init_schema
|
|
165
|
+
|
|
166
|
+
# Preview SQL without writing files
|
|
167
|
+
flint generate --preview
|
|
168
|
+
|
|
169
|
+
# Apply pending migrations
|
|
170
|
+
flint migrate
|
|
171
|
+
|
|
172
|
+
# Check status
|
|
173
|
+
flint migrate --status
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Config
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// flint.config.ts
|
|
180
|
+
import { defineConfig } from 'flint-orm/config';
|
|
181
|
+
|
|
182
|
+
export default defineConfig({
|
|
183
|
+
schema: './src/schema',
|
|
184
|
+
migrations: './flint',
|
|
185
|
+
database: { url: './app.db' },
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### How It Works
|
|
190
|
+
|
|
191
|
+
1. `flint generate` serializes your `table()` definitions, diffs against the last snapshot, and writes a migration folder with TypeScript operations
|
|
192
|
+
2. `flint migrate` reads pending migration folders, executes SQL via `batch()` (atomic), and records applied migrations in `__flint_migrations`
|
|
193
|
+
3. Tables are topologically sorted by foreign key dependencies (Kahn's algorithm) — referenced tables are created first
|
|
194
|
+
|
|
195
|
+
### Programmatic
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## CLI
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
flint <command> [options]
|
|
205
|
+
|
|
206
|
+
Commands:
|
|
207
|
+
generate Generate a new migration from schema changes
|
|
208
|
+
migrate Apply pending migrations
|
|
209
|
+
status Show migration status
|
|
210
|
+
|
|
211
|
+
Options:
|
|
212
|
+
--name, -n Migration name
|
|
213
|
+
--preview, -p Show SQL without writing files
|
|
214
|
+
--status Show which migrations are pending/applied
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Error Handling
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { FlintValidationError, FlintQueryError } from 'flint-orm';
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Error | When |
|
|
224
|
+
| ---------------------- | ----------------------------- |
|
|
225
|
+
| `FlintValidationError` | Invalid query construction |
|
|
226
|
+
| `FlintQueryError` | Runtime SQL execution failure |
|
|
227
|
+
|
|
228
|
+
## Philosophy
|
|
229
|
+
|
|
230
|
+
- **SQLite only** — no multi-dialect abstraction, no driver plugins
|
|
231
|
+
- **Type-safe by construction** — two-phase builders, phantom types, `InferRow<T>`
|
|
232
|
+
- **Immutable** — every chain method returns a new instance
|
|
233
|
+
- **No classes** — functions and plain objects only
|
|
234
|
+
- **Parameterized** — all queries use `?` placeholders, never string interpolation
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -1144,6 +1144,9 @@ function flint(details) {
|
|
|
1144
1144
|
avg: (table, column, condition) => avg(client, table, column, condition),
|
|
1145
1145
|
min: (table, column, condition) => min(client, table, column, condition),
|
|
1146
1146
|
max: (table, column, condition) => max(client, table, column, condition),
|
|
1147
|
+
$run(sql, ...params) {
|
|
1148
|
+
return client.prepare(sql).run(...params);
|
|
1149
|
+
},
|
|
1147
1150
|
$client: client
|
|
1148
1151
|
};
|
|
1149
1152
|
}
|
|
@@ -1329,7 +1332,7 @@ function real(name) {
|
|
|
1329
1332
|
});
|
|
1330
1333
|
}
|
|
1331
1334
|
// src/schema/table.ts
|
|
1332
|
-
function table(name, columns) {
|
|
1335
|
+
function table(name, columns, indexFn) {
|
|
1333
1336
|
const stamped = Object.create(null);
|
|
1334
1337
|
for (const [key, col] of Object.entries(columns)) {
|
|
1335
1338
|
const columnName = col.name || key;
|
|
@@ -1383,14 +1386,22 @@ function table(name, columns) {
|
|
|
1383
1386
|
}
|
|
1384
1387
|
stamped[key] = stampedCol;
|
|
1385
1388
|
}
|
|
1386
|
-
|
|
1389
|
+
const result = Object.assign(stamped, {
|
|
1387
1390
|
_: { name }
|
|
1388
1391
|
});
|
|
1392
|
+
if (indexFn) {
|
|
1393
|
+
const raw = indexFn(result);
|
|
1394
|
+
if (raw.length > 0) {
|
|
1395
|
+
const tableObj = result;
|
|
1396
|
+
tableObj.__indexes = raw.map((item) => item.build());
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return result;
|
|
1389
1400
|
}
|
|
1390
1401
|
function toSnakeCase(str) {
|
|
1391
1402
|
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1392
1403
|
}
|
|
1393
|
-
function snakeCaseTable(tableName, columns) {
|
|
1404
|
+
function snakeCaseTable(tableName, columns, indexFn) {
|
|
1394
1405
|
const converted = {};
|
|
1395
1406
|
for (const [key, col] of Object.entries(columns)) {
|
|
1396
1407
|
const sqlName = toSnakeCase(key);
|
|
@@ -1445,11 +1456,141 @@ function snakeCaseTable(tableName, columns) {
|
|
|
1445
1456
|
}
|
|
1446
1457
|
converted[key] = stampedCol;
|
|
1447
1458
|
}
|
|
1448
|
-
return table(tableName, converted);
|
|
1459
|
+
return table(tableName, converted, indexFn);
|
|
1449
1460
|
}
|
|
1450
1461
|
var snakeCase = {
|
|
1451
1462
|
table: snakeCaseTable
|
|
1452
1463
|
};
|
|
1464
|
+
function index(name) {
|
|
1465
|
+
let columns = [];
|
|
1466
|
+
let isUnique = false;
|
|
1467
|
+
const builder = {
|
|
1468
|
+
on(...cols) {
|
|
1469
|
+
columns = cols;
|
|
1470
|
+
return builder;
|
|
1471
|
+
},
|
|
1472
|
+
unique() {
|
|
1473
|
+
isUnique = true;
|
|
1474
|
+
return builder;
|
|
1475
|
+
},
|
|
1476
|
+
build() {
|
|
1477
|
+
if (columns.length === 0) {
|
|
1478
|
+
throw new Error(`Index "${name}" has no columns \u2014 call .on() before returning`);
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
name,
|
|
1482
|
+
columns: columns.map((c) => c.name),
|
|
1483
|
+
unique: isUnique
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
return builder;
|
|
1488
|
+
}
|
|
1489
|
+
// src/sqlite/introspect.ts
|
|
1490
|
+
var TEXT_TYPES = new Set(["text", "varchar", "character", "char", "clob", "native character", "nvarchar", "nchar", "nyclob", "numeric"]);
|
|
1491
|
+
var INTEGER_TYPES = new Set(["integer", "int", "smallint", "tinyint", "bigint", "unsigned big int", "int2", "int8", "mediumint"]);
|
|
1492
|
+
var REAL_TYPES = new Set(["real", "float", "double", "double precision", "decimal"]);
|
|
1493
|
+
function normalizeType(rawType) {
|
|
1494
|
+
const t = rawType.toLowerCase().trim();
|
|
1495
|
+
const base = t.replace(/\(.*\)/, "").trim();
|
|
1496
|
+
if (INTEGER_TYPES.has(base))
|
|
1497
|
+
return "integer";
|
|
1498
|
+
if (REAL_TYPES.has(base))
|
|
1499
|
+
return "real";
|
|
1500
|
+
if (TEXT_TYPES.has(base))
|
|
1501
|
+
return "text";
|
|
1502
|
+
if (base === "blob")
|
|
1503
|
+
return "blob";
|
|
1504
|
+
return "text";
|
|
1505
|
+
}
|
|
1506
|
+
function parseDefault(dfltValue) {
|
|
1507
|
+
if (dfltValue === null || dfltValue === undefined) {
|
|
1508
|
+
return { hasDefault: false };
|
|
1509
|
+
}
|
|
1510
|
+
const raw = String(dfltValue);
|
|
1511
|
+
if (raw.toUpperCase() === "NULL") {
|
|
1512
|
+
return { hasDefault: true, defaultValue: null };
|
|
1513
|
+
}
|
|
1514
|
+
if (/^-?\d+$/.test(raw)) {
|
|
1515
|
+
return { hasDefault: true, defaultValue: Number(raw) };
|
|
1516
|
+
}
|
|
1517
|
+
if (/^-?\d+\.\d+$/.test(raw)) {
|
|
1518
|
+
return { hasDefault: true, defaultValue: Number(raw) };
|
|
1519
|
+
}
|
|
1520
|
+
if (raw.startsWith("'") && raw.endsWith("'") || raw.startsWith('"') && raw.endsWith('"')) {
|
|
1521
|
+
return { hasDefault: true, defaultValue: raw.slice(1, -1) };
|
|
1522
|
+
}
|
|
1523
|
+
return { hasDefault: true, defaultValue: raw };
|
|
1524
|
+
}
|
|
1525
|
+
function introspectColumns(client, tableName) {
|
|
1526
|
+
const rows = client.query(`PRAGMA table_info('${tableName}')`).all();
|
|
1527
|
+
const fkRows = client.query(`PRAGMA foreign_key_list('${tableName}')`).all();
|
|
1528
|
+
const indexRows = client.query(`PRAGMA index_list('${tableName}')`).all();
|
|
1529
|
+
const fkMap = new Map;
|
|
1530
|
+
for (const fk of fkRows) {
|
|
1531
|
+
fkMap.set(fk.from, { referencesTable: fk.table, referencesColumn: fk.to });
|
|
1532
|
+
}
|
|
1533
|
+
const uniqueColumns = new Set;
|
|
1534
|
+
for (const idx of indexRows) {
|
|
1535
|
+
if (idx.unique === 1 && (idx.origin === "u" || idx.origin === "c")) {
|
|
1536
|
+
const colInfo = client.query(`PRAGMA index_info('${idx.name}')`).all();
|
|
1537
|
+
if (colInfo.length === 1) {
|
|
1538
|
+
uniqueColumns.add(colInfo[0].name);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const columns = rows.map((row) => {
|
|
1543
|
+
const fk = fkMap.get(row.name);
|
|
1544
|
+
const { hasDefault, defaultValue } = parseDefault(row.dflt_value);
|
|
1545
|
+
const col = {
|
|
1546
|
+
name: row.name,
|
|
1547
|
+
sqlType: normalizeType(row.type),
|
|
1548
|
+
isPrimaryKey: row.pk === 1,
|
|
1549
|
+
isNotNull: row.notnull === 1,
|
|
1550
|
+
isUnique: uniqueColumns.has(row.name),
|
|
1551
|
+
hasDefault,
|
|
1552
|
+
defaultValue
|
|
1553
|
+
};
|
|
1554
|
+
if (fk) {
|
|
1555
|
+
col.referencesTable = fk.referencesTable;
|
|
1556
|
+
col.referencesColumn = fk.referencesColumn;
|
|
1557
|
+
}
|
|
1558
|
+
return col;
|
|
1559
|
+
});
|
|
1560
|
+
return { columns, indexRows };
|
|
1561
|
+
}
|
|
1562
|
+
function introspectIndexes(client, indexRows) {
|
|
1563
|
+
const indexes = [];
|
|
1564
|
+
for (const idx of indexRows) {
|
|
1565
|
+
if (idx.name.startsWith("sqlite_autoindex_"))
|
|
1566
|
+
continue;
|
|
1567
|
+
const colRows = client.query(`PRAGMA index_info('${idx.name}')`).all();
|
|
1568
|
+
const columns = colRows.map((c) => c.name);
|
|
1569
|
+
indexes.push({
|
|
1570
|
+
name: idx.name,
|
|
1571
|
+
columns,
|
|
1572
|
+
unique: idx.unique === 1
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
return indexes;
|
|
1576
|
+
}
|
|
1577
|
+
function introspect(client) {
|
|
1578
|
+
const tableRows = client.query(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '__flint_migrations'`).all();
|
|
1579
|
+
const tables = {};
|
|
1580
|
+
for (const { name } of tableRows) {
|
|
1581
|
+
const { columns, indexRows } = introspectColumns(client, name);
|
|
1582
|
+
const indexes = introspectIndexes(client, indexRows);
|
|
1583
|
+
tables[name] = {
|
|
1584
|
+
name,
|
|
1585
|
+
columns,
|
|
1586
|
+
indexes
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
version: 1,
|
|
1591
|
+
tables
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1453
1594
|
export {
|
|
1454
1595
|
text,
|
|
1455
1596
|
table,
|
|
@@ -1469,7 +1610,9 @@ export {
|
|
|
1469
1610
|
isNotNull,
|
|
1470
1611
|
isNotIn,
|
|
1471
1612
|
isIn,
|
|
1613
|
+
introspect,
|
|
1472
1614
|
integer,
|
|
1615
|
+
index,
|
|
1473
1616
|
gte,
|
|
1474
1617
|
gt,
|
|
1475
1618
|
glob,
|