@remix-run/data-table 0.0.0 → 0.2.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +549 -2
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +7 -0
  6. package/dist/lib/adapter.d.ts +550 -0
  7. package/dist/lib/adapter.d.ts.map +1 -0
  8. package/dist/lib/adapter.js +1 -0
  9. package/dist/lib/column.d.ts +193 -0
  10. package/dist/lib/column.d.ts.map +1 -0
  11. package/dist/lib/column.js +302 -0
  12. package/dist/lib/database/execution-context.d.ts +10 -0
  13. package/dist/lib/database/execution-context.d.ts.map +1 -0
  14. package/dist/lib/database/execution-context.js +1 -0
  15. package/dist/lib/database/helpers.d.ts +26 -0
  16. package/dist/lib/database/helpers.d.ts.map +1 -0
  17. package/dist/lib/database/helpers.js +116 -0
  18. package/dist/lib/database/query-execution.d.ts +7 -0
  19. package/dist/lib/database/query-execution.d.ts.map +1 -0
  20. package/dist/lib/database/query-execution.js +401 -0
  21. package/dist/lib/database/relations.d.ts +4 -0
  22. package/dist/lib/database/relations.d.ts.map +1 -0
  23. package/dist/lib/database/relations.js +207 -0
  24. package/dist/lib/database/write-lifecycle.d.ts +13 -0
  25. package/dist/lib/database/write-lifecycle.d.ts.map +1 -0
  26. package/dist/lib/database/write-lifecycle.js +279 -0
  27. package/dist/lib/database.d.ts +264 -0
  28. package/dist/lib/database.d.ts.map +1 -0
  29. package/dist/lib/database.js +319 -0
  30. package/dist/lib/errors.d.ts +59 -0
  31. package/dist/lib/errors.d.ts.map +1 -0
  32. package/dist/lib/errors.js +76 -0
  33. package/dist/lib/inflection.d.ts +3 -0
  34. package/dist/lib/inflection.d.ts.map +1 -0
  35. package/dist/lib/inflection.js +56 -0
  36. package/dist/lib/migrations/filename.d.ts +12 -0
  37. package/dist/lib/migrations/filename.d.ts.map +1 -0
  38. package/dist/lib/migrations/filename.js +20 -0
  39. package/dist/lib/migrations/helpers.d.ts +11 -0
  40. package/dist/lib/migrations/helpers.d.ts.map +1 -0
  41. package/dist/lib/migrations/helpers.js +77 -0
  42. package/dist/lib/migrations/journal-store.d.ts +15 -0
  43. package/dist/lib/migrations/journal-store.d.ts.map +1 -0
  44. package/dist/lib/migrations/journal-store.js +83 -0
  45. package/dist/lib/migrations/registry.d.ts +27 -0
  46. package/dist/lib/migrations/registry.d.ts.map +1 -0
  47. package/dist/lib/migrations/registry.js +51 -0
  48. package/dist/lib/migrations/runner.d.ts +20 -0
  49. package/dist/lib/migrations/runner.d.ts.map +1 -0
  50. package/dist/lib/migrations/runner.js +273 -0
  51. package/dist/lib/migrations/schema-api.d.ts +7 -0
  52. package/dist/lib/migrations/schema-api.d.ts.map +1 -0
  53. package/dist/lib/migrations/schema-api.js +326 -0
  54. package/dist/lib/migrations-node.d.ts +17 -0
  55. package/dist/lib/migrations-node.d.ts.map +1 -0
  56. package/dist/lib/migrations-node.js +65 -0
  57. package/dist/lib/migrations.d.ts +292 -0
  58. package/dist/lib/migrations.d.ts.map +1 -0
  59. package/dist/lib/migrations.js +38 -0
  60. package/dist/lib/operators.d.ts +154 -0
  61. package/dist/lib/operators.d.ts.map +1 -0
  62. package/dist/lib/operators.js +218 -0
  63. package/dist/lib/query.d.ts +159 -0
  64. package/dist/lib/query.d.ts.map +1 -0
  65. package/dist/lib/query.js +401 -0
  66. package/dist/lib/references.d.ts +41 -0
  67. package/dist/lib/references.d.ts.map +1 -0
  68. package/dist/lib/references.js +33 -0
  69. package/dist/lib/sql-helpers.d.ts +50 -0
  70. package/dist/lib/sql-helpers.d.ts.map +1 -0
  71. package/dist/lib/sql-helpers.js +111 -0
  72. package/dist/lib/sql.d.ts +45 -0
  73. package/dist/lib/sql.d.ts.map +1 -0
  74. package/dist/lib/sql.js +65 -0
  75. package/dist/lib/table.d.ts +569 -0
  76. package/dist/lib/table.d.ts.map +1 -0
  77. package/dist/lib/table.js +519 -0
  78. package/dist/lib/types.d.ts +4 -0
  79. package/dist/lib/types.d.ts.map +1 -0
  80. package/dist/lib/types.js +1 -0
  81. package/dist/migrations/node.d.ts +2 -0
  82. package/dist/migrations/node.d.ts.map +1 -0
  83. package/dist/migrations/node.js +1 -0
  84. package/dist/migrations.d.ts +8 -0
  85. package/dist/migrations.d.ts.map +1 -0
  86. package/dist/migrations.js +5 -0
  87. package/dist/operators.d.ts +3 -0
  88. package/dist/operators.d.ts.map +1 -0
  89. package/dist/operators.js +1 -0
  90. package/dist/sql-helpers.d.ts +3 -0
  91. package/dist/sql-helpers.d.ts.map +1 -0
  92. package/dist/sql-helpers.js +1 -0
  93. package/package.json +56 -7
  94. package/src/index.ts +198 -0
  95. package/src/lib/adapter.ts +653 -0
  96. package/src/lib/column.ts +384 -0
  97. package/src/lib/database/execution-context.ts +15 -0
  98. package/src/lib/database/helpers.ts +216 -0
  99. package/src/lib/database/query-execution.ts +638 -0
  100. package/src/lib/database/relations.ts +332 -0
  101. package/src/lib/database/write-lifecycle.ts +487 -0
  102. package/src/lib/database.ts +856 -0
  103. package/src/lib/errors.ts +119 -0
  104. package/src/lib/inflection.ts +69 -0
  105. package/src/lib/migrations/filename.ts +25 -0
  106. package/src/lib/migrations/helpers.ts +108 -0
  107. package/src/lib/migrations/journal-store.ts +122 -0
  108. package/src/lib/migrations/registry.ts +62 -0
  109. package/src/lib/migrations/runner.ts +374 -0
  110. package/src/lib/migrations/schema-api.ts +417 -0
  111. package/src/lib/migrations-node.ts +71 -0
  112. package/src/lib/migrations.ts +328 -0
  113. package/src/lib/operators.ts +436 -0
  114. package/src/lib/query.ts +958 -0
  115. package/src/lib/references.ts +78 -0
  116. package/src/lib/sql-helpers.ts +146 -0
  117. package/src/lib/sql.ts +84 -0
  118. package/src/lib/table.ts +1309 -0
  119. package/src/lib/types.ts +3 -0
  120. package/src/migrations/node.ts +1 -0
  121. package/src/migrations.ts +26 -0
  122. package/src/operators.ts +18 -0
  123. package/src/sql-helpers.ts +9 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,550 @@
