flint-orm 0.7.0 → 0.7.2

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.
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: update-schema
3
+ description: >
4
+ Add columns and tables without data loss. Covers safe vs unsafe migrations,
5
+ rebuildTable operation, additive migration patterns, and which changes
6
+ trigger table rebuilds. Load when modifying schema after initial migration
7
+ or when planning schema evolution.
8
+ metadata:
9
+ type: core
10
+ library: flint-orm
11
+ library_version: 0.7.0
12
+ sources:
13
+ - 'kavenlabs/flint-orm:src/migration/diff.ts'
14
+ - 'kavenlabs/flint-orm:src/migration/sql.ts'
15
+ - 'kavenlabs/flint-orm:src/migration/migrate.ts'
16
+ - 'kavenlabs/flint-orm:README.md'
17
+ ---
18
+
19
+ # flint-orm — Schema Updates
20
+
21
+ ## Setup
22
+
23
+ After defining your initial schema, you'll need to evolve it over time. flint-orm detects changes and generates appropriate migrations.
24
+
25
+ ## Core Patterns
26
+
27
+ ### Safe migration: Adding a column with default
28
+
29
+ ```ts
30
+ // Add a new column with a default value — safe, no data loss
31
+ const users = snakeCase.table('users', {
32
+ id: integer().autoIncrement().primaryKey(),
33
+ name: text().notNull(),
34
+ status: text().notNull().default('active'), // NEW: safe to add
35
+ });
36
+ ```
37
+
38
+ ### Safe migration: Adding a new table
39
+
40
+ ```ts
41
+ // Add a new table — always safe
42
+ const posts = snakeCase.table('posts', {
43
+ id: integer().autoIncrement().primaryKey(),
44
+ userId: integer().notNull().references(users.id),
45
+ title: text().notNull(),
46
+ });
47
+ ```
48
+
49
+ ### Unsafe migration: Changing column type
50
+
51
+ ```ts
52
+ // Changing a column type triggers a table rebuild
53
+ // The old column was: age: text('age')
54
+ // The new column is: age: integer('age')
55
+ const users = snakeCase.table('users', {
56
+ id: integer().autoIncrement().primaryKey(),
57
+ name: text().notNull(),
58
+ age: integer('age').notNull().default(0), // TYPE CHANGE: triggers rebuild
59
+ });
60
+ ```
61
+
62
+ ### Unsafe migration: Adding NOT NULL without default
63
+
64
+ ```ts
65
+ // Adding NOT NULL without a default triggers a table rebuild
66
+ const users = snakeCase.table('users', {
67
+ id: integer().autoIncrement().primaryKey(),
68
+ name: text().notNull(),
69
+ status: text().notNull(), // NO DEFAULT: triggers rebuild
70
+ });
71
+ ```
72
+
73
+ ## Migration Safety Reference
74
+
75
+ ### Safe changes (no rebuild)
76
+
77
+ | Change | Example |
78
+ |--------|---------|
79
+ | Add column with default | `status: text().default('active')` |
80
+ | Add new table | `snakeCase.table('new_table', {...})` |
81
+ | Add index | `index('idx_name').on(t.name)` |
82
+ | Drop index | Remove from index callback |
83
+ | Change default value | `status: text().default('inactive')` |
84
+
85
+ ### Unsafe changes (triggers rebuild)
86
+
87
+ | Change | Example |
88
+ |--------|---------|
89
+ | Change column type | `text()` → `integer()` |
90
+ | Add NOT NULL without default | `text().notNull()` (no `.default()`) |
91
+ | Remove NOT NULL | `text().notNull()` → `text()` |
92
+ | Add/remove UNIQUE | `text().unique()` → `text()` |
93
+ | Remove default | `text().default('x')` → `text()` |
94
+ | Add/change/remove foreign key | `.references(col)` changes |
95
+ | Change FK actions | `.onDelete('cascade')` changes |
96
+ | Change autoincrement | `.autoIncrement()` changes |
97
+
98
+ ### What is rebuildTable?
99
+
100
+ When an unsafe change is detected, flint-orm generates a `rebuildTable` operation that:
101
+
102
+ 1. Creates a temporary table with the new schema
103
+ 2. Copies data from the old table
104
+ 3. Drops the old table
105
+ 4. Renames the temporary table to the original name
106
+ 5. Recreates indexes
107
+
108
+ This happens within a transaction — if anything fails, the original table is preserved.
109
+
110
+ ## Common Mistakes
111
+
112
+ ### HIGH Adding NOT NULL column without a default
113
+
114
+ Wrong:
115
+
116
+ ```ts
117
+ // After initial migration, adding:
118
+ const users = snakeCase.table('users', {
119
+ id: integer().autoIncrement().primaryKey(),
120
+ name: text().notNull(),
121
+ status: text().notNull(), // No default — triggers rebuild
122
+ });
123
+ ```
124
+
125
+ Correct:
126
+
127
+ ```ts
128
+ const users = snakeCase.table('users', {
129
+ id: integer().autoIncrement().primaryKey(),
130
+ name: text().notNull(),
131
+ status: text().notNull().default('active'), // Safe: has default
132
+ });
133
+ ```
134
+
135
+ SQLite cannot add NOT NULL columns to existing tables without a default — this triggers a table rebuild.
136
+
137
+ Source: src/migration/diff.ts
138
+
139
+ ### HIGH Changing a column type
140
+
141
+ Wrong:
142
+
143
+ ```ts
144
+ // Changing after initial migration:
145
+ const users = snakeCase.table('users', {
146
+ id: integer().autoIncrement().primaryKey(),
147
+ name: text().notNull(),
148
+ age: integer('age'), // Was: age: text('age')
149
+ });
150
+ ```
151
+
152
+ Correct:
153
+
154
+ ```ts
155
+ // Plan for the type from the start, or accept the rebuild
156
+ const users = snakeCase.table('users', {
157
+ id: integer().autoIncrement().primaryKey(),
158
+ name: text().notNull(),
159
+ age: integer('age').notNull().default(0), // Correct type from start
160
+ });
161
+ ```
162
+
163
+ SQLite has no `ALTER COLUMN TYPE` — this always triggers a table rebuild.
164
+
165
+ Source: src/migration/diff.ts
166
+
167
+ ### HIGH Dropping a column referenced by foreign keys
168
+
169
+ Wrong:
170
+
171
+ ```ts
172
+ // Dropping users.id when orders.userId references it
173
+ // Migration fails with: Cannot rebuild "users" — referenced by: orders
174
+ ```
175
+
176
+ Correct:
177
+
178
+ ```bash
179
+ # Migrate dependent tables first, then rebuild the parent
180
+ flint migrate # Apply orders migration first
181
+ flint generate # Then rebuild users
182
+ ```
183
+
184
+ The migration runner checks for incoming foreign keys before rebuild — it will refuse if other tables reference the table being rebuilt.
185
+
186
+ Source: src/migration/migrate.ts
@@ -0,0 +1,267 @@
1
+ ---
2
+ name: write-queries
3
+ description: >
4
+ Read, write, and modify data with type-safe SELECT, INSERT, UPDATE, DELETE,
5
+ joins, and aggregates. Covers selectFrom, insert, update, delete, leftJoin,
6
+ innerJoin, where, columns, single, orderBy, limit, offset, distinct,
7
+ returning, onConflictDoNothing, onConflictDoUpdate, count, sum, avg, min,
8
+ max, and raw SQL. Load when building queries or troubleshooting query issues.
9
+ metadata:
10
+ type: core
11
+ library: flint-orm
12
+ library_version: 0.7.0
13
+ sources:
14
+ - 'kavenlabs/flint-orm:src/query/builder.ts'
15
+ - 'kavenlabs/flint-orm:src/query/conditions.ts'
16
+ - 'kavenlabs/flint-orm:src/query/aggregates.ts'
17
+ - 'kavenlabs/flint-orm:src/flint.ts'
18
+ - 'kavenlabs/flint-orm:README.md'
19
+ - 'kavenlabs/flint-orm:API.md'
20
+ ---
21
+
22
+ # flint-orm — Queries
23
+
24
+ ## Setup
25
+
26
+ ```ts
27
+ import { flint } from 'flint-orm/bun-sqlite';
28
+ import { table, text, integer } from 'flint-orm/table';
29
+ import { eq, and } from 'flint-orm/expressions';
30
+
31
+ const users = table('users', {
32
+ id: text('id').primaryKey(),
33
+ name: text('name').notNull(),
34
+ age: integer('age'),
35
+ });
36
+
37
+ const db = flint({ url: './app.db' });
38
+ ```
39
+
40
+ ## Core Patterns
41
+
42
+ ### SELECT with conditions
43
+
44
+ ```ts
45
+ const adults = await db
46
+ .selectFrom(users)
47
+ .where(eq(users.age, 18))
48
+ .execute();
49
+ ```
50
+
51
+ ### SELECT specific columns
52
+
53
+ ```ts
54
+ const names = await db
55
+ .selectFrom(users)
56
+ .columns(['id', 'name'])
57
+ .execute();
58
+ // Returns: { id: string; name: string }[]
59
+ ```
60
+
61
+ ### Get a single row
62
+
63
+ ```ts
64
+ const user = await db
65
+ .selectFrom(users)
66
+ .where(eq(users.id, 'u1'))
67
+ .single()
68
+ .execute();
69
+ // Returns: User | null
70
+ ```
71
+
72
+ ### INSERT with returning
73
+
74
+ ```ts
75
+ // Return all columns
76
+ const inserted = await db
77
+ .insert(users)
78
+ .values({ id: 'u1', name: 'Alice', age: 30 })
79
+ .returning()
80
+ .execute();
81
+
82
+ // Return specific columns
83
+ const inserted = await db
84
+ .insert(users)
85
+ .values({ id: 'u1', name: 'Alice', age: 30 })
86
+ .returning(['id', 'name'])
87
+ .execute();
88
+ ```
89
+
90
+ ### UPSERT
91
+
92
+ ```ts
93
+ await db
94
+ .insert(users)
95
+ .values({ id: 'u1', name: 'Alice' })
96
+ .onConflictDoUpdate({
97
+ target: users.id,
98
+ set: { name: 'Alice Updated' },
99
+ })
100
+ .execute();
101
+ ```
102
+
103
+ ### UPDATE with conditions
104
+
105
+ ```ts
106
+ // Update without returning
107
+ await db
108
+ .update(users)
109
+ .set({ name: 'Bob' })
110
+ .where(eq(users.id, 'u1'))
111
+ .execute();
112
+
113
+ // Update with returning
114
+ const updated = await db
115
+ .update(users)
116
+ .set({ name: 'Bob' })
117
+ .where(eq(users.id, 'u1'))
118
+ .returning(['id', 'name'])
119
+ .execute();
120
+ ```
121
+
122
+ ### DELETE with conditions
123
+
124
+ ```ts
125
+ // Delete without returning
126
+ await db
127
+ .delete(users)
128
+ .where(eq(users.id, 'u1'))
129
+ .execute();
130
+
131
+ // Delete with returning
132
+ const deleted = await db
133
+ .delete(users)
134
+ .where(eq(users.id, 'u1'))
135
+ .returning()
136
+ .execute();
137
+ ```
138
+
139
+ ### LEFT JOIN
140
+
141
+ ```ts
142
+ const posts = table('posts', {
143
+ id: text('id').primaryKey(),
144
+ userId: text('userId').notNull().references(users.id),
145
+ title: text('title').notNull(),
146
+ });
147
+
148
+ const postsWithAuthors = await db
149
+ .leftJoin(users)
150
+ .on(posts)
151
+ .execute();
152
+ // Returns: { id: string; name: string; posts: Post[] }[]
153
+ ```
154
+
155
+ ### Aggregate functions
156
+
157
+ ```ts
158
+ const total = await db.count(users);
159
+ const activeCount = await db.count(users, eq(users.active, true));
160
+ const totalViews = await db.sum(posts, posts.views);
161
+ const avgAge = await db.avg(users, users.age);
162
+ ```
163
+
164
+ ## Building Queries Conditionally
165
+
166
+ The query builder is immutable — you can compose queries conditionally:
167
+
168
+ ```ts
169
+ let query = db.selectFrom(users);
170
+
171
+ if (filter.name) {
172
+ query = query.where(eq(users.name, filter.name));
173
+ }
174
+ if (filter.minAge) {
175
+ query = query.where(gte(users.age, filter.minAge));
176
+ }
177
+
178
+ const results = await query.execute();
179
+ ```
180
+
181
+ Call `.execute()` at the terminal state when ready to run the query.
182
+
183
+ ## Common Mistakes
184
+
185
+ ### CRITICAL Not calling .execute() at terminal state
186
+
187
+ Wrong:
188
+
189
+ ```ts
190
+ const query = db.selectFrom(users).where(eq(users.id, 'u1')); // Builder, not executed
191
+ const data = query; // Missing .execute()
192
+ ```
193
+
194
+ Correct:
195
+
196
+ ```ts
197
+ // Building queries conditionally is fine:
198
+ let query = db.selectFrom(users);
199
+ if (filter) query = query.where(eq(users.active, true));
200
+ // But call .execute() at the terminal state:
201
+ const data = await query.execute();
202
+ ```
203
+
204
+ Queries are builders — you can compose them conditionally, but `.execute()` must be called when ready to run the query.
205
+
206
+ Source: maintainer interview
207
+
208
+ ### CRITICAL Not awaiting .execute()
209
+
210
+ Wrong:
211
+
212
+ ```ts
213
+ const users = db.selectFrom(users).where(eq(users.id, 'u1')).execute(); // Promise, not data
214
+ ```
215
+
216
+ Correct:
217
+
218
+ ```ts
219
+ const users = await db.selectFrom(users).where(eq(users.id, 'u1')).execute();
220
+ ```
221
+
222
+ `.execute()` returns a Promise — without `await`, you get the Promise object, not the data.
223
+
224
+ Source: maintainer interview
225
+
226
+ ### HIGH Mixing Kysely/Drizzle/Supabase API patterns
227
+
228
+ Wrong:
229
+
230
+ ```ts
231
+ // Supabase-style (wrong)
232
+ const users = await db.from('users').select('id, name');
233
+ ```
234
+
235
+ Correct:
236
+
237
+ ```ts
238
+ // Flint-style
239
+ const users = await db.selectFrom(users).columns(['id', 'name']).execute();
240
+ ```
241
+
242
+ flint-orm has its own API — `selectFrom().columns([...]).single()` is specific to this library.
243
+
244
+ Source: maintainer interview
245
+
246
+ ### HIGH Using WHERE conditions with columns from wrong table
247
+
248
+ Wrong:
249
+
250
+ ```ts
251
+ // Using posts column in users query
252
+ await db.selectFrom(users).where(eq(posts.userId, 'u1')).execute(); // ERROR
253
+ ```
254
+
255
+ Correct:
256
+
257
+ ```ts
258
+ await db.selectFrom(users).where(eq(users.id, 'u1')).execute();
259
+ ```
260
+
261
+ The builder validates column ownership — columns in WHERE must belong to the queried table(s).
262
+
263
+ Source: src/query/builder.ts
264
+
265
+ ## References
266
+
267
+ - [Full condition operators reference](references/condition-operators.md)
@@ -0,0 +1,220 @@
1
+ # Condition Operators Reference
2
+
3
+ Detailed reference for all condition operators in flint-orm.
4
+
5
+ All conditions are imported from `flint-orm/expressions`.
6
+
7
+ ## Comparison Operators
8
+
9
+ ### eq(column, value)
10
+
11
+ Equal comparison.
12
+
13
+ ```ts
14
+ import { eq } from 'flint-orm/expressions';
15
+
16
+ // Value comparison
17
+ eq(users.name, 'Alice') // name = 'Alice'
18
+
19
+ // Column-to-column comparison
20
+ eq(posts.userId, users.id) // posts.userId = users.id
21
+ ```
22
+
23
+ ### neq(column, value)
24
+
25
+ Not equal comparison.
26
+
27
+ ```ts
28
+ import { neq } from 'flint-orm/expressions';
29
+
30
+ neq(users.id, 'excluded') // id != 'excluded'
31
+ ```
32
+
33
+ ### gt(column, value)
34
+
35
+ Greater than.
36
+
37
+ ```ts
38
+ import { gt } from 'flint-orm/expressions';
39
+
40
+ gt(users.age, 18) // age > 18
41
+ ```
42
+
43
+ ### gte(column, value)
44
+
45
+ Greater than or equal.
46
+
47
+ ```ts
48
+ import { gte } from 'flint-orm/expressions';
49
+
50
+ gte(users.age, 18) // age >= 18
51
+ ```
52
+
53
+ ### lt(column, value)
54
+
55
+ Less than.
56
+
57
+ ```ts
58
+ import { lt } from 'flint-orm/expressions';
59
+
60
+ lt(users.age, 65) // age < 65
61
+ ```
62
+
63
+ ### lte(column, value)
64
+
65
+ Less than or equal.
66
+
67
+ ```ts
68
+ import { lte } from 'flint-orm/expressions';
69
+
70
+ lte(users.age, 65) // age <= 65
71
+ ```
72
+
73
+ ## Range Operator
74
+
75
+ ### between(column, low, high)
76
+
77
+ Between two values (inclusive).
78
+
79
+ ```ts
80
+ import { between } from 'flint-orm/expressions';
81
+
82
+ between(users.age, 18, 65) // age BETWEEN 18 AND 65
83
+ ```
84
+
85
+ ## Null Check Operators
86
+
87
+ ### isNull(column)
88
+
89
+ Check for NULL.
90
+
91
+ ```ts
92
+ import { isNull } from 'flint-orm/expressions';
93
+
94
+ isNull(users.deletedAt) // deletedAt IS NULL
95
+ ```
96
+
97
+ ### isNotNull(column)
98
+
99
+ Check for NOT NULL.
100
+
101
+ ```ts
102
+ import { isNotNull } from 'flint-orm/expressions';
103
+
104
+ isNotNull(users.email) // email IS NOT NULL
105
+ ```
106
+
107
+ ## Array Operators
108
+
109
+ ### isIn(column, values)
110
+
111
+ Check if value is in array.
112
+
113
+ ```ts
114
+ import { isIn } from 'flint-orm/expressions';
115
+
116
+ isIn(users.id, ['u1', 'u2', 'u3']) // id IN ('u1', 'u2', 'u3')
117
+ ```
118
+
119
+ ### isNotIn(column, values)
120
+
121
+ Check if value is not in array.
122
+
123
+ ```ts
124
+ import { isNotIn } from 'flint-orm/expressions';
125
+
126
+ isNotIn(users.id, ['excluded']) // id NOT IN ('excluded')
127
+ ```
128
+
129
+ ## Pattern Matching Operators
130
+
131
+ ### like(column, pattern)
132
+
133
+ SQL LIKE pattern matching (% = any characters, _ = single char, case-insensitive).
134
+
135
+ ```ts
136
+ import { like } from 'flint-orm/expressions';
137
+
138
+ like(users.name, 'A%') // name LIKE 'A%'
139
+ ```
140
+
141
+ ### glob(column, pattern)
142
+
143
+ SQL GLOB pattern matching (* = any characters, ? = single char, case-sensitive).
144
+
145
+ ```ts
146
+ import { glob } from 'flint-orm/expressions';
147
+
148
+ glob(users.name, 'A*') // name GLOB 'A*'
149
+ ```
150
+
151
+ ## Logical Operators
152
+
153
+ ### and(...conditions)
154
+
155
+ Logical AND.
156
+
157
+ ```ts
158
+ import { and, eq, gt } from 'flint-orm/expressions';
159
+
160
+ and(eq(users.active, true), gt(users.age, 18))
161
+ // active = 1 AND age > 18
162
+ ```
163
+
164
+ ### or(...conditions)
165
+
166
+ Logical OR (wraps in parentheses).
167
+
168
+ ```ts
169
+ import { or, eq } from 'flint-orm/expressions';
170
+
171
+ or(eq(users.role, 'admin'), eq(users.role, 'moderator'))
172
+ // (role = 'admin' OR role = 'moderator')
173
+ ```
174
+
175
+ ## Composing Conditions
176
+
177
+ Conditions can be nested and combined:
178
+
179
+ ```ts
180
+ import { and, or, eq, gt, lte, isIn } from 'flint-orm/expressions';
181
+
182
+ // Complex condition
183
+ and(
184
+ or(eq(users.role, 'admin'), eq(users.role, 'moderator')),
185
+ gt(users.age, 18),
186
+ isIn(users.status, ['active', 'pending'])
187
+ )
188
+ // ((role = 'admin' OR role = 'moderator') AND age > 18 AND status IN ('active', 'pending'))
189
+ ```
190
+
191
+ ## Column-to-Column Comparisons
192
+
193
+ `eq` supports comparing two columns:
194
+
195
+ ```ts
196
+ import { eq } from 'flint-orm/expressions';
197
+
198
+ // Compare columns from different tables (useful for joins)
199
+ eq(posts.userId, users.id)
200
+ ```
201
+
202
+ ## Operator Summary
203
+
204
+ | Operator | SQL | Notes |
205
+ |----------|-----|-------|
206
+ | `eq` | `=` | Supports column-to-column |
207
+ | `neq` | `!=` | |
208
+ | `gt` | `>` | |
209
+ | `gte` | `>=` | |
210
+ | `lt` | `<` | |
211
+ | `lte` | `<=` | |
212
+ | `between` | `BETWEEN` | Inclusive range |
213
+ | `isNull` | `IS NULL` | |
214
+ | `isNotNull` | `IS NOT NULL` | |
215
+ | `isIn` | `IN` | Array of values |
216
+ | `isNotIn` | `NOT IN` | Array of values |
217
+ | `like` | `LIKE` | % and _ wildcards, case-insensitive |
218
+ | `glob` | `GLOB` | * and ? wildcards, case-sensitive |
219
+ | `and` | `AND` | Composable |
220
+ | `or` | `OR` | Wraps in parentheses |