remix 3.0.0-beta.4 → 3.0.0-beta.6

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 (143) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
  7. package/dist/node-hmr/runtime.d.ts +2 -0
  8. package/dist/node-hmr/runtime.d.ts.map +1 -0
  9. package/dist/node-hmr/runtime.js +2 -0
  10. package/dist/node-hmr/types.d.ts +2 -0
  11. package/dist/node-hmr.d.ts +2 -0
  12. package/dist/node-hmr.d.ts.map +1 -0
  13. package/dist/{ui/glyph.js → node-hmr.js} +1 -1
  14. package/dist/ui/accordion/primitives.d.ts +2 -0
  15. package/dist/ui/accordion/primitives.d.ts.map +1 -0
  16. package/dist/ui/accordion/primitives.js +2 -0
  17. package/dist/ui/button.d.ts +1 -0
  18. package/dist/ui/button.d.ts.map +1 -1
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/checkbox.d.ts +3 -0
  21. package/dist/ui/checkbox.d.ts.map +1 -0
  22. package/dist/ui/checkbox.js +3 -0
  23. package/dist/ui/combobox/primitives.d.ts +2 -0
  24. package/dist/ui/combobox/primitives.d.ts.map +1 -0
  25. package/dist/ui/combobox/primitives.js +2 -0
  26. package/dist/ui/dev/refresh.d.ts +2 -0
  27. package/dist/ui/dev/refresh.d.ts.map +1 -0
  28. package/dist/ui/dev/refresh.js +2 -0
  29. package/dist/ui/input.d.ts +3 -0
  30. package/dist/ui/input.d.ts.map +1 -0
  31. package/dist/ui/input.js +3 -0
  32. package/dist/ui/menu/primitives.d.ts +2 -0
  33. package/dist/ui/menu/primitives.d.ts.map +1 -0
  34. package/dist/ui/menu/primitives.js +2 -0
  35. package/dist/ui/radio.d.ts +3 -0
  36. package/dist/ui/radio.d.ts.map +1 -0
  37. package/dist/ui/radio.js +3 -0
  38. package/dist/ui/select/primitives.d.ts +2 -0
  39. package/dist/ui/select/primitives.d.ts.map +1 -0
  40. package/dist/ui/select/primitives.js +2 -0
  41. package/dist/ui/tabs/primitives.d.ts +2 -0
  42. package/dist/ui/tabs/primitives.d.ts.map +1 -0
  43. package/dist/ui/tabs/primitives.js +2 -0
  44. package/dist/ui/tabs.d.ts +2 -0
  45. package/dist/ui/tabs.d.ts.map +1 -0
  46. package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
  47. package/dist/ui/toggle/primitives.d.ts +2 -0
  48. package/dist/ui/toggle/primitives.d.ts.map +1 -0
  49. package/dist/ui/toggle/primitives.js +2 -0
  50. package/dist/ui/toggle.d.ts +3 -0
  51. package/dist/ui/toggle.d.ts.map +1 -0
  52. package/dist/ui/toggle.js +3 -0
  53. package/dist/ui-hmr/assets.d.ts +2 -0
  54. package/dist/ui-hmr/assets.d.ts.map +1 -0
  55. package/dist/ui-hmr/assets.js +2 -0
  56. package/dist/ui-hmr/node.d.ts +3 -0
  57. package/dist/ui-hmr/node.d.ts.map +1 -0
  58. package/dist/ui-hmr/node.js +3 -0
  59. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  60. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  61. package/dist/ui-hmr/runtime/browser.js +2 -0
  62. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  63. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  64. package/dist/ui-hmr/runtime/server.js +2 -0
  65. package/dist/ui-hmr.d.ts +2 -0
  66. package/dist/ui-hmr.d.ts.map +1 -0
  67. package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
  68. package/package.json +122 -142
  69. package/src/assets/README.md +322 -56
  70. package/src/assets/types/hmr.d.ts +2 -0
  71. package/src/cli/README.md +105 -1
  72. package/src/cookie/README.md +4 -4
  73. package/src/data-table/README.md +202 -68
  74. package/src/data-table/cli.ts +2 -0
  75. package/src/data-table-mysql/README.md +46 -17
  76. package/src/data-table-postgres/README.md +39 -13
  77. package/src/data-table-sqlite/README.md +38 -20
  78. package/src/fetch-proxy/README.md +25 -0
  79. package/src/form-data-parser/README.md +4 -4
  80. package/src/mime/README.md +8 -1
  81. package/src/node-fetch-server/README.md +39 -13
  82. package/src/node-hmr/README.md +307 -0
  83. package/src/node-hmr/runtime.ts +2 -0
  84. package/src/node-hmr/types.d.ts +2 -0
  85. package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
  86. package/src/route-pattern/README.md +141 -13
  87. package/src/session/README.md +1 -1
  88. package/src/session-middleware/README.md +9 -7
  89. package/src/test/README.md +161 -115
  90. package/src/ui/README.md +116 -157
  91. package/src/ui/accordion/README.md +50 -14
  92. package/src/ui/accordion/primitives/README.md +202 -0
  93. package/src/ui/accordion/primitives.ts +2 -0
  94. package/src/ui/anchor/README.md +37 -2
  95. package/src/ui/breadcrumbs/README.md +4 -4
  96. package/src/ui/button/README.md +26 -26
  97. package/src/ui/button.ts +1 -0
  98. package/src/ui/checkbox/README.md +59 -0
  99. package/src/ui/checkbox.ts +3 -0
  100. package/src/ui/combobox/README.md +58 -9
  101. package/src/ui/combobox/primitives/README.md +194 -0
  102. package/src/ui/combobox/primitives.ts +2 -0
  103. package/src/ui/dev/refresh.ts +2 -0
  104. package/src/ui/input/README.md +52 -0
  105. package/src/ui/input.ts +3 -0
  106. package/src/ui/listbox/README.md +9 -41
  107. package/src/ui/menu/README.md +55 -14
  108. package/src/ui/menu/primitives/README.md +161 -0
  109. package/src/ui/menu/primitives.ts +2 -0
  110. package/src/ui/popover/README.md +20 -39
  111. package/src/ui/radio/README.md +53 -0
  112. package/src/ui/radio.ts +3 -0
  113. package/src/ui/select/README.md +29 -19
  114. package/src/ui/select/primitives/README.md +117 -0
  115. package/src/ui/select/primitives.ts +2 -0
  116. package/src/ui/tabs/README.md +141 -0
  117. package/src/ui/tabs/primitives/README.md +141 -0
  118. package/src/ui/tabs/primitives.ts +2 -0
  119. package/src/ui/tabs.ts +2 -0
  120. package/src/ui/test/README.md +151 -60
  121. package/src/ui/toggle/README.md +56 -0
  122. package/src/ui/toggle/primitives/README.md +56 -0
  123. package/src/ui/toggle/primitives.ts +2 -0
  124. package/src/ui/toggle.ts +3 -0
  125. package/src/ui-hmr/README.md +119 -0
  126. package/{dist/ui/separator.js → src/ui-hmr/assets.ts} +1 -1
  127. package/src/ui-hmr/node.ts +3 -0
  128. package/src/ui-hmr/runtime/browser.ts +2 -0
  129. package/src/ui-hmr/runtime/server.ts +2 -0
  130. package/src/ui-hmr.ts +2 -0
  131. package/dist/ui/glyph.d.ts +0 -2
  132. package/dist/ui/glyph.d.ts.map +0 -1
  133. package/dist/ui/scroll-lock.d.ts +0 -2
  134. package/dist/ui/scroll-lock.d.ts.map +0 -1
  135. package/dist/ui/separator.d.ts +0 -2
  136. package/dist/ui/separator.d.ts.map +0 -1
  137. package/dist/ui/theme.d.ts +0 -2
  138. package/dist/ui/theme.d.ts.map +0 -1
  139. package/src/ui/glyph/README.md +0 -72
  140. package/src/ui/scroll-lock/README.md +0 -33
  141. package/src/ui/scroll-lock.ts +0 -2
  142. package/src/ui/separator.ts +0 -2
  143. package/src/ui/theme/README.md +0 -103
