@remix-run/data-table-sqlite 0.5.1 → 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 +38 -20
- 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} +218 -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} +278 -42
- package/src/lib/sql-compiler.ts +8 -7
- package/dist/lib/adapter.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# data-table-sqlite
|
|
2
2
|
|
|
3
|
-
SQLite
|
|
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.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **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
|
|
8
8
|
- **Full `data-table` API Support**: Queries, relations, writes, and transactions
|
|
9
|
-
- **
|
|
9
|
+
- **SQLite Compiler**: SQL compilation is handled automatically for SQLite
|
|
10
10
|
- **Multi-Statement Migrations**: `executeScript()` runs `up.sql` / `down.sql` files via `Database.exec()`
|
|
11
11
|
- **SQLite Capabilities Enabled By Default**:
|
|
12
12
|
- `returning: true`
|
|
@@ -23,31 +23,42 @@ npm i remix
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
-
### Node
|
|
27
|
-
|
|
28
26
|
```ts
|
|
29
|
-
import {
|
|
30
|
-
import { createDatabase } from 'remix/data-table'
|
|
31
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
27
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
32
28
|
|
|
33
|
-
let
|
|
34
|
-
|
|
29
|
+
let db = createSqliteDatabase({
|
|
30
|
+
filename: 'app.db',
|
|
31
|
+
foreignKeys: true,
|
|
32
|
+
})
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
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:
|
|
38
42
|
|
|
39
43
|
```ts
|
|
40
44
|
import { Database } from 'bun:sqlite'
|
|
41
|
-
import {
|
|
42
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
45
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
43
46
|
|
|
44
47
|
let sqlite = new Database('app.db')
|
|
45
|
-
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()
|
|
46
55
|
```
|
|
47
56
|
|
|
57
|
+
Destructive lifecycle methods are unavailable when you pass an existing client.
|
|
58
|
+
|
|
48
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.
|
|
49
60
|
|
|
50
|
-
##
|
|
61
|
+
## Database Capabilities
|
|
51
62
|
|
|
52
63
|
`data-table-sqlite` reports this capability set by default:
|
|
53
64
|
|
|
@@ -59,23 +70,30 @@ This is a good fit for local development, embedded deployments, and single-node
|
|
|
59
70
|
|
|
60
71
|
## Advanced Usage
|
|
61
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
|
+
|
|
62
81
|
### In-Memory Database For Tests
|
|
63
82
|
|
|
64
83
|
```ts
|
|
65
84
|
import { DatabaseSync } from 'node:sqlite'
|
|
66
|
-
import {
|
|
67
|
-
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
|
|
85
|
+
import { createSqliteDatabase } from 'remix/data-table/sqlite'
|
|
68
86
|
|
|
69
87
|
let sqlite = new DatabaseSync(':memory:')
|
|
70
|
-
let db =
|
|
88
|
+
let db = createSqliteDatabase(sqlite)
|
|
71
89
|
```
|
|
72
90
|
|
|
73
91
|
## Related Packages
|
|
74
92
|
|
|
75
93
|
- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
|
|
76
94
|
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation
|
|
77
|
-
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL
|
|
78
|
-
- [`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
|
|
79
97
|
|
|
80
98
|
## License
|
|
81
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,
|
|
@@ -77,6 +117,7 @@ export class SqliteDatabaseAdapter {
|
|
|
77
117
|
* @returns A promise that resolves once execution completes.
|
|
78
118
|
*/
|
|
79
119
|
async executeScript(sql, transaction) {
|
|
120
|
+
this.#assertDatabaseOpen();
|
|
80
121
|
if (transaction) {
|
|
81
122
|
this.#assertTransaction(transaction);
|
|
82
123
|
}
|
|
@@ -89,6 +130,7 @@ export class SqliteDatabaseAdapter {
|
|
|
89
130
|
* @returns `true` when the table exists.
|
|
90
131
|
*/
|
|
91
132
|
async hasTable(table, transaction) {
|
|
133
|
+
this.#assertDatabaseOpen();
|
|
92
134
|
if (transaction) {
|
|
93
135
|
this.#assertTransaction(transaction);
|
|
94
136
|
}
|
|
@@ -96,8 +138,9 @@ export class SqliteDatabaseAdapter {
|
|
|
96
138
|
? quoteIdentifier(table.schema) + '.sqlite_master'
|
|
97
139
|
: 'sqlite_master';
|
|
98
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`
|
|
99
142
|
let row = statement.get('table', table.name);
|
|
100
|
-
return row
|
|
143
|
+
return row != null;
|
|
101
144
|
}
|
|
102
145
|
/**
|
|
103
146
|
* Checks whether a column exists in sqlite.
|
|
@@ -107,6 +150,7 @@ export class SqliteDatabaseAdapter {
|
|
|
107
150
|
* @returns `true` when the column exists.
|
|
108
151
|
*/
|
|
109
152
|
async hasColumn(table, column, transaction) {
|
|
153
|
+
this.#assertDatabaseOpen();
|
|
110
154
|
if (transaction) {
|
|
111
155
|
this.#assertTransaction(transaction);
|
|
112
156
|
}
|
|
@@ -115,12 +159,52 @@ export class SqliteDatabaseAdapter {
|
|
|
115
159
|
let rows = statement.all();
|
|
116
160
|
return rows.some((row) => row.name === column);
|
|
117
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
|
+
}
|
|
118
201
|
/**
|
|
119
202
|
* Starts a sqlite transaction.
|
|
120
203
|
* @param options Transaction options.
|
|
121
204
|
* @returns Transaction token.
|
|
122
205
|
*/
|
|
123
206
|
async beginTransaction(options) {
|
|
207
|
+
this.#assertDatabaseOpen();
|
|
124
208
|
if (options?.isolationLevel === 'read uncommitted') {
|
|
125
209
|
this.#database.exec('pragma read_uncommitted = true');
|
|
126
210
|
}
|
|
@@ -137,8 +221,30 @@ export class SqliteDatabaseAdapter {
|
|
|
137
221
|
*/
|
|
138
222
|
async commitTransaction(token) {
|
|
139
223
|
this.#assertTransaction(token);
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
248
|
}
|
|
143
249
|
/**
|
|
144
250
|
* Rolls back an open sqlite transaction.
|
|
@@ -147,8 +253,21 @@ export class SqliteDatabaseAdapter {
|
|
|
147
253
|
*/
|
|
148
254
|
async rollbackTransaction(token) {
|
|
149
255
|
this.#assertTransaction(token);
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}
|
|
152
271
|
}
|
|
153
272
|
/**
|
|
154
273
|
* Creates a savepoint in an open sqlite transaction.
|
|
@@ -180,29 +299,90 @@ export class SqliteDatabaseAdapter {
|
|
|
180
299
|
this.#assertTransaction(token);
|
|
181
300
|
this.#database.exec('release savepoint ' + quoteIdentifier(name));
|
|
182
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
|
+
}
|
|
183
336
|
#assertTransaction(token) {
|
|
337
|
+
this.#assertDatabaseOpen();
|
|
184
338
|
if (!this.#transactions.has(token.id)) {
|
|
185
339
|
throw new Error('Unknown transaction token: ' + token.id);
|
|
186
340
|
}
|
|
187
341
|
}
|
|
342
|
+
#assertDatabaseOpen() {
|
|
343
|
+
if (!this.#databaseOpen) {
|
|
344
|
+
throw new Error('SQLite database is closed');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
188
347
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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');
|
|
206
386
|
}
|
|
207
387
|
function normalizeRows(rows) {
|
|
208
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/data-table": "0.
|
|
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,
|
|
@@ -133,6 +209,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
133
209
|
* @returns A promise that resolves once execution completes.
|
|
134
210
|
*/
|
|
135
211
|
async executeScript(sql: string, transaction?: TransactionToken): Promise<void> {
|
|
212
|
+
this.#assertDatabaseOpen()
|
|
213
|
+
|
|
136
214
|
if (transaction) {
|
|
137
215
|
this.#assertTransaction(transaction)
|
|
138
216
|
}
|
|
@@ -147,6 +225,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
147
225
|
* @returns `true` when the table exists.
|
|
148
226
|
*/
|
|
149
227
|
async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
|
|
228
|
+
this.#assertDatabaseOpen()
|
|
229
|
+
|
|
150
230
|
if (transaction) {
|
|
151
231
|
this.#assertTransaction(transaction)
|
|
152
232
|
}
|
|
@@ -157,8 +237,9 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
157
237
|
let statement = this.#database.prepare(
|
|
158
238
|
'select 1 from ' + masterTable + ' where type = ? and name = ? limit 1',
|
|
159
239
|
)
|
|
240
|
+
// node:sqlite returns `undefined` for a missing row while bun:sqlite returns `null`
|
|
160
241
|
let row = statement.get('table', table.name)
|
|
161
|
-
return row
|
|
242
|
+
return row != null
|
|
162
243
|
}
|
|
163
244
|
|
|
164
245
|
/**
|
|
@@ -173,6 +254,8 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
173
254
|
column: string,
|
|
174
255
|
transaction?: TransactionToken,
|
|
175
256
|
): Promise<boolean> {
|
|
257
|
+
this.#assertDatabaseOpen()
|
|
258
|
+
|
|
176
259
|
if (transaction) {
|
|
177
260
|
this.#assertTransaction(transaction)
|
|
178
261
|
}
|
|
@@ -186,12 +269,56 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
186
269
|
return rows.some((row) => row.name === column)
|
|
187
270
|
}
|
|
188
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
|
+
|
|
189
314
|
/**
|
|
190
315
|
* Starts a sqlite transaction.
|
|
191
316
|
* @param options Transaction options.
|
|
192
317
|
* @returns Transaction token.
|
|
193
318
|
*/
|
|
194
319
|
async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
|
|
320
|
+
this.#assertDatabaseOpen()
|
|
321
|
+
|
|
195
322
|
if (options?.isolationLevel === 'read uncommitted') {
|
|
196
323
|
this.#database.exec('pragma read_uncommitted = true')
|
|
197
324
|
}
|
|
@@ -212,8 +339,26 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
212
339
|
*/
|
|
213
340
|
async commitTransaction(token: TransactionToken): Promise<void> {
|
|
214
341
|
this.#assertTransaction(token)
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
362
|
}
|
|
218
363
|
|
|
219
364
|
/**
|
|
@@ -223,8 +368,22 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
223
368
|
*/
|
|
224
369
|
async rollbackTransaction(token: TransactionToken): Promise<void> {
|
|
225
370
|
this.#assertTransaction(token)
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
387
|
}
|
|
229
388
|
|
|
230
389
|
/**
|
|
@@ -260,30 +419,107 @@ export class SqliteDatabaseAdapter implements DatabaseAdapter {
|
|
|
260
419
|
this.#database.exec('release savepoint ' + quoteIdentifier(name))
|
|
261
420
|
}
|
|
262
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
|
+
|
|
263
461
|
#assertTransaction(token: TransactionToken): void {
|
|
462
|
+
this.#assertDatabaseOpen()
|
|
264
463
|
if (!this.#transactions.has(token.id)) {
|
|
265
464
|
throw new Error('Unknown transaction token: ' + token.id)
|
|
266
465
|
}
|
|
267
466
|
}
|
|
467
|
+
|
|
468
|
+
#assertDatabaseOpen(): void {
|
|
469
|
+
if (!this.#databaseOpen) {
|
|
470
|
+
throw new Error('SQLite database is closed')
|
|
471
|
+
}
|
|
472
|
+
}
|
|
268
473
|
}
|
|
269
474
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
)
|
|
287
523
|
}
|
|
288
524
|
|
|
289
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,CAqC/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"}
|