1
- # Placeholder package
1
+ # data-table
2
2
 
3
- This package is intentionally empty and published only as a temporary placeholder.
3
+ Typed relational query toolkit for JavaScript runtimes.
4
+
5
+ ## Features
6
+
7
+ - **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite adapters
8
+ - **One Query API**: Build reusable `Query` objects with `query(table)` and execute them with `db.exec(...)`, or use `db.query(table)` as shorthand
9
+ - **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys
10
+ - **Optional Runtime Validation**: Add `validate(context)` at the table level for create/update validation and coercion
11
+ - **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading
12
+ - **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction
13
+ - **First-Class Migrations**: Up/down migrations with schema builders, runner controls, and dry-run planning
14
+ - **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)`
15
+
16
+ `data-table` gives you two complementary APIs:
17
+
18
+ - [**Query Objects**](#query-objects) for expressive joins, aggregates, eager loading, and scoped writes
19
+ - [**CRUD Helpers**](#crud-helpers) for common create/read/update/delete flows (`find`, `create`, `update`, `delete`)
20
+
21
+ Both APIs are type-safe. Runtime validation is opt-in with table-level `validate(context)`.
22
+
23
+ ## Installation
24
+
25
+ ```sh
26
+ npm i remix
27
+ npm i pg
28
+ # or
29
+ npm i mysql2
30
+ # or
31
+ npm i better-sqlite3
32
+ ```
33
+
34
+ ## Setup
35
+
36
+ Define tables once, then create a database with an adapter.
37
+
38
+ ```ts
39
+ import { Pool } from 'pg'
40
+ import { column as c, createDatabase, hasMany, query, table } from 'remix/data-table'
41
+ import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'
42
+
43
+ let users = table({
44
+ name: 'users',
45
+ columns: {
46
+ id: c.uuid(),
47
+ email: c.varchar(255),
48
+ role: c.enum(['customer', 'admin']),
49
+ created_at: c.integer(),
50
+ },
51
+ })
52
+
53
+ let orders = table({
54
+ name: 'orders',
55
+ columns: {
56
+ id: c.uuid(),
57
+ user_id: c.uuid(),
58
+ status: c.enum(['pending', 'processing', 'shipped', 'delivered']),
59
+ total: c.decimal(10, 2),
60
+ created_at: c.integer(),
61
+ },
62
+ })
63
+
64
+ let userOrders = hasMany(users, orders)
65
+
66
+ let pool = new Pool({ connectionString: process.env.DATABASE_URL })
67
+ let db = createDatabase(createPostgresDatabaseAdapter(pool))
68
+ ```
69
+
70
+ ## Query Objects
71
+
72
+ Use `query(table)` when you want to build a standalone reusable query object. Execute it later with `db.exec(query)`. Use `db.query(table)` when you want the same chainable `Query` already bound to a database instance.
73
+
74
+ ### Standalone Query Builder
75
+
76
+ `query(table)` is the primary query-builder API. It gives you an unbound `Query` value that can be composed, stored, reused, and executed against any compatible database instance.
77
+
78
+ ```ts
79
+ import { eq, ilike, query } from 'remix/data-table'
80
+
81
+ let pendingOrdersForExampleUsers = query(orders)
82
+ .join(users, eq(orders.user_id, users.id))
83
+ .where({ status: 'pending' })
84
+ .where(ilike(users.email, '%@example.com'))
85
+ .select({
86
+ orderId: orders.id,
87
+ customerEmail: users.email,
88
+ total: orders.total,
89
+ placedAt: orders.created_at,
90
+ })
91
+ .orderBy(orders.created_at, 'desc')
92
+ .limit(20)
93
+
94
+ let recentPendingOrders = await db.exec(pendingOrdersForExampleUsers)
95
+ ```
96
+
97
+ Unbound queries stay lazy until you pass them to `db.exec(...)`:
98
+
99
+ ```ts
100
+ let shippedCustomerQuery = query(users)
101
+ .where({ role: 'customer' })
102
+ .with({
103
+ recentOrders: userOrders.where({ status: 'shipped' }).orderBy('created_at', 'desc').limit(3),
104
+ })
105
+
106
+ let customers = await db.exec(shippedCustomerQuery)
107
+
108
+ // customers[0].recentOrders is fully typed
109
+ ```
110
+
111
+ The same standalone query builder also handles terminal read and write operations:
112
+
113
+ ```ts
114
+ let nextPendingOrder = await db.exec(
115
+ query(orders).where({ status: 'pending' }).orderBy('created_at', 'asc').first(),
116
+ )
117
+
118
+ await db.exec(
119
+ query(orders)
120
+ .where({ status: 'pending' })
121
+ .orderBy('created_at', 'asc')
122
+ .limit(100)
123
+ .update({ status: 'processing' }),
124
+ )
125
+ ```
126
+
127
+ ### Bound Query Shorthand
128
+
129
+ If you already have a `db` instance in hand and do not need a standalone query value, `db.query(table)` returns the same query builder already bound to that database:
130
+
131
+ ```ts
132
+ let recentPendingOrders = await db
133
+ .query(orders)
134
+ .where({ status: 'pending' })
135
+ .orderBy('created_at', 'desc')
136
+ .limit(20)
137
+ .all()
138
+ ```
139
+
140
+ ## CRUD Helpers
141
+
142
+ `data-table` provides helpers for common create/read/update/delete operations. Use these helpers for common operations without building a full query chain.
143
+
144
+ ### Read operations
145
+
146
+ ```ts
147
+ import { or } from 'remix/data-table'
148
+
149
+ let user = await db.find(users, 'u_001')
150
+
151
+ let firstPending = await db.findOne(orders, {
152
+ where: { status: 'pending' },
153
+ orderBy: ['created_at', 'asc'],
154
+ })
155
+
156
+ let page = await db.findMany(orders, {
157
+ where: or({ status: 'pending' }, { status: 'processing' }),
158
+ orderBy: [
159
+ ['status', 'asc'],
160
+ ['created_at', 'desc'],
161
+ ],
162
+ limit: 50,
163
+ offset: 0,
164
+ })
165
+ ```
166
+
167
+ `where` accepts the same single-table object/predicate inputs as `query().where(...)`, and `orderBy` uses tuple form:
168
+
169
+ - `['column', 'asc' | 'desc']`
170
+ - `[['columnA', 'asc'], ['columnB', 'desc']]`
171
+
172
+ ### Create helpers
173
+
174
+ ```ts
175
+ // Default: metadata (affectedRows/insertId)
176
+ let createResult = await db.create(users, {
177
+ id: 'u_002',
178
+ email: 'sam@example.com',
179
+ role: 'customer',
180
+ created_at: Date.now(),
181
+ })
182
+
183
+ // Return a typed row (with optional relations)
184
+ let createdUser = await db.create(
185
+ users,
186
+ {
187
+ id: 'u_003',
188
+ email: 'pat@example.com',
189
+ role: 'customer',
190
+ created_at: Date.now(),
191
+ },
192
+ {
193
+ returnRow: true,
194
+ with: { recentOrders: userOrders.orderBy('created_at', 'desc').limit(1) },
195
+ },
196
+ )
197
+
198
+ // Bulk insert metadata
199
+ let createManyResult = await db.createMany(orders, [
200
+ { id: 'o_101', user_id: 'u_002', status: 'pending', total: 24.99, created_at: Date.now() },
201
+ { id: 'o_102', user_id: 'u_003', status: 'pending', total: 48.5, created_at: Date.now() },
202
+ ])
203
+
204
+ // Return inserted rows (requires adapter RETURNING support)
205
+ let insertedRows = await db.createMany(
206
+ orders,
207
+ [{ id: 'o_103', user_id: 'u_003', status: 'pending', total: 12, created_at: Date.now() }],
208
+ { returnRows: true },
209
+ )
210
+ ```
211
+
212
+ `createMany`/`insertMany` throw when every row in the batch is empty (no explicit values).
213
+
214
+ ### Update and delete helpers
215
+
216
+ ```ts
217
+ let updatedUser = await db.update(users, 'u_003', { role: 'admin' })
218
+
219
+ let updateManyResult = await db.updateMany(
220
+ orders,
221
+ { status: 'processing' },
222
+ {
223
+ where: { status: 'pending' },
224
+ orderBy: ['created_at', 'asc'],
225
+ limit: 25,
226
+ },
227
+ )
228
+
229
+ let deletedUser = await db.delete(users, 'u_002')
230
+
231
+ let deleteManyResult = await db.deleteMany(orders, {
232
+ where: { status: 'delivered' },
233
+ orderBy: [['created_at', 'asc']],
234
+ limit: 200,
235
+ })
236
+ ```
237
+
238
+ `db.update(...)` throws when the target row cannot be found.
239
+
240
+ Return behavior:
241
+
242
+ - `find`/`findOne` -> row or `null`
243
+ - `findMany` -> rows
244
+ - `create` -> `WriteResult` by default, row when `returnRow: true`
245
+ - `createMany` -> `WriteResult` by default, rows when `returnRows: true` (not supported in MySQL because it doesn't support `RETURNING`)
246
+ - `update` -> updated row (throws when target row is missing)
247
+ - `updateMany`/`deleteMany` -> `WriteResult`
248
+ - `delete` -> `boolean`
249
+
250
+ ### Validation and Lifecycle
251
+
252
+ Validation is optional and table-scoped. Define `validate(context)` to validate/coerce write
253
+ payloads, and add lifecycle callbacks when you need custom read/write/delete behavior.
254
+
255
+ ```ts
256
+ import { column as c, fail, table } from 'remix/data-table'
257
+
258
+ let payments = table({
259
+ name: 'payments',
260
+ columns: {
261
+ id: c.uuid(),
262
+ amount: c.decimal(10, 2),
263
+ },
264
+ beforeWrite({ value }) {
265
+ return {
266
+ value: {
267
+ ...value,
268
+ amount: typeof value.amount === 'string' ? value.amount.trim() : value.amount,
269
+ },
270
+ }
271
+ },
272
+ validate({ operation, value }) {
273
+ if (operation === 'create' && typeof value.amount === 'string') {
274
+ let amount = Number(value.amount)
275
+
276
+ if (!Number.isFinite(amount)) {
277
+ return fail('Expected a numeric amount', ['amount'])
278
+ }
279
+
280
+ return { value: { ...value, amount } }
281
+ }
282
+
283
+ return { value }
284
+ },
285
+ beforeDelete({ where }) {
286
+ if (where.length === 0) {
287
+ return fail('Refusing unscoped delete')
288
+ }
289
+ },
290
+ afterRead({ value }) {
291
+ if (!('amount' in value)) {
292
+ return { value }
293
+ }
294
+
295
+ return {
296
+ value: {
297
+ ...value,
298
+ // Example read-time shaping
299
+ amount:
300
+ typeof value.amount === 'number' ? Math.round(value.amount * 100) / 100 : value.amount,
301
+ },
302
+ }
303
+ },
304
+ })
305
+ ```
306
+
307
+ Use `fail(...)` in hooks when you want to return issues without manually building `{ issues: [...] }`.
308
+
309
+ Validation and lifecycle semantics:
310
+
311
+ - Write order is `beforeWrite -> validate -> timestamp/default touch -> execute -> afterWrite`
312
+ - `validate` runs for writes (`create`, `createMany`, `insert`, `insertMany`, `update`, `updateMany`, `upsert`)
313
+ - Hook context includes `{ operation: 'create' | 'update', tableName, value }`
314
+ - Write payloads are partial objects
315
+ - Unknown columns fail validation before and after hook processing
316
+ - `beforeDelete` can veto deletes by returning `{ issues }`
317
+ - `afterDelete` runs after successful deletes with `affectedRows`
318
+ - `afterRead` runs for each loaded row (root rows, eager-loaded relation rows, and write-returning rows)
319
+ - `afterRead` receives the current read shape, which may be partial/projection rows; guard field access accordingly
320
+ - Predicate values (`where`, `having`, join predicates) are not runtime-validated
321
+ - Lifecycle callbacks are synchronous; returning a Promise throws a validation error
322
+ - Callback validation errors include `metadata.source` (`beforeWrite`, `validate`, `beforeDelete`, `afterRead`, etc.) for easier debugging
323
+ - Callbacks do not introduce implicit transactions (use `db.transaction(...)` when you need rollback guarantees)
324
+
325
+ ## Transactions
326
+
327
+ ```ts
328
+ await db.transaction(async (tx) => {
329
+ let user = await tx.create(
330
+ users,
331
+ { id: 'u_010', email: 'new@example.com', role: 'customer', created_at: Date.now() },
332
+ { returnRow: true },
333
+ )
334
+
335
+ await tx.create(orders, {
336
+ id: 'o_500',
337
+ user_id: user.id,
338
+ status: 'pending',
339
+ total: 79,
340
+ created_at: Date.now(),
341
+ })
342
+ })
343
+ ```
344
+
345
+ ## Migrations
346
+
347
+ `data-table` includes a first-class migration system under `remix/data-table/migrations`.
348
+ Migrations are adapter-driven: adapters execute SQL for their dialect/runtime, and SQL compilation
349
+ is handled by adapter-owned compilers (with optional shared pure helpers from `data-table`).
350
+ For adapter authors (including third-party adapters), shared SQL helper utilities are available at
351
+ `remix/data-table/sql-helpers`.
352
+
353
+ ### Example Setup
354
+
355
+ ```txt
356
+ app/
357
+ db/
358
+ migrations/
359
+ 20260228090000_create_users.ts
360
+ 20260301113000_add_user_status.ts
361
+ migrate.ts
362
+ ```
363
+
364
+ - Keep migration files in one directory (for example `app/db/migrations`).
365
+ - Name each file as `YYYYMMDDHHmmss_name.ts` (or `.js`, `.mjs`, `.cjs`, `.cts`).
366
+ - Each file must `default` export `createMigration(...)`; `id` and `name` are inferred from filename.
367
+
368
+ ### Migration File Example
369
+
370
+ ```ts
371
+ import { column as c, table } from 'remix/data-table'
372
+ import { createMigration } from 'remix/data-table/migrations'
373
+
374
+ let users = table({
375
+ name: 'users',
376
+ columns: {
377
+ id: c.integer().primaryKey(),
378
+ email: c.varchar(255).notNull().unique(),
379
+ created_at: c.timestamp({ withTimezone: true }).defaultNow(),
380
+ },
381
+ })
382
+
383
+ export default createMigration({
384
+ async up({ db, schema }) {
385
+ await schema.createTable(users)
386
+ await schema.createIndex(users, 'email', { unique: true })
387
+
388
+ if (db.adapter.dialect === 'sqlite') {
389
+ await db.exec('pragma foreign_keys = on')
390
+ }
391
+ },
392
+ async down({ schema }) {
393
+ await schema.dropTable(users, { ifExists: true })
394
+ },
395
+ })
396
+ ```
397
+
398
+ ### Runner Script Example
399
+
400
+ In `app/db/migrate.ts`:
401
+
402
+ ```ts
403
+ import path from 'node:path'
404
+ import { Pool } from 'pg'
405
+ import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'
406
+ import { createMigrationRunner } from 'remix/data-table/migrations'
407
+ import { loadMigrations } from 'remix/data-table/migrations/node'
408
+
409
+ let directionArg = process.argv[2] ?? 'up'
410
+ let direction = directionArg === 'down' ? 'down' : 'up'
411
+ let to = process.argv[3]
412
+
413
+ let pool = new Pool({ connectionString: process.env.DATABASE_URL })
414
+ let adapter = createPostgresDatabaseAdapter(pool)
415
+ let migrations = await loadMigrations(path.resolve('app/db/migrations'))
416
+ let runner = createMigrationRunner(adapter, migrations)
417
+
418
+ try {
419
+ let result = direction === 'up' ? await runner.up({ to }) : await runner.down({ to })
420
+ console.log(direction + ' complete', {
421
+ applied: result.applied.map((entry) => entry.id),
422
+ reverted: result.reverted.map((entry) => entry.id),
423
+ })
424
+ } finally {
425
+ await pool.end()
426
+ }
427
+ ```
428
+
429
+ Use `journalTable` if you want a custom migrations journal table name:
430
+
431
+ ```ts
432
+ let runner = createMigrationRunner(adapter, migrations, {
433
+ journalTable: 'app_migrations',
434
+ })
435
+ ```
436
+
437
+ Run it with your runtime, for example:
438
+
439
+ ```sh
440
+ node ./app/db/migrate.ts up
441
+ node ./app/db/migrate.ts up 20260301113000
442
+ node ./app/db/migrate.ts down
443
+ node ./app/db/migrate.ts down 20260228090000
444
+ ```
445
+
446
+ Use `step` when you want bounded rollforward/rollback behavior instead of a target id:
447
+
448
+ ```ts
449
+ await runner.up({ step: 1 })
450
+ await runner.down({ step: 1 })
451
+ ```
452
+
453
+ `to` and `step` are mutually exclusive. Use one or the other for a given run.
454
+
455
+ Use `dryRun` to compile and inspect the SQL plan without applying migrations:
456
+
457
+ ```ts
458
+ let dryRunResult = await runner.up({ dryRun: true })
459
+ console.log(dryRunResult.sql)
460
+ ```
461
+
462
+ When migration transactions are enabled, migration-time `schema.createTable(...)`, `db.exec(...)`,
463
+ query-builder data operations, and `schema.hasTable(...)` / `schema.hasColumn(...)` all run in the same
464
+ adapter transaction context.
465
+
466
+ You can also pass a pre-built SQL statement into `schema.plan(...)` when authoring migrations:
467
+
468
+ ```ts
469
+ import { sql } from 'remix/data-table'
470
+
471
+ await schema.plan(sql`update users set status = ${'active'} where status is null`)
472
+ ```
473
+
474
+ You can run lightweight schema checks inside a migration with `schema.hasTable(...)` and
475
+ `schema.hasColumn(...)` when you need defensive conditional behavior. Methods that take a table name
476
+ accept either a string (`'app.users'`) or a `table(...)` object.
477
+
478
+ In `dryRun` mode, introspection methods still check the live database state. They do not simulate
479
+ tables/columns from pending operations in the current dry-run plan.
480
+
481
+ For key-oriented migration APIs, single-column and compound forms are both supported:
482
+
483
+ ```ts
484
+ await schema.alterTable(users, (table) => {
485
+ table.addPrimaryKey('id')
486
+ table.addForeignKey('account_id', 'accounts', 'id')
487
+ table.addForeignKey(['tenant_id', 'account_id'], 'accounts', ['tenant_id', 'id'])
488
+ })
489
+ ```
490
+
491
+ Constraint and index names are optional in migration APIs. When omitted, `data-table` generates
492
+ deterministic names for primary keys, uniques, foreign keys, checks, and indexes.
493
+
494
+ This is useful when you want to:
495
+
496
+ - Review generated SQL in CI before deploying
497
+ - Verify migration ordering and target/step selection
498
+ - Audit dialect-specific SQL differences across adapters
499
+
500
+ For non-filesystem runtimes, register migrations manually:
501
+
502
+ ```ts
503
+ import { createMigrationRegistry, createMigrationRunner } from 'remix/data-table/migrations'
504
+ import createUsers from './db/migrations/20260228090000_create_users.ts'
505
+
506
+ let registry = createMigrationRegistry()
507
+ registry.register({ id: '20260228090000', name: 'create_users', migration: createUsers })
508
+
509
+ // adapter from createPostgresDatabaseAdapter/createMysqlDatabaseAdapter/createSqliteDatabaseAdapter
510
+ let runner = createMigrationRunner(adapter, registry)
511
+ await runner.up()
512
+ ```
513
+
514
+ ## Raw SQL Escape Hatch
515
+
516
+ ```ts
517
+ import { rawSql, sql } from 'remix/data-table'
518
+
519
+ await db.exec(sql`select * from users where id = ${'u_001'}`)
520
+ await db.exec(rawSql('update users set role = ? where id = ?', ['admin', 'u_001']))
521
+ ```
522
+
523
+ Use `sql` when you need raw SQL plus safe value interpolation:
524
+
525
+ ```ts
526
+ import { sql } from 'remix/data-table'
527
+
528
+ let email = input.email
529
+ let minCreatedAt = input.minCreatedAt
530
+
531
+ let result = await db.exec(sql`
532
+ select id, email
533
+ from users
534
+ where email = ${email}
535
+ and created_at >= ${minCreatedAt}
536
+ `)
537
+ ```
538
+
539
+ `sql` keeps values parameterized per adapter dialect, so you can avoid manual string concatenation.
540
+
541
+ ## Related Packages
542
+
543
+ - [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Optional schema parsing you can use inside table-level `validate(...)` hooks
544
+ - [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
545
+ - [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter
546
+ - [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter
547
+
548
+ ## License
549
+
550
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,15 @@
1
+ export type { AdapterCapabilityOverrides, AdapterCapabilities, DataManipulationRequest, DataMigrationRequest, AddCheckChange, AddCheckOperation, AddColumnChange, AddForeignKeyChange, AddForeignKeyOperation, AddPrimaryKeyChange, AddUniqueChange, AlterTableChange, AlterTableOperation, ChangeColumnChange, CheckConstraint, ColumnCheck, ColumnComputed, ColumnDefault, ColumnDefinition, ColumnTypeName, CountOperation, CreateIndexOperation, CreateTableOperation, DataMigrationResult, DataMigrationOperation, DataManipulationResult, DataManipulationOperation, DeleteOperation, DatabaseAdapter, DropCheckChange, DropCheckOperation, DropColumnChange, DropForeignKeyChange, DropForeignKeyOperation, DropIndexOperation, DropPrimaryKeyChange, DropTableOperation, DropUniqueChange, ExistsOperation, ForeignKeyAction, ForeignKeyConstraint, IndexDefinition, IndexMethod, InsertManyOperation, InsertOperation, JoinClause, JoinType, PrimaryKeyConstraint, RawOperation, RenameColumnChange, RenameIndexOperation, RenameTableOperation, ReturningSelection, SelectColumn, SelectOperation, SetTableCommentChange, TableRef, TransactionOptions, TransactionToken, UniqueConstraint, UpdateOperation, UpsertOperation, } from './lib/adapter.ts';
2
+ export { DataTableAdapterError, DataTableConstraintError, DataTableError, DataTableQueryError, DataTableValidationError, } from './lib/errors.ts';
3
+ export type { AnyRelation, AnyColumn, AnyTable, BelongsToOptions, ColumnReference, ColumnReferenceForQualifiedName, HasManyOptions, HasManyThroughOptions, HasOneOptions, KeySelector, OrderByClause, OrderDirection, PrimaryKeyInput, Relation, RelationCardinality, RelationKind, RelationMapForTable, Table, TableAfterDelete, TableAfterDeleteContext, TableAfterRead, TableAfterReadContext, TableAfterReadResult, TableAfterWrite, TableAfterWriteContext, TableBeforeDelete, TableBeforeDeleteContext, TableBeforeDeleteResult, TableBeforeWrite, TableBeforeWriteContext, TableBeforeWriteResult, TableColumnInput, TableColumnName, TableColumns, TableLifecycleOperation, TableName, TablePrimaryKey, TableReference, TableRow, TableRowWith, TableColumnsDefinition, TableValidate, TableValidationContext, TableWriteOperation, TableValidationOperation, TableValidationResult, TimestampConfig, TimestampOptions, ValidationFailure, ValidationIssue, } from './lib/table.ts';
4
+ export { belongsTo, columnMetadataKey, fail, getTableColumns, getTableColumnDefinitions, getTableBeforeDelete, getTableBeforeWrite, getTableAfterDelete, getTableAfterRead, getTableAfterWrite, getTableName, getTablePrimaryKey, getTableReference, getTableTimestamps, getTableValidator, hasMany, hasManyThrough, hasOne, table, tableMetadataKey, timestamps, } from './lib/table.ts';
5
+ export type { ColumnNamespace } from './lib/column.ts';
6
+ export { ColumnBuilder, column } from './lib/column.ts';
7
+ export type { Predicate, WhereInput, WhereObject } from './lib/operators.ts';
8
+ export { and, between, eq, gt, gte, ilike, inList, isNull, like, lt, lte, ne, notInList, notNull, or, } from './lib/operators.ts';
9
+ export type { SqlStatement } from './lib/sql.ts';
10
+ export { rawSql, sql } from './lib/sql.ts';
11
+ export type { CountOptions, CreateManyResultOptions, CreateManyRowsOptions, CreateResultOptions, CreateRowOptions, DeleteManyOptions, FindManyOptions, FindOneOptions, OrderByInput, OrderByTuple, QueryColumnTypesForTable, QueryForTable, QueryTableInput, SingleTableColumn, SingleTableWhere, UpdateManyOptions, UpdateOptions, WriteResult, WriteRowResult, WriteRowsResult, } from './lib/database.ts';
12
+ export { createDatabase, Database } from './lib/database.ts';
13
+ export type { AnyQuery } from './lib/query.ts';
14
+ export { Query, query } from './lib/query.ts';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { DataTableAdapterError, DataTableConstraintError, DataTableError, DataTableQueryError, DataTableValidationError, } from "./lib/errors.js";
2
+ export { belongsTo, columnMetadataKey, fail, getTableColumns, getTableColumnDefinitions, getTableBeforeDelete, getTableBeforeWrite, getTableAfterDelete, getTableAfterRead, getTableAfterWrite, getTableName, getTablePrimaryKey, getTableReference, getTableTimestamps, getTableValidator, hasMany, hasManyThrough, hasOne, table, tableMetadataKey, timestamps, } from "./lib/table.js";
3
+ export { ColumnBuilder, column } from "./lib/column.js";
4
+ export { and, between, eq, gt, gte, ilike, inList, isNull, like, lt, lte, ne, notInList, notNull, or, } from "./lib/operators.js";
5
+ export { rawSql, sql } from "./lib/sql.js";
6
+ export { createDatabase, Database } from "./lib/database.js";
7
+ export { Query, query } from "./lib/query.js";