package/src/cli/README.md CHANGED
@@ -7,9 +7,11 @@ Command-line interface for creating and managing Remix projects.
7
7
  - Create new Remix projects with `npx remix@next new` or installed `remix new`
8
8
  - Print shell completion scripts with `remix completion`
9
9
  - Check project environment and Remix app conventions with `remix doctor`
10
- - Create low-risk project and controller files with `remix doctor --fix`
10
+ - Apply available low-risk project fixes with `remix doctor --fix`
11
+ - Manage the current app database with `remix db`
11
12
  - Inspect the current app route tree with `remix routes`
12
13
  - Run project tests with `remix test`
14
+ - Configure commands with a static, commented `remix.json` file
13
15
  - Print the current Remix version with `remix version`
14
16
  - Use the same CLI through the `remix` package or the `remix/cli` API
15
17
  - Scaffold a starter app that matches the Remix project layout conventions
@@ -47,6 +49,9 @@ remix new my-remix-app
47
49
  remix completion bash >> ~/.bashrc
48
50
  remix doctor
49
51
  remix doctor --fix
52
+ remix db migrate
53
+ remix db status
54
+ remix db reset --force
50
55
  remix routes
51
56
  remix routes --table
52
57
  remix routes --table --no-headers
@@ -64,6 +69,9 @@ await runRemix(['new', 'my-remix-app'])
64
69
  await runRemix(['completion', 'bash'])
