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 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 or null.
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
- orders: [
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; orders: OrderRow[] }[]
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; orders: OrderRow[] } | null
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
- Access the underlying `bun:sqlite` client directly for raw queries.
591
+ ### `db.$run(sql, ...params)`
592
+
593
+ Execute raw SQL directly against the database.
561
594
 
562
595
  ```ts
563
- // Simple query
564
- const users = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
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
- // With type annotation
567
- const rows = db.$client.prepare('SELECT count(*) as cnt FROM users').all() as { cnt: number }[];
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
- // Execute with db.$client
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
- ## Escape Hatch
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
- Access the underlying `bun:sqlite` client directly.
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
- db.$client; // Database instance
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
- return Object.assign(stamped, {
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,