@remix-run/data-table-sqlite 0.5.0 → 0.6.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/README.md +39 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/database.d.ts +29 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +32 -0
- package/dist/lib/{adapter.d.ts → driver.d.ts} +49 -34
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/{adapter.js → driver.js} +221 -38
- package/dist/lib/sql-compiler.d.ts +2 -1
- package/dist/lib/sql-compiler.d.ts.map +1 -1
- package/dist/lib/sql-compiler.js +6 -6
- package/package.json +12 -12
- package/src/index.ts +7 -2
- package/src/lib/database.ts +42 -0
- package/src/lib/{adapter.ts → driver.ts} +282 -42
- package/src/lib/sql-compiler.ts +8 -7
- package/dist/lib/adapter.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# data-table-sqlite
|
|
2
2
|
|
|
3
|
-
SQLite
|
|
4
|
-
Use this package when you want `data-table` APIs backed by a synchronous SQLite client.
|
|
3
|
+
SQLite database driver for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table), backed by a synchronous SQLite client.
|
|
5
4
|
|
|
6
5
|
## Features
|
|
7
6
|
|
|
8
|
-
- **Native Runtime SQLite Support**:
|
|
7
|
+
- **Native Runtime SQLite Support**: Opens a configured filename with Node's `node:sqlite` or Bun's `bun:sqlite`, or uses a compatible synchronous SQLite client
|
|
9
8
|
- **Full `data-table` API Support**: Queries, relations, writes, and transactions
|
|
10
|
-
- **
|
|
9
|
+
- **SQLite Compiler**: SQL compilation is handled automatically for SQLite
|
|
11
10
|
- **Multi-Statement Migrations**: `executeScript()` runs `up.sql` / `down.sql` files via `Database.exec()`
|
|
12
11
|
- **SQLite Capabilities Enabled By Default**:
|
|
13
12
|
- `returning: true`
|
|
@@ -24,32 +23,42 @@ npm i remix
|
|
|
24
23
|
|
|
25
24
|
## Usage
|
|
26
25
|
|
|
27
|
-
### Node
|
|
28
|
-
|
|
29
26
|
```ts
|
|
30
|
-
import {
|
|
31
|
-
import { createDatabase } from 'remix/data-table'
|
|
32
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
27
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
33
28
|
|
|
34
|
-
let
|
|
35
|
-
|
|
29
|
+
let db = createSqliteDatabase({
|
|
30
|
+
filename: 'app.db',
|
|
31
|
+
foreignKeys: true,
|
|
32
|
+
})
|
|
36
33
|
```
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
The config-backed database uses `node:sqlite` in Node.js and `bun:sqlite` in Bun. It supports `db.wipe()` and `db.reset()` because it can close and reopen the database file. Call `await db.close()` during application shutdown to release the connection and its file handle.
|
|
36
|
+
|
|
37
|
+
Foreign key enforcement defaults to off on every runtime. When `foreignKeys` is enabled, the database restores foreign key enforcement each time it opens the connection, including after destructive lifecycle operations.
|
|
38
|
+
|
|
39
|
+
The database also applies `pragma busy_timeout = 5000` whenever it opens the connection, so writes wait for a locked database instead of failing immediately with `SQLITE_BUSY`. Use `busyTimeout` to override the timeout in milliseconds (`0` disables the wait).
|
|
40
|
+
|
|
41
|
+
You may also pass an existing synchronous client when your application owns its lifecycle:
|
|
39
42
|
|
|
40
43
|
```ts
|
|
41
44
|
import { Database } from 'bun:sqlite'
|
|
42
|
-
import {
|
|
43
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
45
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
44
46
|
|
|
45
47
|
let sqlite = new Database('app.db')
|
|
46
|
-
let db =
|
|
48
|
+
let db = createSqliteDatabase(sqlite)
|
|
49
|
+
|
|
50
|
+
// Leaves the supplied client open.
|
|
51
|
+
await db.close()
|
|
52
|
+
|
|
53
|
+
// The application closes the client it owns.
|
|
54
|
+
sqlite.close()
|
|
47
55
|
```
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
Destructive lifecycle methods are unavailable when you pass an existing client.
|
|
58
|
+
|
|
59
|
+
This is a good fit for local development, embedded deployments, and single-node services. Import any driver-specific types you need directly from your runtime's SQLite module.
|
|
51
60
|
|
|
52
|
-
##
|
|
61
|
+
## Database Capabilities
|
|
53
62
|
|
|
54
63
|
`data-table-sqlite` reports this capability set by default:
|
|
55
64
|
|
|
@@ -61,23 +70,30 @@ Import any driver-specific types you need directly from your runtime's SQLite mo
|
|
|
61
70
|
|
|
62
71
|
## Advanced Usage
|
|
63
72
|
|
|
73
|
+
### Destructive Lifecycle And Locking
|
|
74
|
+
|
|
75
|
+
`db.wipe()` and `db.reset()` assume a single process owns the database file. Stop other processes before wiping: on POSIX systems another process keeps writing to the deleted inode, and on Windows an open handle blocks deletion entirely. Wiping removes the `-wal`, `-shm`, and `-journal` sidecar files along with the main database file so a freshly created database never associates with stale sidecars.
|
|
76
|
+
|
|
77
|
+
SQLite migrations run without a cross-process migration lock (`migrationLock: false`), so run migrations from one process at a time.
|
|
78
|
+
|
|
79
|
+
`filename` resolves against the current working directory — for `remix db` commands, wherever you invoke the CLI. Prefer absolute paths or paths derived from `import.meta.dirname`.
|
|
80
|
+
|
|
64
81
|
### In-Memory Database For Tests
|
|
65
82
|
|
|
66
83
|
```ts
|
|
67
84
|
import { DatabaseSync } from 'node:sqlite'
|
|
68
|
-
import {
|
|
69
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
85
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
70
86
|
|
|
71
87
|
let sqlite = new DatabaseSync(':memory:')
|
|
72
|
-
let db =
|
|
88
|
+
let db = createSqliteDatabase(sqlite)
|
|
73
89
|
```
|
|
74
90
|
|
|
75
91
|
## Related Packages
|
|
76
92
|
|
|
77
93
|
- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
|
|
78
94
|
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation
|
|
79
|
-
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL
|
|
80
|
-
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL
|
|
95
|
+
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL database driver
|
|
96
|
+
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL database driver
|
|
81
97
|
|
|
82
98
|
## License
|
|
83
99
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { createSqliteDatabase, SqliteDatabase } from './lib/database.ts';
|
|
2
|
+
export type { SqliteDatabaseClient, SqliteDatabaseConfig, SqliteRunResult, SqliteStatement, } from './lib/driver.ts';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACxE,YAAY,EACV,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,eAAe,GAChB,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createSqliteDatabase, SqliteDatabase } from './lib/database.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Database, type DatabaseOptions } from '@remix-run/data-table';
|
|
2
|
+
import { type SqliteDatabaseClient, type SqliteDatabaseConfig } from './driver.ts';
|
|
3
|
+
/** A {@link Database} backed by SQLite. */
|
|
4
|
+
export declare class SqliteDatabase extends Database<'sqlite'> {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a SQLite-backed database.
|
|
7
|
+
* @param input SQLite configuration or synchronous database client.
|
|
8
|
+
* @param options Database runtime options.
|
|
9
|
+
*/
|
|
10
|
+
constructor(input: SqliteDatabaseClient | SqliteDatabaseConfig, options?: DatabaseOptions);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a SQLite-backed database.
|
|
14
|
+
*
|
|
15
|
+
* @param input SQLite configuration or synchronous database client.
|
|
16
|
+
* @param options Database runtime options.
|
|
17
|
+
* @returns A SQLite database.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
21
|
+
*
|
|
22
|
+
* let db = createSqliteDatabase({
|
|
23
|
+
* filename: './data/app.db',
|
|
24
|
+
* foreignKeys: true,
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSqliteDatabase(input: SqliteDatabaseClient | SqliteDatabaseConfig, options?: DatabaseOptions): SqliteDatabase;
|
|
29
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEtE,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,aAAa,CAAA;AAEpB,2CAA2C;AAC3C,qBAAa,cAAe,SAAQ,QAAQ,CAAC,QAAQ,CAAC;IACpD;;;;OAIG;IACH,YAAY,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,EAAE,OAAO,GAAE,eAAoB,EAE5F;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,EAClD,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAEhB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Database } from '@remix-run/data-table';
|
|
2
|
+
import { SqliteDatabaseDriver, } from './driver.js';
|
|
3
|
+
/** A {@link Database} backed by SQLite. */
|
|
4
|
+
export class SqliteDatabase extends Database {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a SQLite-backed database.
|
|
7
|
+
* @param input SQLite configuration or synchronous database client.
|
|
8
|
+
* @param options Database runtime options.
|
|
9
|
+
*/
|
|
10
|
+
constructor(input, options = {}) {
|
|
11
|
+
super(new SqliteDatabaseDriver(input), options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates a SQLite-backed database.
|
|
16
|
+
*
|
|
17
|
+
* @param input SQLite configuration or synchronous database client.
|
|
18
|
+
* @param options Database runtime options.
|
|
19
|
+
* @returns A SQLite database.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
23
|
+
*
|
|
24
|
+
* let db = createSqliteDatabase({
|
|
25
|
+
* filename: './data/app.db',
|
|
26
|
+
* foreignKeys: true,
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function createSqliteDatabase(input, options = {}) {
|
|
31
|
+
return new SqliteDatabase(input, options);
|
|
32
|
+
}
|
|
@@ -1,16 +1,33 @@
|
|
|
1
|
-
import type { DataManipulationRequest, DataManipulationResult,
|
|
1
|
+
import type { DataManipulationOperation, DataManipulationRequest, DataManipulationResult, DatabaseDriver, SqlStatement, TableRef, TransactionOptions, TransactionToken } from '@remix-run/data-table';
|
|
2
2
|
/**
|
|
3
|
-
* Synchronous SQLite
|
|
3
|
+
* Synchronous SQLite client accepted by `createSqliteDatabase()`.
|
|
4
4
|
*
|
|
5
5
|
* This matches the shared surface of Node's `node:sqlite` `DatabaseSync`, Bun's `bun:sqlite`
|
|
6
6
|
* `Database`, and compatible synchronous SQLite clients.
|
|
7
7
|
*/
|
|
8
|
-
export interface
|
|
8
|
+
export interface SqliteDatabaseClient {
|
|
9
9
|
prepare(sql: string): SqliteStatement;
|
|
10
10
|
exec(sql: string): unknown;
|
|
11
|
+
close?: () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Configuration for a SQLite database created by `createSqliteDatabase()`. */
|
|
14
|
+
export interface SqliteDatabaseConfig {
|
|
15
|
+
/** SQLite database filename or `:memory:` for an in-memory database. */
|
|
16
|
+
filename: string;
|
|
17
|
+
/**
|
|
18
|
+
* Enables SQLite foreign key enforcement whenever the database opens a connection.
|
|
19
|
+
* Defaults to `false` (enforcement off) on every runtime, including Node.js where
|
|
20
|
+
* `node:sqlite` would otherwise enable it by default.
|
|
21
|
+
*/
|
|
22
|
+
foreignKeys?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* SQLite `busy_timeout` in milliseconds, applied whenever the database opens a connection.
|
|
25
|
+
* Defaults to `5000`. Set `0` to fail immediately when another process holds a write lock.
|
|
26
|
+
*/
|
|
27
|
+
busyTimeout?: number;
|
|
11
28
|
}
|
|
12
29
|
/**
|
|
13
|
-
* Prepared statement shape used by {@link
|
|
30
|
+
* Prepared statement shape used by {@link SqliteDatabaseClient}.
|
|
14
31
|
*/
|
|
15
32
|
export interface SqliteStatement {
|
|
16
33
|
all(...values: unknown[]): unknown[];
|
|
@@ -28,25 +45,25 @@ export interface SqliteRunResult {
|
|
|
28
45
|
lastInsertRowid: unknown;
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
|
-
*
|
|
48
|
+
* SQLite database driver backed by a synchronous SQLite client.
|
|
32
49
|
*/
|
|
33
|
-
export declare class
|
|
50
|
+
export declare class SqliteDatabaseDriver implements DatabaseDriver<'sqlite'> {
|
|
34
51
|
#private;
|
|
35
52
|
/**
|
|
36
|
-
* The SQL dialect identifier reported by this
|
|
53
|
+
* The SQL dialect identifier reported by this database.
|
|
37
54
|
*/
|
|
38
|
-
dialect:
|
|
55
|
+
get dialect(): 'sqlite';
|
|
39
56
|
/**
|
|
40
|
-
* Feature flags describing the
|
|
57
|
+
* Feature flags describing the SQLite behaviors supported by this database.
|
|
41
58
|
*/
|
|
42
|
-
capabilities: {
|
|
43
|
-
returning:
|
|
44
|
-
savepoints:
|
|
45
|
-
upsert:
|
|
46
|
-
transactionalDdl:
|
|
47
|
-
migrationLock:
|
|
48
|
-
}
|
|
49
|
-
constructor(
|
|
59
|
+
get capabilities(): Readonly<{
|
|
60
|
+
returning: true;
|
|
61
|
+
savepoints: true;
|
|
62
|
+
upsert: true;
|
|
63
|
+
transactionalDdl: true;
|
|
64
|
+
migrationLock: false;
|
|
65
|
+
}>;
|
|
66
|
+
constructor(input: SqliteDatabaseClient | SqliteDatabaseConfig);
|
|
50
67
|
/**
|
|
51
68
|
* Compiles a data-manipulation operation to sqlite SQL statements.
|
|
52
69
|
* @param operation Operation to compile.
|
|
@@ -81,6 +98,20 @@ export declare class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
81
98
|
* @returns `true` when the column exists.
|
|
82
99
|
*/
|
|
83
100
|
hasColumn(table: TableRef, column: string, transaction?: TransactionToken): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Destructively recreates the configured SQLite database.
|
|
103
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
104
|
+
*/
|
|
105
|
+
wipe(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Closes a database connection created from configuration.
|
|
108
|
+
*
|
|
109
|
+
* Config-backed databases keep an open handle that locks the database file on
|
|
110
|
+
* Windows until it is closed, so callers that need to move or delete the file
|
|
111
|
+
* should close the database first. Supplied clients remain caller-owned. Safe
|
|
112
|
+
* to call more than once.
|
|
113
|
+
*/
|
|
114
|
+
close(): void;
|
|
84
115
|
/**
|
|
85
116
|
* Starts a sqlite transaction.
|
|
86
117
|
* @param options Transaction options.
|
|
@@ -121,20 +152,4 @@ export declare class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
121
152
|
*/
|
|
122
153
|
releaseSavepoint(token: TransactionToken, name: string): Promise<void>;
|
|
123
154
|
}
|
|
124
|
-
|
|
125
|
-
* Creates a sqlite `DatabaseAdapter`.
|
|
126
|
-
* @param database Synchronous SQLite database client.
|
|
127
|
-
* @returns A configured sqlite adapter.
|
|
128
|
-
* @example
|
|
129
|
-
* ```ts
|
|
130
|
-
* import { DatabaseSync } from 'node:sqlite'
|
|
131
|
-
* import { createDatabase } from 'remix/data-table'
|
|
132
|
-
* import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
133
|
-
*
|
|
134
|
-
* let sqlite = new DatabaseSync('./data/app.db')
|
|
135
|
-
* let adapter = createSqliteDatabaseAdapter(sqlite)
|
|
136
|
-
* let db = createDatabase(adapter)
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
139
|
-
export declare function createSqliteDatabaseAdapter(database: SqliteDatabase): SqliteDatabaseAdapter;
|
|
140
|
-
//# sourceMappingURL=adapter.d.ts.map
|
|
155
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/lib/driver.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAK9B;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAA;IACrC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;CACnB;AAkDD,+EAA+E;AAC/E,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;IACpC,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAClC,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAA;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,OAAO,EAAE,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,eAAe,EAAE,OAAO,CAAA;CACzB;AAED;;GAEG;AACH,qBAAa,oBAAqB,YAAW,cAAc,CAAC,QAAQ,CAAC;;IACnE;;OAEG;IACH,IAAI,OAAO,IAAI,QAAQ,CAEtB;IAED;;OAEG;IACH,IAAI,YAAY;;;;;;OAEf;IAQD,YAAY,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,EAO7D;IAED;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,EAAE,CAG/D;IAED;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAuC/E;IAED;;;;;OAKG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9E;IAED;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBhF;IAED;;;;;;OAMG;IACG,SAAS,CACb,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,gBAAgB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAclB;IAED;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB1B;IAED;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAKZ;IAED;;;;OAIG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAc9E;IAED;;;;OAIG;IACG,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB9D;IAED;;;;OAIG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhE;IAED;;;;;OAKG;IACG,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;CAqDF"}
|
|
@@ -1,29 +1,68 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
1
4
|
import { getTablePrimaryKey } from '@remix-run/data-table';
|
|
2
|
-
import { compileSqliteOperation } from
|
|
5
|
+
import { compileSqliteOperation } from './sql-compiler.js';
|
|
6
|
+
const sqliteCapabilities = Object.freeze({
|
|
7
|
+
returning: true,
|
|
8
|
+
savepoints: true,
|
|
9
|
+
upsert: true,
|
|
10
|
+
transactionalDdl: true,
|
|
11
|
+
migrationLock: false,
|
|
12
|
+
});
|
|
13
|
+
let loadedDriverConstructor;
|
|
14
|
+
// The runtime driver loads lazily on config-backed construction so client-backed databases
|
|
15
|
+
// keep working in environments that cannot resolve node:sqlite or bun:sqlite at import time,
|
|
16
|
+
// and so bundlers never try to resolve those specifiers statically.
|
|
17
|
+
function loadSqliteDatabaseConstructor() {
|
|
18
|
+
if (!loadedDriverConstructor) {
|
|
19
|
+
if ('Bun' in globalThis) {
|
|
20
|
+
// import.meta.require is Bun's synchronous require for ES modules; Bun does not
|
|
21
|
+
// implement process.getBuiltinModule
|
|
22
|
+
let importMeta = import.meta;
|
|
23
|
+
let driver = importMeta.require?.('bun:sqlite');
|
|
24
|
+
loadedDriverConstructor = driver?.Database;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// process.getBuiltinModule loads node:sqlite synchronously (Node.js 22.3+)
|
|
28
|
+
let driver = globalThis.process?.getBuiltinModule?.('node:sqlite');
|
|
29
|
+
loadedDriverConstructor = driver?.DatabaseSync;
|
|
30
|
+
}
|
|
31
|
+
if (!loadedDriverConstructor) {
|
|
32
|
+
throw new Error('SQLite config-based construction requires node:sqlite (Node.js 22.5+) or bun:sqlite; pass a SQLite database client instead');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return loadedDriverConstructor;
|
|
36
|
+
}
|
|
3
37
|
/**
|
|
4
|
-
*
|
|
38
|
+
* SQLite database driver backed by a synchronous SQLite client.
|
|
5
39
|
*/
|
|
6
|
-
export class
|
|
40
|
+
export class SqliteDatabaseDriver {
|
|
7
41
|
/**
|
|
8
|
-
* The SQL dialect identifier reported by this
|
|
42
|
+
* The SQL dialect identifier reported by this database.
|
|
9
43
|
*/
|
|
10
|
-
dialect
|
|
44
|
+
get dialect() {
|
|
45
|
+
return 'sqlite';
|
|
46
|
+
}
|
|
11
47
|
/**
|
|
12
|
-
* Feature flags describing the
|
|
48
|
+
* Feature flags describing the SQLite behaviors supported by this database.
|
|
13
49
|
*/
|
|
14
|
-
capabilities
|
|
50
|
+
get capabilities() {
|
|
51
|
+
return sqliteCapabilities;
|
|
52
|
+
}
|
|
53
|
+
#config;
|
|
15
54
|
#database;
|
|
55
|
+
#databaseOpen = true;
|
|
16
56
|
#transactions = new Set();
|
|
17
57
|
#transactionCounter = 0;
|
|
18
|
-
constructor(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
58
|
+
constructor(input) {
|
|
59
|
+
if (isSqliteDatabase(input)) {
|
|
60
|
+
this.#database = input;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.#config = input;
|
|
64
|
+
this.#database = openSqliteDatabase(input);
|
|
65
|
+
}
|
|
27
66
|
}
|
|
28
67
|
/**
|
|
29
68
|
* Compiles a data-manipulation operation to sqlite SQL statements.
|
|
@@ -40,6 +79,7 @@ export class SqliteDatabaseAdapter {
|
|
|
40
79
|
* @returns Execution result.
|
|
41
80
|
*/
|
|
42
81
|
async execute(request) {
|
|
82
|
+
this.#assertDatabaseOpen();
|
|
43
83
|
if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
|
|
44
84
|
return {
|
|
45
85
|
affectedRows: 0,
|
|
@@ -47,6 +87,9 @@ export class SqliteDatabaseAdapter {
|
|
|
47
87
|
rows: request.operation.returning ? [] : undefined,
|
|
48
88
|
};
|
|
49
89
|
}
|
|
90
|
+
if (request.transaction) {
|
|
91
|
+
this.#assertTransaction(request.transaction);
|
|
92
|
+
}
|
|
50
93
|
let statement = this.compileSql(request.operation)[0];
|
|
51
94
|
let prepared = this.#database.prepare(statement.text);
|
|
52
95
|
let values = normalizeStatementValues(statement.values);
|
|
@@ -74,6 +117,7 @@ export class SqliteDatabaseAdapter {
|
|
|
74
117
|
* @returns A promise that resolves once execution completes.
|
|
75
118
|
*/
|
|
76
119
|
async executeScript(sql, transaction) {
|
|
120
|
+
this.#assertDatabaseOpen();
|
|
77
121
|
if (transaction) {
|
|
78
122
|
this.#assertTransaction(transaction);
|
|
79
123
|
}
|
|
@@ -86,6 +130,7 @@ export class SqliteDatabaseAdapter {
|
|
|
86
130
|
* @returns `true` when the table exists.
|
|
87
131
|
*/
|
|
88
132
|
async hasTable(table, transaction) {
|
|
133
|
+
this.#assertDatabaseOpen();
|
|
89
134
|
if (transaction) {
|
|
90
135
|
this.#assertTransaction(transaction);
|
|
91
136
|
}
|
|
@@ -93,8 +138,9 @@ export class SqliteDatabaseAdapter {
|
|
|
93
138
|
? quoteIdentifier(table.schema) + '.sqlite_master'
|
|
94
139
|
: 'sqlite_master';
|
|
95
140
|
let statement = this.#database.prepare('select 1 from ' + masterTable + ' where type = ? and name = ? limit 1');
|
|
141
|
+
// node:sqlite returns `undefined` for a missing row while bun:sqlite returns `null`
|
|
96
142
|
let row = statement.get('table', table.name);
|
|
97
|
-
return row
|
|
143
|
+
return row != null;
|
|
98
144
|
}
|
|
99
145
|
/**
|
|
100
146
|
* Checks whether a column exists in sqlite.
|
|
@@ -104,6 +150,7 @@ export class SqliteDatabaseAdapter {
|
|
|
104
150
|
* @returns `true` when the column exists.
|
|
105
151
|
*/
|
|
106
152
|
async hasColumn(table, column, transaction) {
|
|
153
|
+
this.#assertDatabaseOpen();
|
|
107
154
|
if (transaction) {
|
|
108
155
|
this.#assertTransaction(transaction);
|
|
109
156
|
}
|
|
@@ -112,12 +159,52 @@ export class SqliteDatabaseAdapter {
|
|
|
112
159
|
let rows = statement.all();
|
|
113
160
|
return rows.some((row) => row.name === column);
|
|
114
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Destructively recreates the configured SQLite database.
|
|
164
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
165
|
+
*/
|
|
166
|
+
async wipe() {
|
|
167
|
+
let config = this.#configOrThrow('wipe');
|
|
168
|
+
this.#assertNoOpenTransactions('wipe');
|
|
169
|
+
this.#closeDatabase();
|
|
170
|
+
if (config.filename === ':memory:') {
|
|
171
|
+
this.#replaceDatabase();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await mkdir(dirname(config.filename), { recursive: true });
|
|
176
|
+
await removeDatabaseFile(config.filename);
|
|
177
|
+
// SQLite associates a database file with -wal/-shm/-journal sidecars by path, so
|
|
178
|
+
// stale sidecars left next to a freshly created database are a corruption vector
|
|
179
|
+
await removeDatabaseFile(config.filename + '-wal');
|
|
180
|
+
await removeDatabaseFile(config.filename + '-shm');
|
|
181
|
+
await removeDatabaseFile(config.filename + '-journal');
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
this.#replaceDatabase();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Closes a database connection created from configuration.
|
|
189
|
+
*
|
|
190
|
+
* Config-backed databases keep an open handle that locks the database file on
|
|
191
|
+
* Windows until it is closed, so callers that need to move or delete the file
|
|
192
|
+
* should close the database first. Supplied clients remain caller-owned. Safe
|
|
193
|
+
* to call more than once.
|
|
194
|
+
*/
|
|
195
|
+
close() {
|
|
196
|
+
this.#assertNoOpenTransactions('close');
|
|
197
|
+
if (this.#config) {
|
|
198
|
+
this.#closeDatabase();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
115
201
|
/**
|
|
116
202
|
* Starts a sqlite transaction.
|
|
117
203
|
* @param options Transaction options.
|
|
118
204
|
* @returns Transaction token.
|
|
119
205
|
*/
|
|
120
206
|
async beginTransaction(options) {
|
|
207
|
+
this.#assertDatabaseOpen();
|
|
121
208
|
if (options?.isolationLevel === 'read uncommitted') {
|
|
122
209
|
this.#database.exec('pragma read_uncommitted = true');
|
|
123
210
|
}
|
|
@@ -134,8 +221,30 @@ export class SqliteDatabaseAdapter {
|
|
|
134
221
|
*/
|
|
135
222
|
async commitTransaction(token) {
|
|
136
223
|
this.#assertTransaction(token);
|
|
137
|
-
|
|
138
|
-
|
|
224
|
+
try {
|
|
225
|
+
this.#database.exec('commit');
|
|
226
|
+
}
|
|
227
|
+
catch (commitError) {
|
|
228
|
+
try {
|
|
229
|
+
this.#database.exec('rollback');
|
|
230
|
+
}
|
|
231
|
+
catch (rollbackError) {
|
|
232
|
+
let failures = [commitError, rollbackError];
|
|
233
|
+
try {
|
|
234
|
+
this.#discardUncertainConnection();
|
|
235
|
+
}
|
|
236
|
+
catch (recoveryError) {
|
|
237
|
+
failures.push(recoveryError);
|
|
238
|
+
}
|
|
239
|
+
throw new AggregateError(failures, 'SQLite commit and rollback both failed', {
|
|
240
|
+
cause: commitError,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
throw commitError;
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
this.#transactions.delete(token.id);
|
|
247
|
+
}
|
|
139
248
|
}
|
|
140
249
|
/**
|
|
141
250
|
* Rolls back an open sqlite transaction.
|
|
@@ -144,8 +253,21 @@ export class SqliteDatabaseAdapter {
|
|
|
144
253
|
*/
|
|
145
254
|
async rollbackTransaction(token) {
|
|
146
255
|
this.#assertTransaction(token);
|
|
147
|
-
|
|
148
|
-
|
|
256
|
+
try {
|
|
257
|
+
this.#database.exec('rollback');
|
|
258
|
+
}
|
|
259
|
+
catch (rollbackError) {
|
|
260
|
+
try {
|
|
261
|
+
this.#discardUncertainConnection();
|
|
262
|
+
}
|
|
263
|
+
catch (recoveryError) {
|
|
264
|
+
throw new AggregateError([rollbackError, recoveryError], 'SQLite rollback and connection cleanup both failed', { cause: rollbackError });
|
|
265
|
+
}
|
|
266
|
+
throw rollbackError;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.#transactions.delete(token.id);
|
|
270
|
+
}
|
|
149
271
|
}
|
|
150
272
|
/**
|
|
151
273
|
* Creates a savepoint in an open sqlite transaction.
|
|
@@ -177,29 +299,90 @@ export class SqliteDatabaseAdapter {
|
|
|
177
299
|
this.#assertTransaction(token);
|
|
178
300
|
this.#database.exec('release savepoint ' + quoteIdentifier(name));
|
|
179
301
|
}
|
|
302
|
+
#replaceDatabase() {
|
|
303
|
+
if (this.#config) {
|
|
304
|
+
this.#database = openSqliteDatabase(this.#config);
|
|
305
|
+
this.#databaseOpen = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
#closeDatabase() {
|
|
309
|
+
if (this.#databaseOpen) {
|
|
310
|
+
this.#databaseOpen = false;
|
|
311
|
+
this.#database.close?.();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
#discardUncertainConnection() {
|
|
315
|
+
if (this.#config) {
|
|
316
|
+
this.#closeDatabase();
|
|
317
|
+
this.#replaceDatabase();
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// The supplied client remains caller-owned, but this wrapper cannot safely
|
|
321
|
+
// reuse a connection whose transaction state is unknown.
|
|
322
|
+
this.#databaseOpen = false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
#configOrThrow(method) {
|
|
326
|
+
if (!this.#config) {
|
|
327
|
+
throw new Error('SQLite database ' + method + '() requires config-based construction');
|
|
328
|
+
}
|
|
329
|
+
return this.#config;
|
|
330
|
+
}
|
|
331
|
+
#assertNoOpenTransactions(method) {
|
|
332
|
+
if (this.#transactions.size > 0) {
|
|
333
|
+
throw new Error('SQLite database cannot ' + method + ' while transactions are open');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
180
336
|
#assertTransaction(token) {
|
|
337
|
+
this.#assertDatabaseOpen();
|
|
181
338
|
if (!this.#transactions.has(token.id)) {
|
|
182
339
|
throw new Error('Unknown transaction token: ' + token.id);
|
|
183
340
|
}
|
|
184
341
|
}
|
|
342
|
+
#assertDatabaseOpen() {
|
|
343
|
+
if (!this.#databaseOpen) {
|
|
344
|
+
throw new Error('SQLite database is closed');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
185
347
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
348
|
+
const REMOVE_RETRIES = 10;
|
|
349
|
+
const REMOVE_RETRY_DELAY_MS = 100;
|
|
350
|
+
async function removeDatabaseFile(filename) {
|
|
351
|
+
// Windows keeps a just-closed database file locked for a short window (deferred handle
|
|
352
|
+
// release, antivirus scans), so removal is retried with a linear backoff
|
|
353
|
+
for (let attempt = 0;; attempt++) {
|
|
354
|
+
try {
|
|
355
|
+
await rm(filename, { force: true });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (attempt >= REMOVE_RETRIES || !isRetryableRemoveError(error)) {
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
await setTimeout(REMOVE_RETRY_DELAY_MS * (attempt + 1));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function isRetryableRemoveError(error) {
|
|
367
|
+
let code = error?.code;
|
|
368
|
+
return code === 'EBUSY' || code === 'EPERM' || code === 'EMFILE' || code === 'ENFILE';
|
|
369
|
+
}
|
|
370
|
+
function openSqliteDatabase(config) {
|
|
371
|
+
let SqliteDatabaseConstructor = loadSqliteDatabaseConstructor();
|
|
372
|
+
let database = new SqliteDatabaseConstructor(config.filename);
|
|
373
|
+
// node:sqlite enables foreign keys by default while bun:sqlite follows SQLite's default
|
|
374
|
+
// (off), so set the pragma explicitly to make the option authoritative on both runtimes
|
|
375
|
+
database.exec('pragma foreign_keys = ' + (config.foreignKeys ? 'on' : 'off'));
|
|
376
|
+
// node:sqlite defaults to busy_timeout 0, which fails immediately with SQLITE_BUSY when
|
|
377
|
+
// another process holds a write lock
|
|
378
|
+
database.exec('pragma busy_timeout = ' + String(config.busyTimeout ?? 5000));
|
|
379
|
+
return database;
|
|
380
|
+
}
|
|
381
|
+
function isSqliteDatabase(input) {
|
|
382
|
+
return ('prepare' in input &&
|
|
383
|
+
typeof input.prepare === 'function' &&
|
|
384
|
+
'exec' in input &&
|
|
385
|
+
typeof input.exec === 'function');
|
|
203
386
|
}
|
|
204
387
|
function normalizeRows(rows) {
|
|
205
388
|
return rows.map((row) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SqlStatement } from '@remix-run/data-table';
|
|
2
|
+
import type { DataManipulationOperation } from '@remix-run/data-table';
|
|
2
3
|
export declare function compileSqliteOperation(operation: DataManipulationOperation): SqlStatement;
|
|
3
4
|
//# sourceMappingURL=sql-compiler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAetE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,CAuGzF"}
|
package/dist/lib/sql-compiler.js
CHANGED
|
@@ -24,8 +24,8 @@ export function compileSqliteOperation(operation) {
|
|
|
24
24
|
compileGroupByClause(operation.groupBy) +
|
|
25
25
|
compileHavingClause(operation.having, context) +
|
|
26
26
|
compileOrderByClause(operation.orderBy) +
|
|
27
|
-
compileLimitClause(operation.limit) +
|
|
28
|
-
compileOffsetClause(operation.offset),
|
|
27
|
+
compileLimitClause(operation.limit, context) +
|
|
28
|
+
compileOffsetClause(operation.offset, context),
|
|
29
29
|
values: context.values,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -218,17 +218,17 @@ function compileOrderByClause(orderBy) {
|
|
|
218
218
|
.map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())
|
|
219
219
|
.join(', '));
|
|
220
220
|
}
|
|
221
|
-
function compileLimitClause(limit) {
|
|
221
|
+
function compileLimitClause(limit, context) {
|
|
222
222
|
if (limit === undefined) {
|
|
223
223
|
return '';
|
|
224
224
|
}
|
|
225
|
-
return ' limit ' +
|
|
225
|
+
return ' limit ' + pushValue(context, limit);
|
|
226
226
|
}
|
|
227
|
-
function compileOffsetClause(offset) {
|
|
227
|
+
function compileOffsetClause(offset, context) {
|
|
228
228
|
if (offset === undefined) {
|
|
229
229
|
return '';
|
|
230
230
|
}
|
|
231
|
-
return ' offset ' +
|
|
231
|
+
return ' offset ' + pushValue(context, offset);
|
|
232
232
|
}
|
|
233
233
|
function compileReturningClause(returning) {
|
|
234
234
|
if (!returning) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/data-table-sqlite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SQLite
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "SQLite database implementation for remix/data-table",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^24.6.0",
|
|
31
|
-
"
|
|
32
|
-
"@remix-run/
|
|
33
|
-
"@remix-run/
|
|
34
|
-
"@remix-run/
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"@remix-run/test": "0.6.0",
|
|
33
|
+
"@remix-run/data-table": "0.4.0",
|
|
34
|
+
"@remix-run/assert": "0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@remix-run/data-table": "^0.
|
|
37
|
+
"@remix-run/data-table": "^0.4.0"
|
|
38
38
|
},
|
|
39
39
|
"keywords": [
|
|
40
40
|
"remix",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"sql"
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "
|
|
47
|
+
"build": "tsc -p tsconfig.build.json",
|
|
48
48
|
"clean": "git clean -fdX",
|
|
49
|
-
"test": "remix
|
|
50
|
-
"test:bun": "bun x --bun remix
|
|
51
|
-
"test:coverage": "remix
|
|
52
|
-
"typecheck": "
|
|
49
|
+
"test": "remix test",
|
|
50
|
+
"test:bun": "bun x --bun remix test",
|
|
51
|
+
"test:coverage": "remix test --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { createSqliteDatabase, SqliteDatabase } from './lib/database.ts'
|
|
2
|
+
export type {
|
|
3
|
+
SqliteDatabaseClient,
|
|
4
|
+
SqliteDatabaseConfig,
|
|
5
|
+
SqliteRunResult,
|
|
6
|
+
SqliteStatement,
|
|
7
|
+
} from './lib/driver.ts'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Database, type DatabaseOptions } from '@remix-run/data-table'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SqliteDatabaseDriver,
|
|
5
|
+
type SqliteDatabaseClient,
|
|
6
|
+
type SqliteDatabaseConfig,
|
|
7
|
+
} from './driver.ts'
|
|
8
|
+
|
|
9
|
+
/** A {@link Database} backed by SQLite. */
|
|
10
|
+
export class SqliteDatabase extends Database<'sqlite'> {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a SQLite-backed database.
|
|
13
|
+
* @param input SQLite configuration or synchronous database client.
|
|
14
|
+
* @param options Database runtime options.
|
|
15
|
+
*/
|
|
16
|
+
constructor(input: SqliteDatabaseClient | SqliteDatabaseConfig, options: DatabaseOptions = {}) {
|
|
17
|
+
super(new SqliteDatabaseDriver(input), options)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a SQLite-backed database.
|
|
23
|
+
*
|
|
24
|
+
* @param input SQLite configuration or synchronous database client.
|
|
25
|
+
* @param options Database runtime options.
|
|
26
|
+
* @returns A SQLite database.
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
30
|
+
*
|
|
31
|
+
* let db = createSqliteDatabase({
|
|
32
|
+
* filename: './data/app.db',
|
|
33
|
+
* foreignKeys: true,
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function createSqliteDatabase(
|
|
38
|
+
input: SqliteDatabaseClient | SqliteDatabaseConfig,
|
|
39
|
+
options: DatabaseOptions = {},
|
|
40
|
+
): SqliteDatabase {
|
|
41
|
+
return new SqliteDatabase(input, options)
|
|
42
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { dirname } from 'node:path'
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises'
|
|
3
|
+
import { setTimeout } from 'node:timers/promises'
|
|
4
|
+
|
|
1
5
|
import type {
|
|
6
|
+
DataManipulationOperation,
|
|
2
7
|
DataManipulationRequest,
|
|
3
8
|
DataManipulationResult,
|
|
4
|
-
|
|
5
|
-
DatabaseAdapter,
|
|
9
|
+
DatabaseDriver,
|
|
6
10
|
SqlStatement,
|
|
7
11
|
TableRef,
|
|
8
12
|
TransactionOptions,
|
|
@@ -13,18 +17,84 @@ import { getTablePrimaryKey } from '@remix-run/data-table'
|
|
|
13
17
|
import { compileSqliteOperation } from './sql-compiler.ts'
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
|
-
* Synchronous SQLite
|
|
20
|
+
* Synchronous SQLite client accepted by `createSqliteDatabase()`.
|
|
17
21
|
*
|
|
18
22
|
* This matches the shared surface of Node's `node:sqlite` `DatabaseSync`, Bun's `bun:sqlite`
|
|
19
23
|
* `Database`, and compatible synchronous SQLite clients.
|
|
20
24
|
*/
|
|
21
|
-
export interface
|
|
25
|
+
export interface SqliteDatabaseClient {
|
|
22
26
|
prepare(sql: string): SqliteStatement
|
|
23
27
|
exec(sql: string): unknown
|
|
28
|
+
close?: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type SqliteDatabaseConstructor = {
|
|
32
|
+
new (path: string): SqliteDatabaseClient
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type SqliteDriverModule = {
|
|
36
|
+
Database?: SqliteDatabaseConstructor
|
|
37
|
+
DatabaseSync?: SqliteDatabaseConstructor
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sqliteCapabilities = Object.freeze({
|
|
41
|
+
returning: true,
|
|
42
|
+
savepoints: true,
|
|
43
|
+
upsert: true,
|
|
44
|
+
transactionalDdl: true,
|
|
45
|
+
migrationLock: false,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
let loadedDriverConstructor: SqliteDatabaseConstructor | undefined
|
|
49
|
+
|
|
50
|
+
// The runtime driver loads lazily on config-backed construction so client-backed databases
|
|
51
|
+
// keep working in environments that cannot resolve node:sqlite or bun:sqlite at import time,
|
|
52
|
+
// and so bundlers never try to resolve those specifiers statically.
|
|
53
|
+
function loadSqliteDatabaseConstructor(): SqliteDatabaseConstructor {
|
|
54
|
+
if (!loadedDriverConstructor) {
|
|
55
|
+
if ('Bun' in globalThis) {
|
|
56
|
+
// import.meta.require is Bun's synchronous require for ES modules; Bun does not
|
|
57
|
+
// implement process.getBuiltinModule
|
|
58
|
+
let importMeta = import.meta as ImportMeta & { require?: (id: string) => unknown }
|
|
59
|
+
let driver = importMeta.require?.('bun:sqlite') as SqliteDriverModule | undefined
|
|
60
|
+
loadedDriverConstructor = driver?.Database
|
|
61
|
+
} else {
|
|
62
|
+
// process.getBuiltinModule loads node:sqlite synchronously (Node.js 22.3+)
|
|
63
|
+
let driver = globalThis.process?.getBuiltinModule?.('node:sqlite') as
|
|
64
|
+
| SqliteDriverModule
|
|
65
|
+
| undefined
|
|
66
|
+
loadedDriverConstructor = driver?.DatabaseSync
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!loadedDriverConstructor) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'SQLite config-based construction requires node:sqlite (Node.js 22.5+) or bun:sqlite; pass a SQLite database client instead',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return loadedDriverConstructor
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Configuration for a SQLite database created by `createSqliteDatabase()`. */
|
|
80
|
+
export interface SqliteDatabaseConfig {
|
|
81
|
+
/** SQLite database filename or `:memory:` for an in-memory database. */
|
|
82
|
+
filename: string
|
|
83
|
+
/**
|
|
84
|
+
* Enables SQLite foreign key enforcement whenever the database opens a connection.
|
|
85
|
+
* Defaults to `false` (enforcement off) on every runtime, including Node.js where
|
|
86
|
+
* `node:sqlite` would otherwise enable it by default.
|
|
87
|
+
*/
|
|
88
|
+
foreignKeys?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* SQLite `busy_timeout` in milliseconds, applied whenever the database opens a connection.
|
|
91
|
+
* Defaults to `5000`. Set `0` to fail immediately when another process holds a write lock.
|
|
92
|
+
*/
|
|
93
|
+
busyTimeout?: number
|
|
24
94
|
}
|
|
25
95
|
|
|
26
96
|
/**
|
|
27
|
-
* Prepared statement shape used by {@link
|
|
97
|
+
* Prepared statement shape used by {@link SqliteDatabaseClient}.
|
|
28
98
|
*/
|
|
29
99
|
export interface SqliteStatement {
|
|
30
100
|
all(...values: unknown[]): unknown[]
|
|
@@ -44,31 +114,35 @@ export interface SqliteRunResult {
|
|
|
44
114
|
}
|
|
45
115
|
|
|
46
116
|
/**
|
|
47
|
-
*
|
|
117
|
+
* SQLite database driver backed by a synchronous SQLite client.
|
|
48
118
|
*/
|
|
49
|
-
export class
|
|
119
|
+
export class SqliteDatabaseDriver implements DatabaseDriver<'sqlite'> {
|
|
50
120
|
/**
|
|
51
|
-
* The SQL dialect identifier reported by this
|
|
121
|
+
* The SQL dialect identifier reported by this database.
|
|
52
122
|
*/
|
|
53
|
-
dialect
|
|
123
|
+
get dialect(): 'sqlite' {
|
|
124
|
+
return 'sqlite'
|
|
125
|
+
}
|
|
54
126
|
|
|
55
127
|
/**
|
|
56
|
-
* Feature flags describing the
|
|
128
|
+
* Feature flags describing the SQLite behaviors supported by this database.
|
|
57
129
|
*/
|
|
58
|
-
capabilities
|
|
130
|
+
get capabilities() {
|
|
131
|
+
return sqliteCapabilities
|
|
132
|
+
}
|
|
59
133
|
|
|
60
|
-
#
|
|
134
|
+
#config?: SqliteDatabaseConfig
|
|
135
|
+
#database: SqliteDatabaseClient
|
|
136
|
+
#databaseOpen = true
|
|
61
137
|
#transactions = new Set<string>()
|
|
62
138
|
#transactionCounter = 0
|
|
63
139
|
|
|
64
|
-
constructor(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
transactionalDdl: true,
|
|
71
|
-
migrationLock: false,
|
|
140
|
+
constructor(input: SqliteDatabaseClient | SqliteDatabaseConfig) {
|
|
141
|
+
if (isSqliteDatabase(input)) {
|
|
142
|
+
this.#database = input
|
|
143
|
+
} else {
|
|
144
|
+
this.#config = input
|
|
145
|
+
this.#database = openSqliteDatabase(input)
|
|
72
146
|
}
|
|
73
147
|
}
|
|
74
148
|
|
|
@@ -88,6 +162,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
88
162
|
* @returns Execution result.
|
|
89
163
|
*/
|
|
90
164
|
async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
|
|
165
|
+
this.#assertDatabaseOpen()
|
|
166
|
+
|
|
91
167
|
if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
|
|
92
168
|
return {
|
|
93
169
|
affectedRows: 0,
|
|
@@ -96,6 +172,10 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
96
172
|
}
|
|
97
173
|
}
|
|
98
174
|
|
|
175
|
+
if (request.transaction) {
|
|
176
|
+
this.#assertTransaction(request.transaction)
|
|
177
|
+
}
|
|
178
|
+
|
|
99
179
|
let statement = this.compileSql(request.operation)[0]
|
|
100
180
|
let prepared = this.#database.prepare(statement.text)
|
|
101
181
|
let values = normalizeStatementValues(statement.values)
|
|
@@ -129,6 +209,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
129
209
|
* @returns A promise that resolves once execution completes.
|
|
130
210
|
*/
|
|
131
211
|
async executeScript(sql: string, transaction?: TransactionToken): Promise<void> {
|
|
212
|
+
this.#assertDatabaseOpen()
|
|
213
|
+
|
|
132
214
|
if (transaction) {
|
|
133
215
|
this.#assertTransaction(transaction)
|
|
134
216
|
}
|
|
@@ -143,6 +225,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
143
225
|
* @returns `true` when the table exists.
|
|
144
226
|
*/
|
|
145
227
|
async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
|
|
228
|
+
this.#assertDatabaseOpen()
|
|
229
|
+
|
|
146
230
|
if (transaction) {
|
|
147
231
|
this.#assertTransaction(transaction)
|
|
148
232
|
}
|
|
@@ -153,8 +237,9 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
153
237
|
let statement = this.#database.prepare(
|
|
154
238
|
'select 1 from ' + masterTable + ' where type = ? and name = ? limit 1',
|
|
155
239
|
)
|
|
240
|
+
// node:sqlite returns `undefined` for a missing row while bun:sqlite returns `null`
|
|
156
241
|
let row = statement.get('table', table.name)
|
|
157
|
-
return row
|
|
242
|
+
return row != null
|
|
158
243
|
}
|
|
159
244
|
|
|
160
245
|
/**
|
|
@@ -169,6 +254,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
169
254
|
column: string,
|
|
170
255
|
transaction?: TransactionToken,
|
|
171
256
|
): Promise<boolean> {
|
|
257
|
+
this.#assertDatabaseOpen()
|
|
258
|
+
|
|
172
259
|
if (transaction) {
|
|
173
260
|
this.#assertTransaction(transaction)
|
|
174
261
|
}
|
|
@@ -182,12 +269,56 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
182
269
|
return rows.some((row) => row.name === column)
|
|
183
270
|
}
|
|
184
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Destructively recreates the configured SQLite database.
|
|
274
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
275
|
+
*/
|
|
276
|
+
async wipe(): Promise<void> {
|
|
277
|
+
let config = this.#configOrThrow('wipe')
|
|
278
|
+
this.#assertNoOpenTransactions('wipe')
|
|
279
|
+
this.#closeDatabase()
|
|
280
|
+
|
|
281
|
+
if (config.filename === ':memory:') {
|
|
282
|
+
this.#replaceDatabase()
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await mkdir(dirname(config.filename), { recursive: true })
|
|
288
|
+
await removeDatabaseFile(config.filename)
|
|
289
|
+
// SQLite associates a database file with -wal/-shm/-journal sidecars by path, so
|
|
290
|
+
// stale sidecars left next to a freshly created database are a corruption vector
|
|
291
|
+
await removeDatabaseFile(config.filename + '-wal')
|
|
292
|
+
await removeDatabaseFile(config.filename + '-shm')
|
|
293
|
+
await removeDatabaseFile(config.filename + '-journal')
|
|
294
|
+
} finally {
|
|
295
|
+
this.#replaceDatabase()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Closes a database connection created from configuration.
|
|
301
|
+
*
|
|
302
|
+
* Config-backed databases keep an open handle that locks the database file on
|
|
303
|
+
* Windows until it is closed, so callers that need to move or delete the file
|
|
304
|
+
* should close the database first. Supplied clients remain caller-owned. Safe
|
|
305
|
+
* to call more than once.
|
|
306
|
+
*/
|
|
307
|
+
close(): void {
|
|
308
|
+
this.#assertNoOpenTransactions('close')
|
|
309
|
+
if (this.#config) {
|
|
310
|
+
this.#closeDatabase()
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
185
314
|
/**
|
|
186
315
|
* Starts a sqlite transaction.
|
|
187
316
|
* @param options Transaction options.
|
|
188
317
|
* @returns Transaction token.
|
|
189
318
|
*/
|
|
190
319
|
async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
|
|
320
|
+
this.#assertDatabaseOpen()
|
|
321
|
+
|
|
191
322
|
if (options?.isolationLevel === 'read uncommitted') {
|
|
192
323
|
this.#database.exec('pragma read_uncommitted = true')
|
|
193
324
|
}
|
|
@@ -208,8 +339,26 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
208
339
|
*/
|
|
209
340
|
async commitTransaction(token: TransactionToken): Promise<void> {
|
|
210
341
|
this.#assertTransaction(token)
|
|
211
|
-
|
|
212
|
-
|
|
342
|
+
try {
|
|
343
|
+
this.#database.exec('commit')
|
|
344
|
+
} catch (commitError) {
|
|
345
|
+
try {
|
|
346
|
+
this.#database.exec('rollback')
|
|
347
|
+
} catch (rollbackError) {
|
|
348
|
+
let failures: unknown[] = [commitError, rollbackError]
|
|
349
|
+
try {
|
|
350
|
+
this.#discardUncertainConnection()
|
|
351
|
+
} catch (recoveryError) {
|
|
352
|
+
failures.push(recoveryError)
|
|
353
|
+
}
|
|
354
|
+
throw new AggregateError(failures, 'SQLite commit and rollback both failed', {
|
|
355
|
+
cause: commitError,
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
throw commitError
|
|
359
|
+
} finally {
|
|
360
|
+
this.#transactions.delete(token.id)
|
|
361
|
+
}
|
|
213
362
|
}
|
|
214
363
|
|
|
215
364
|
/**
|
|
@@ -219,8 +368,22 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
219
368
|
*/
|
|
220
369
|
async rollbackTransaction(token: TransactionToken): Promise<void> {
|
|
221
370
|
this.#assertTransaction(token)
|
|
222
|
-
|
|
223
|
-
|
|
371
|
+
try {
|
|
372
|
+
this.#database.exec('rollback')
|
|
373
|
+
} catch (rollbackError) {
|
|
374
|
+
try {
|
|
375
|
+
this.#discardUncertainConnection()
|
|
376
|
+
} catch (recoveryError) {
|
|
377
|
+
throw new AggregateError(
|
|
378
|
+
[rollbackError, recoveryError],
|
|
379
|
+
'SQLite rollback and connection cleanup both failed',
|
|
380
|
+
{ cause: rollbackError },
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
throw rollbackError
|
|
384
|
+
} finally {
|
|
385
|
+
this.#transactions.delete(token.id)
|
|
386
|
+
}
|
|
224
387
|
}
|
|
225
388
|
|
|
226
389
|
/**
|
|
@@ -256,30 +419,107 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
256
419
|
this.#database.exec('release savepoint ' + quoteIdentifier(name))
|
|
257
420
|
}
|
|
258
421
|
|
|
422
|
+
#replaceDatabase(): void {
|
|
423
|
+
if (this.#config) {
|
|
424
|
+
this.#database = openSqliteDatabase(this.#config)
|
|
425
|
+
this.#databaseOpen = true
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#closeDatabase(): void {
|
|
430
|
+
if (this.#databaseOpen) {
|
|
431
|
+
this.#databaseOpen = false
|
|
432
|
+
this.#database.close?.()
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#discardUncertainConnection(): void {
|
|
437
|
+
if (this.#config) {
|
|
438
|
+
this.#closeDatabase()
|
|
439
|
+
this.#replaceDatabase()
|
|
440
|
+
} else {
|
|
441
|
+
// The supplied client remains caller-owned, but this wrapper cannot safely
|
|
442
|
+
// reuse a connection whose transaction state is unknown.
|
|
443
|
+
this.#databaseOpen = false
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#configOrThrow(method: string): SqliteDatabaseConfig {
|
|
448
|
+
if (!this.#config) {
|
|
449
|
+
throw new Error('SQLite database ' + method + '() requires config-based construction')
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return this.#config
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#assertNoOpenTransactions(method: string): void {
|
|
456
|
+
if (this.#transactions.size > 0) {
|
|
457
|
+
throw new Error('SQLite database cannot ' + method + ' while transactions are open')
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
259
461
|
#assertTransaction(token: TransactionToken): void {
|
|
462
|
+
this.#assertDatabaseOpen()
|
|
260
463
|
if (!this.#transactions.has(token.id)) {
|
|
261
464
|
throw new Error('Unknown transaction token: ' + token.id)
|
|
262
465
|
}
|
|
263
466
|
}
|
|
467
|
+
|
|
468
|
+
#assertDatabaseOpen(): void {
|
|
469
|
+
if (!this.#databaseOpen) {
|
|
470
|
+
throw new Error('SQLite database is closed')
|
|
471
|
+
}
|
|
472
|
+
}
|
|
264
473
|
}
|
|
265
474
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
475
|
+
const REMOVE_RETRIES = 10
|
|
476
|
+
const REMOVE_RETRY_DELAY_MS = 100
|
|
477
|
+
|
|
478
|
+
async function removeDatabaseFile(filename: string): Promise<void> {
|
|
479
|
+
// Windows keeps a just-closed database file locked for a short window (deferred handle
|
|
480
|
+
// release, antivirus scans), so removal is retried with a linear backoff
|
|
481
|
+
for (let attempt = 0; ; attempt++) {
|
|
482
|
+
try {
|
|
483
|
+
await rm(filename, { force: true })
|
|
484
|
+
return
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (attempt >= REMOVE_RETRIES || !isRetryableRemoveError(error)) {
|
|
487
|
+
throw error
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await setTimeout(REMOVE_RETRY_DELAY_MS * (attempt + 1))
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isRetryableRemoveError(error: unknown): boolean {
|
|
496
|
+
let code = (error as NodeJS.ErrnoException | null)?.code
|
|
497
|
+
return code === 'EBUSY' || code === 'EPERM' || code === 'EMFILE' || code === 'ENFILE'
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function openSqliteDatabase(config: SqliteDatabaseConfig): SqliteDatabaseClient {
|
|
501
|
+
let SqliteDatabaseConstructor = loadSqliteDatabaseConstructor()
|
|
502
|
+
let database = new SqliteDatabaseConstructor(config.filename)
|
|
503
|
+
|
|
504
|
+
// node:sqlite enables foreign keys by default while bun:sqlite follows SQLite's default
|
|
505
|
+
// (off), so set the pragma explicitly to make the option authoritative on both runtimes
|
|
506
|
+
database.exec('pragma foreign_keys = ' + (config.foreignKeys ? 'on' : 'off'))
|
|
507
|
+
// node:sqlite defaults to busy_timeout 0, which fails immediately with SQLITE_BUSY when
|
|
508
|
+
// another process holds a write lock
|
|
509
|
+
database.exec('pragma busy_timeout = ' + String(config.busyTimeout ?? 5000))
|
|
510
|
+
|
|
511
|
+
return database
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isSqliteDatabase(
|
|
515
|
+
input: SqliteDatabaseClient | SqliteDatabaseConfig,
|
|
516
|
+
): input is SqliteDatabaseClient {
|
|
517
|
+
return (
|
|
518
|
+
'prepare' in input &&
|
|
519
|
+
typeof input.prepare === 'function' &&
|
|
520
|
+
'exec' in input &&
|
|
521
|
+
typeof input.exec === 'function'
|
|
522
|
+
)
|
|
283
523
|
}
|
|
284
524
|
|
|
285
525
|
function normalizeRows(rows: unknown[]): Record<string, unknown>[] {
|
package/src/lib/sql-compiler.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'
|
|
2
|
-
import type {
|
|
2
|
+
import type { Predicate, SqlStatement } from '@remix-run/data-table'
|
|
3
|
+
import type { DataManipulationOperation } from '@remix-run/data-table'
|
|
3
4
|
import {
|
|
4
5
|
collectColumns as collectColumnsHelper,
|
|
5
6
|
normalizeJoinType as normalizeJoinTypeHelper,
|
|
@@ -43,8 +44,8 @@ export function compileSqliteOperation(operation: DataManipulationOperation): Sq
|
|
|
43
44
|
compileGroupByClause(operation.groupBy) +
|
|
44
45
|
compileHavingClause(operation.having, context) +
|
|
45
46
|
compileOrderByClause(operation.orderBy) +
|
|
46
|
-
compileLimitClause(operation.limit) +
|
|
47
|
-
compileOffsetClause(operation.offset),
|
|
47
|
+
compileLimitClause(operation.limit, context) +
|
|
48
|
+
compileOffsetClause(operation.offset, context),
|
|
48
49
|
values: context.values,
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -309,20 +310,20 @@ function compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'des
|
|
|
309
310
|
)
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
function compileLimitClause(limit: number | undefined): string {
|
|
313
|
+
function compileLimitClause(limit: number | undefined, context: CompileContext): string {
|
|
313
314
|
if (limit === undefined) {
|
|
314
315
|
return ''
|
|
315
316
|
}
|
|
316
317
|
|
|
317
|
-
return ' limit ' +
|
|
318
|
+
return ' limit ' + pushValue(context, limit)
|
|
318
319
|
}
|
|
319
320
|
|
|
320
|
-
function compileOffsetClause(offset: number | undefined): string {
|
|
321
|
+
function compileOffsetClause(offset: number | undefined, context: CompileContext): string {
|
|
321
322
|
if (offset === undefined) {
|
|
322
323
|
return ''
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
return ' offset ' +
|
|
326
|
+
return ' offset ' + pushValue(context, offset)
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
function compileReturningClause(returning: '*' | string[] | undefined): string {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/lib/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAK9B;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAA;IACrC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;IACpC,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAClC,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAA;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,OAAO,EAAE,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,eAAe,EAAE,OAAO,CAAA;CACzB;AAED;;GAEG;AACH,qBAAa,qBAAsB,YAAW,eAAe;;IAC3D;;OAEG;IACH,OAAO,SAAW;IAElB;;OAEG;IACH,YAAY;;;;;;MAAA;IAMZ,YAAY,QAAQ,EAAE,cAAc,EASnC;IAED;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,EAAE,CAG/D;IAED;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAiC/E;IAED;;;;;OAKG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9E;IAED;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAahF;IAED;;;;;;OAMG;IACG,SAAS,CACb,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,gBAAgB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAYlB;IAED;;;;OAIG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAY9E;IAED;;;;OAIG;IACG,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9D;IAED;;;;OAIG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhE;IAED;;;;;OAKG;IACG,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAED;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;CAOF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,cAAc,GAAG,qBAAqB,CAE3F"}
|