65
70
  await runRemix(['doctor'])
66
71
  await runRemix(['doctor', '--fix'])
72
+ await runRemix(['db', 'migrate'])
73
+ await runRemix(['db', 'status'])
74
+ await runRemix(['db', 'reset', '--force'])
67
75
  await runRemix(['routes'])
68
76
  await runRemix(['routes', '--table'])
69
77
  await runRemix(['routes', '--table', '--no-headers'])
@@ -71,8 +79,104 @@ await runRemix(['test'])
71
79
  await runRemix(['version'])
72
80
  ```
73
81
 
82
+ Destructive database commands (`remix db wipe` and `remix db reset`) refuse to run without `--force`.
83
+
74
84
  `runRemix()` returns the CLI exit code as a promise.
75
85
 
86
+ ## Configuration
87
+
88
+ The CLI loads an optional `remix.json`. The file is parsed as JSONC, so it may contain comments and
89
+ trailing commas. Every top-level field is optional:
90
+
91
+ ```jsonc
92
+ {
93
+ "$schema": "https://remix.run/schemas/remix.json",
94
+
95
+ "db": {
96
+ "adapter": {
97
+ "type": "sqlite",
98
+ "filename": { "env": "DATABASE_URL", "default": "./db/app.sqlite" },
99
+ "foreignKeys": true,
100
+ "busyTimeout": 5000,
101
+ },
102
+ "migrations": {
103
+ "directory": "./db/migrations",
104
+ "journalTable": "data_table_migrations",
105
+ },
106
+ "seed": "./db/seed.sql",
107
+ },
108
+
109
+ "doctor": {
110
+ "strict": true,
111
+ },
112
+
113
+ "test": {
114
+ // Test discovery
115
+ "files": ["**/*.test{,.browser,.e2e}.{ts,tsx}"],
116
+ "browserFiles": ["**/*.test.browser.{ts,tsx}"],
117
+ "e2eFiles": ["**/*.test.e2e.{ts,tsx}"],
118
+ "exclude": ["node_modules/**", "dist/**"],
119
+ "type": ["server", "browser", "e2e"],
120
+ "only": ["/checkout/i"],
121
+
122
+ // Test execution
123
+ "concurrency": 4,
124
+ "pool": "forks",
125
+ "setup": "./test/setup.ts",
126
+ "watch": false,
127
+
128
+ // Playwright
129
+ "playwright": {
130
+ "echo": false,
131
+ "open": false,
132
+ "configFile": "./playwright.config.ts",
133
+ "projects": ["chromium", "firefox"],
134
+ },
135
+
136
+ // Output
137
+ "reporter": "spec",
138
+ "quiet": false,
139
+
140
+ // Coverage
141
+ "coverage": {
142
+ "enabled": true,
143
+ "dir": ".coverage",
144
+ "include": ["app/**"],
145
+ "exclude": ["**/*.test.*"],
146
+ "branches": 80,
147
+ "functions": 80,
148
+ "lines": 80,
149
+ "statements": 80,
150
+ },
151
+ },
152
+ }
153
+ ```
154
+
155
+ Explicit command flags and positional arguments override configured values. Repeated flags replace
156
+ configured arrays, while nested Playwright and coverage settings merge by field. Relative paths and
157
+ globs are resolved from the directory containing the config file. Use `remix doctor --no-strict` to
158
+ disable configured strict mode for one run.
159
+
160
+ `remix db` requires `db.adapter`. Adapters use `type: "sqlite"`, `type: "postgres"`, or
161
+ `type: "mysql"`; PostgreSQL uses `connectionString` and MySQL uses `uri`. A connection value may be
162
+ a string or an object naming an environment variable with an optional default. `db.seed` names a
163
+ SQL file that `remix db seed` and `remix db reset` run against the database. Database flags such as
164
+ `--migrations`, `--seed`, `--journal-table`, and `--connection-env` override the corresponding
165
+ config for one invocation. When no global `--config` is provided, database commands find the
166
+ nearest `remix.json` by walking up from the working directory.
167
+
168
+ Use the global `--config` option to select another JSONC file. The option itself is resolved from the
169
+ CLI working directory and may appear before or after the command:
170
+
171
+ ```sh
172
+ remix --config ./config/remix.ci.json test
173
+ remix test --config ./config/remix.ci.json
174
+ ```
175
+
176
+ A missing default `remix.json` is ignored. A missing explicitly selected file, malformed JSONC,
177
+ unknown property, or invalid value is reported as a CLI error. The optional `$schema` field enables
178
+ editor completion and validation; it has no runtime effect.
179
+
76
180
  ## License
77
181
 
78
182
  See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -84,16 +84,16 @@ let response = new Response('Hello, world!', {
84
84
 
85
85
  ### Custom Encoding
86
86
 
87
- By default, [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) and [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) are used to encode and decode the cookie value. This is suitable for most use cases, but you can provide your own functions to customize the encoding and decoding of the cookie value.
87
+ By default, cookie values are percent-encoded and wrapped in base64 so arbitrary string values can be safely stored in cookies. This is suitable for most use cases, but you can provide your own functions to customize the encoding and decoding of the cookie value.
88
88
 
89
89
  ```tsx
90
90
  let sessionCookie = createCookie('session', {
91
- encode: (value) => value,
92
- decode: (value) => value,
91
+ encode: (value) => value.replaceAll(' ', '-'),
92
+ decode: (value) => value.replaceAll('-', ' '),
93
93
  })
94
94
  ```
95
95
 
96
- This can be useful for viewing the value of cookies in a human-readable format in the browser's developer tools. But you should be sure that the cookie value contains only characters that are [valid in a cookie value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#attributes).
96
+ Custom `encode` functions are used as the full cookie value codec. Their return values are signed when `secrets` are configured and then serialized as-is, without the default base64 wrapper. This can be useful for viewing the value of cookies in a human-readable format in the browser's developer tools. But you should be sure that the encoded cookie value contains only characters that are [valid in a cookie value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#attributes) and safe for HTTP headers. Raw Unicode characters should be escaped or encoded by your custom function before they are returned.
97
97
 
98
98
  ## Related Packages
99
99
 
@@ -4,13 +4,14 @@ Typed relational query toolkit for JavaScript runtimes.
4
4
 
5
5
  ## Features
6
6
 
7
- - **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite adapters
7
+ - **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite
8
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
9
  - **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys
10
10
  - **Optional Runtime Validation**: Add `validate(context)` at the table level for create/update validation and coercion
11
11
  - **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading
12
12
  - **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction
13
13
  - **First-Class Migrations**: Plain SQL `up.sql`/`down.sql` files with a journaling runner and dry-run planning
14
+ - **Database Lifecycle Commands**: Wipe, migrate, inspect, seed, and reset through `remix db`
14
15
  - **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)`
15
16
 
16
17
  `data-table` gives you two complementary APIs:
@@ -33,12 +34,11 @@ npm i mysql2
33
34
 
34
35
  ## Setup
35
36
 
36
- Define tables once, then create a database with an adapter.
37
+ Define tables once, then create a database for your SQL dialect.
37
38
 
38
39
  ```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'
40
+ import { column as c, hasMany, query, table } from 'remix/data-table'
41
+ import { createPostgresDatabase } from 'remix/data-table/postgres'
42
42
 
43
43
  let users = table({
44
44
  name: 'users',
@@ -63,8 +63,7 @@ let orders = table({
63
63
 
64
64
  let userOrders = hasMany(users, orders)
65
65
 
66
- let pool = new Pool({ connectionString: process.env.DATABASE_URL })
67
- let db = createDatabase(createPostgresDatabaseAdapter(pool))
66
+ let db = createPostgresDatabase({ connectionString: process.env.DATABASE_URL })
68
67
  ```
69
68
 
70
69
  ## Query Objects
@@ -201,7 +200,7 @@ let createManyResult = await db.createMany(orders, [
201
200
  { id: 'o_102', user_id: 'u_003', status: 'pending', total: 48.5, created_at: Date.now() },
202
201
  ])
203
202
 
204
- // Return inserted rows (requires adapter RETURNING support)
203
+ // Return inserted rows (requires database RETURNING support)
205
204
  let insertedRows = await db.createMany(
206
205
  orders,
207
206
  [{ id: 'o_103', user_id: 'u_003', status: 'pending', total: 12, created_at: Date.now() }],
@@ -343,7 +342,7 @@ await db.transaction(async (tx) => {
343
342
 
344
343
  ## Migrations
345
344
 
346
- `data-table` ships a SQL-first migration system under `remix/data-table/migrations`. Each migration is a directory containing hand-written `up.sql` and (optionally) `down.sql`. The runner journals applied migrations, detects checksum drift, and wraps each migration in a transaction when the adapter supports transactional DDL.
345
+ `data-table` ships a SQL-first migration system under `remix/data-table/migrations`. Each migration is a directory containing hand-written `up.sql` and (optionally) `down.sql`. The runner journals applied migrations, detects checksum drift and missing applied migrations, and wraps each migration in a transaction when the database supports transactional DDL.
347
346
 
348
347
  ### Example Setup
349
348
 
@@ -357,7 +356,7 @@ app/
357
356
  20260301113000_add_user_status/
358
357
  up.sql
359
358
  down.sql
360
- migrate.ts
359
+ db.ts
361
360
  ```
362
361
 
363
362
  - Keep migration directories in one parent directory (for example `app/db/migrations`).
@@ -388,90 +387,120 @@ drop table if exists users;
388
387
 
389
388
  ### Multi-Statement Driver Configuration
390
389
 
391
- The runner sends each migration to the adapter as a single multi-statement script. Make sure the underlying driver accepts multiple statements:
390
+ The runner sends each migration to the database as a single multi-statement script. Make sure the underlying driver accepts multiple statements:
392
391
 
393
392
  - `better-sqlite3`: works out of the box (`db.exec`).
394
393
  - `pg`: works out of the box when no parameter array is passed.
395
394
  - `mysql2`: requires `multipleStatements: true` on the connection/pool.
396
395
 
397
396
  ```ts
398
- import { createPool } from 'mysql2/promise'
397
+ import { createMysqlDatabase } from 'remix/data-table/mysql'
399
398
 
400
- let pool = createPool({
399
+ let db = createMysqlDatabase({
401
400
  uri: process.env.DATABASE_URL,
402
401
  multipleStatements: true,
403
402
  })
404
403
  ```
405
404
 
406
- ### Runner Script Example
405
+ ### Database Command Configuration
406
+
407
+ Configure `remix db` statically in `remix.json`. Connection secrets are read from the named
408
+ environment variable at command runtime, and paths are resolved relative to `remix.json`:
409
+
410
+ ```jsonc
411
+ {
412
+ "$schema": "https://remix.run/schemas/remix.json",
413
+ "db": {
414
+ "adapter": {
415
+ "type": "postgres",
416
+ "connectionString": { "env": "DATABASE_URL" },
417
+ },
418
+ "migrations": {
419
+ "directory": "./db/migrations",
420
+ "journalTable": "app_migrations",
421
+ },
422
+ "seed": "./db/seed.sql",
423
+ },
424
+ }
425
+ ```
407
426
 
408
- In `app/db/migrate.ts`:
427
+ Application runtime setup remains application-owned. It does not need to expose any special
428
+ exports for the CLI.
409
429
 
410
- ```ts
411
- import path from 'node:path'
412
- import { Pool } from 'pg'
413
- import { createPostgresDatabaseAdapter } from 'remix/data-table/postgres'
414
- import { createMigrationRunner } from 'remix/data-table/migrations'
415
- import { loadMigrations } from 'remix/data-table/migrations/node'
430
+ Run lifecycle commands through the Remix CLI:
416
431
 
417
- let directionArg = process.argv[2] ?? 'up'
418
- let direction = directionArg === 'down' ? 'down' : 'up'
419
- let to = process.argv[3]
432
+ ```sh
433
+ remix db status
434
+ remix db migrate
435
+ remix db migrate --to 20260301113000_add_user_status
436
+ remix db seed
437
+ remix db reset
438
+ remix db wipe
439
+ ```
420
440
 
421
- let pool = new Pool({ connectionString: process.env.DATABASE_URL })
422
- let adapter = createPostgresDatabaseAdapter(pool)
423
- let migrations = await loadMigrations(path.resolve('app/db/migrations'))
424
- let runner = createMigrationRunner(adapter, migrations)
441
+ `--to` accepts a bare migration id (`20260301113000`) or the full directory name (`20260301113000_add_user_status`).
425
442
 
426
- try {
427
- let result = direction === 'up' ? await runner.up({ to }) : await runner.down({ to })
428
- console.log(direction + ' complete', {
429
- applied: result.applied.map((entry) => entry.id),
430
- reverted: result.reverted.map((entry) => entry.id),
431
- })
432
- } finally {
433
- await pool.end()
434
- }
435
- ```
443
+ `remix db status` reports applied migrations whose files are no longer present as `missing`. If the journal table does not exist, it reports every migration as pending without creating the table. Forward migration runs stop before executing SQL when an applied journal entry is missing from the current migration set. Rollbacks skip those orphaned journal entries so migrations that are still present can be reverted.
444
+
445
+ `wipe` and `reset` are destructive. They require a config-backed database so it can close, recreate, and reconnect to the configured database.
446
+
447
+ ### Programmatic Migrations
436
448
 
437
- Use `journalTable` if you want a custom migrations journal table name:
449
+ Load migrations and pass the resolved collection directly to the database:
438
450
 
439
451
  ```ts
440
- let runner = createMigrationRunner(adapter, migrations, {
441
- journalTable: 'app_migrations',
442
- })
452
+ import { loadMigrations } from 'remix/data-table/migrations/node'
453
+
454
+ let migrations = await loadMigrations('./db/migrations')
455
+ await db.migrate(migrations)
443
456
  ```
444
457
 
445
- Run it with your runtime, for example:
458
+ `Database.migrate()` supports forward and backward directions, a target or step bound, dry runs,
459
+ and a custom journal table:
446
460
 
447
- ```sh
448
- node ./app/db/migrate.ts up
449
- node ./app/db/migrate.ts up 20260301113000
450
- node ./app/db/migrate.ts down
451
- node ./app/db/migrate.ts down 20260228090000
461
+ ```ts
462
+ await db.migrate(migrations)
463
+ await db.migrate(migrations, { to: '20260301113000_add_user_status' })
464
+ await db.migrate(migrations, { step: 1 })
465
+ await db.migrate(migrations, { direction: 'down' })
466
+ await db.migrate(migrations, { direction: 'down', to: '20260301113000' })
467
+ await db.migrate(migrations, { direction: 'down', step: 1 })
468
+ await db.migrate(migrations, { journalTable: 'app_migrations' })
469
+
470
+ let plan = await db.migrate(migrations, { dryRun: true })
471
+ for (let script of plan.sql) console.log(script)
452
472
  ```
453
473
 
454
- Use `step` for bounded rollforward/rollback behavior instead of a target id:
474
+ `to` and `step` are mutually exclusive. Omit `journalTable` to use `data_table_migrations`.
475
+
476
+ Database drivers with migration locking run the complete migration and journal lifecycle through the
477
+ connection that owns the lock. This keeps advisory locks correctly paired when the driver uses a
478
+ connection pool, including pools configured with a single connection.
479
+
480
+ Read status separately, or rebuild a database with migrations and an optional seed. A seed is a
481
+ function that receives the database; `loadSeed()` builds one from a SQL file:
455
482
 
456
483
  ```ts
457
- await runner.up({ step: 1 })
458
- await runner.down({ step: 1 })
459
- ```
484
+ import { loadSeed } from 'remix/data-table/migrations/node'
460
485
 
461
- `to` and `step` are mutually exclusive within a single run.
486
+ let seed = await loadSeed('./db/seed.sql')
462
487
 
463
- Use `dryRun` to inspect the SQL plan without applying or journaling anything:
488
+ let status = await db.migrationStatus(migrations, { journalTable: 'app_migrations' })
489
+ await db.reset({ migrations })
490
+ await db.reset({ migrations, seed })
491
+ await db.reset({ migrations, seed, journalTable: 'app_migrations' })
492
+ ```
493
+
494
+ When a lifecycle command is the last thing a process does, close database-owned connections so
495
+ the process can exit:
464
496
 
465
497
  ```ts
466
- let plan = await runner.up({ dryRun: true })
467
- for (let script of plan.sql) {
468
- console.log(script)
469
- }
498
+ await db.close()
470
499
  ```
471
500
 
472
501
  ### Transaction Modes
473
502
 
474
- By default each migration is wrapped in a transaction when the adapter supports transactional DDL. Override per migration with a directive on the first non-blank line of `up.sql`:
503
+ By default each migration is wrapped in a transaction when the database supports transactional DDL. Override per migration with a directive on the first non-blank line of `up.sql`:
475
504
 
476
505
  ```sql
477
506
  -- data-table/transaction: none
@@ -480,8 +509,8 @@ create index concurrently users_email_active_idx on users (email) where status =
480
509
 
481
510
  Supported modes:
482
511
 
483
- - `auto` (default): wrap when the adapter supports transactional DDL.
484
- - `required`: wrap; the runner throws if the adapter cannot support it.
512
+ - `auto` (default): wrap when the database supports transactional DDL.
513
+ - `required`: wrap; the runner throws if the database cannot support it.
485
514
  - `none`: never wrap. Use this for statements like postgres `CREATE INDEX CONCURRENTLY` that cannot run inside a transaction.
486
515
 
487
516
  You can also set `transaction` directly on a `MigrationDescriptor` when registering migrations programmatically.
@@ -491,7 +520,7 @@ You can also set `transaction` directly on a `MigrationDescriptor` when register
491
520
  For non-filesystem runtimes, register migrations directly:
492
521
 
493
522
  ```ts
494
- import { createMigrationRegistry, createMigrationRunner } from 'remix/data-table/migrations'
523
+ import { createMigrationRegistry } from 'remix/data-table/migrations'
495
524
 
496
525
  let registry = createMigrationRegistry()
497
526
  registry.register({
@@ -501,8 +530,7 @@ registry.register({
501
530
  down: 'drop table users;',
502
531
  })
503
532
 
504
- let runner = createMigrationRunner(adapter, registry)
505
- await runner.up()
533
+ await db.migrate(registry)
506
534
  ```
507
535
 
508
536
  ## Raw SQL Escape Hatch
@@ -530,14 +558,120 @@ let result = await db.exec(sql`
530
558
  `)
531
559
  ```
532
560
 
533
- `sql` keeps values parameterized per adapter dialect, so you can avoid manual string concatenation.
561
+ `sql` keeps values parameterized for the database dialect, so you can avoid manual string concatenation.
562
+
563
+ ## Custom Database Drivers
564
+
565
+ Applications normally use one of the concrete SQLite, PostgreSQL, or MySQL factories. Integration
566
+ packages can add another dialect by implementing `DatabaseDriver` and extending `Database`. This
567
+ complete skeleton assumes the underlying client exposes the operations needed by the driver:
568
+
569
+ ```ts
570
+ import {
571
+ Database,
572
+ type DatabaseDriver,
573
+ type DataManipulationRequest,
574
+ type DataManipulationResult,
575
+ type DatabaseOptions,
576
+ type TableRef,
577
+ type TransactionOptions,
578
+ type TransactionToken,
579
+ } from 'remix/data-table'
580
+ import type { AcmeClient } from 'acme-database'
581
+
582
+ class AcmeDriver implements DatabaseDriver<'acme'> {
583
+ readonly dialect = 'acme'
584
+ readonly capabilities = {
585
+ returning: true,
586
+ savepoints: true,
587
+ upsert: true,
588
+ transactionalDdl: true,
589
+ migrationLock: false,
590
+ } as const
591
+
592
+ #client: AcmeClient
593
+
594
+ constructor(client: AcmeClient) {
595
+ this.#client = client
596
+ }
597
+
598
+ execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
599
+ return this.#client.execute(request)
600
+ }
601
+
602
+ executeScript(sql: string, transaction?: TransactionToken): Promise<void> {
603
+ return this.#client.executeScript(sql, transaction)
604
+ }
605
+
606
+ beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
607
+ return this.#client.beginTransaction(options)
608
+ }
609
+
610
+ commitTransaction(transaction: TransactionToken): Promise<void> {
611
+ return this.#client.commitTransaction(transaction)
612
+ }
613
+
614
+ rollbackTransaction(transaction: TransactionToken): Promise<void> {
615
+ return this.#client.rollbackTransaction(transaction)
616
+ }
617
+
618
+ hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
619
+ return this.#client.hasTable(table, transaction)
620
+ }
621
+
622
+ hasColumn(table: TableRef, column: string, transaction?: TransactionToken): Promise<boolean> {
623
+ return this.#client.hasColumn(table, column, transaction)
624
+ }
625
+
626
+ createSavepoint(transaction: TransactionToken, name: string): Promise<void> {
627
+ return this.#client.createSavepoint(transaction, name)
628
+ }
629
+
630
+ rollbackToSavepoint(transaction: TransactionToken, name: string): Promise<void> {
631
+ return this.#client.rollbackToSavepoint(transaction, name)
632
+ }
633
+
634
+ releaseSavepoint(transaction: TransactionToken, name: string): Promise<void> {
635
+ return this.#client.releaseSavepoint(transaction, name)
636
+ }
637
+
638
+ wipe(): Promise<void> {
639
+ return this.#client.wipe()
640
+ }
641
+
642
+ close(): void | Promise<void> {
643
+ return this.#client.close()
644
+ }
645
+ }
646
+
647
+ export class AcmeDatabase extends Database<'acme'> {
648
+ constructor(client: AcmeClient, options?: DatabaseOptions) {
649
+ super(new AcmeDriver(client), options)
650
+ }
651
+ }
652
+ ```
653
+
654
+ `Database` supplies queries, CRUD helpers, relations, transactions, and migrations. The private
655
+ driver owns SQL execution and connection lifecycle. A driver provides:
656
+
657
+ - `dialect` and an immutable `capabilities` object
658
+ - `execute()` and `executeScript()`
659
+ - `hasTable()`, `hasColumn()`, `wipe()`, and idempotent `close()`
660
+ - transaction and savepoint lifecycle methods using opaque `TransactionToken` values
661
+
662
+ Drivers whose capabilities report `migrationLock: true` also implement `withMigrationLock()` and
663
+ run its callback with a `DatabaseDriver` bound to the connection that owns the lock.
664
+
665
+ `Database`, `DatabaseDriver`, and the supporting driver protocol types are all exported directly
666
+ from `remix/data-table`. Transaction callbacks receive a transaction-scoped `Database`, so custom
667
+ subclass methods are intentionally unavailable inside the callback.
534
668
 
535
669
  ## Related Packages
536
670
 
537
671
  - [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Optional schema parsing you can use inside table-level `validate(...)` hooks
538
- - [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
539
- - [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter
540
- - [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter
672
+ - [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL database integration
673
+ - [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL database integration
674
+ - [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite database integration
541
675
 
542
676
  ## License
543
677
 
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/data-table/cli'