@remix-run/data-table-sqlite 0.0.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Placeholder package
1
+ # data-table-sqlite
2
2
 
3
- This package is intentionally empty and published only as a temporary placeholder.
3
+ SQLite adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).
4
+ Use this package when you want `data-table` APIs backed by `better-sqlite3`.
5
+
6
+ ## Features
7
+
8
+ - **Native `better-sqlite3` Integration**: Works well for local and embedded deployments
9
+ - **Full `data-table` API Support**: Queries, relations, writes, and transactions
10
+ - **SQLite Capabilities Enabled By Default**:
11
+ - `returning: true`
12
+ - `savepoints: true`
13
+ - `upsert: true`
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm i remix better-sqlite3
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import Database from 'better-sqlite3'
25
+ import { createDatabase } from 'remix/data-table'
26
+ import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
27
+
28
+ let sqlite = new Database('app.db')
29
+ let db = createDatabase(createSqliteDatabaseAdapter(sqlite))
30
+ ```
31
+
32
+ This is a good fit for local development, embedded deployments, and single-node services.
33
+
34
+ ## Adapter Capabilities
35
+
36
+ `data-table-sqlite` reports this capability set by default:
37
+
38
+ - `returning: true`
39
+ - `savepoints: true`
40
+ - `upsert: true`
41
+
42
+ ## Advanced Usage
43
+
44
+ ### In-Memory Database For Tests
45
+
46
+ ```ts
47
+ import Database from 'better-sqlite3'
48
+ import { createDatabase } from 'remix/data-table'
49
+ import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
50
+
51
+ let sqlite = new Database(':memory:')
52
+ let db = createDatabase(createSqliteDatabaseAdapter(sqlite))
53
+ ```
54
+
55
+ ### Capability Overrides For Fallback Testing
56
+
57
+ ```ts
58
+ import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
59
+
60
+ let adapter = createSqliteDatabaseAdapter(sqlite, {
61
+ capabilities: {
62
+ returning: false,
63
+ },
64
+ })
65
+ ```
66
+
67
+ ## Related Packages
68
+
69
+ - [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
70
+ - [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema definitions and validation
71
+ - [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
72
+ - [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter
73
+
74
+ ## License
75
+
76
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,3 @@
1
+ export type { SqliteDatabaseAdapterOptions, SqliteDatabaseConnection } from './lib/adapter.ts';
2
+ export { createSqliteDatabaseAdapter, SqliteDatabaseAdapter } from './lib/adapter.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,4BAA4B,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC9F,OAAO,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createSqliteDatabaseAdapter, SqliteDatabaseAdapter } from "./lib/adapter.js";
@@ -0,0 +1,40 @@
1
+ import type { AdapterCapabilityOverrides, AdapterExecuteRequest, AdapterResult, DatabaseAdapter, TransactionOptions, TransactionToken } from '@remix-run/data-table';
2
+ import type { Database as BetterSqliteDatabase } from 'better-sqlite3';
3
+ /**
4
+ * Better SQLite3 database handle accepted by the sqlite adapter.
5
+ */
6
+ export type SqliteDatabaseConnection = BetterSqliteDatabase;
7
+ /**
8
+ * Sqlite adapter configuration.
9
+ */
10
+ export type SqliteDatabaseAdapterOptions = {
11
+ capabilities?: AdapterCapabilityOverrides;
12
+ };
13
+ /**
14
+ * `DatabaseAdapter` implementation for Better SQLite3.
15
+ */
16
+ export declare class SqliteDatabaseAdapter implements DatabaseAdapter {
17
+ #private;
18
+ dialect: string;
19
+ capabilities: {
20
+ returning: boolean;
21
+ savepoints: boolean;
22
+ upsert: boolean;
23
+ };
24
+ constructor(database: SqliteDatabaseConnection, options?: SqliteDatabaseAdapterOptions);
25
+ execute(request: AdapterExecuteRequest): Promise<AdapterResult>;
26
+ beginTransaction(options?: TransactionOptions): Promise<TransactionToken>;
27
+ commitTransaction(token: TransactionToken): Promise<void>;
28
+ rollbackTransaction(token: TransactionToken): Promise<void>;
29
+ createSavepoint(token: TransactionToken, name: string): Promise<void>;
30
+ rollbackToSavepoint(token: TransactionToken, name: string): Promise<void>;
31
+ releaseSavepoint(token: TransactionToken, name: string): Promise<void>;
32
+ }
33
+ /**
34
+ * Creates a sqlite `DatabaseAdapter`.
35
+ * @param database Better SQLite3 database instance.
36
+ * @param options Optional adapter capability overrides.
37
+ * @returns A configured sqlite adapter.
38
+ */
39
+ export declare function createSqliteDatabaseAdapter(database: SqliteDatabaseConnection, options?: SqliteDatabaseAdapterOptions): SqliteDatabaseAdapter;
40
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/lib/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,KAAK,EAAE,QAAQ,IAAI,oBAAoB,EAAa,MAAM,gBAAgB,CAAA;AAIjF;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,oBAAoB,CAAA;AAE3D;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,YAAY,CAAC,EAAE,0BAA0B,CAAA;CAC1C,CAAA;AAED;;GAEG;AACH,qBAAa,qBAAsB,YAAW,eAAe;;IAC3D,OAAO,SAAW;IAClB,YAAY;;;;MAAA;IAMZ,YAAY,QAAQ,EAAE,wBAAwB,EAAE,OAAO,CAAC,EAAE,4BAA4B,EAOrF;IAEK,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CAgCpE;IAEK,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAY9E;IAEK,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9D;IAEK,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhE;IAEK,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;IAEK,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;IAEK,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;CAOF;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,wBAAwB,EAClC,OAAO,CAAC,EAAE,4BAA4B,GACrC,qBAAqB,CAEvB"}
@@ -0,0 +1,171 @@
1
+ import { getTablePrimaryKey } from '@remix-run/data-table';
2
+ import { compileSqliteStatement } from "./sql-compiler.js";
3
+ /**
4
+ * `DatabaseAdapter` implementation for Better SQLite3.
5
+ */
6
+ export class SqliteDatabaseAdapter {
7
+ dialect = 'sqlite';
8
+ capabilities;
9
+ #database;
10
+ #transactions = new Set();
11
+ #transactionCounter = 0;
12
+ constructor(database, options) {
13
+ this.#database = database;
14
+ this.capabilities = {
15
+ returning: options?.capabilities?.returning ?? true,
16
+ savepoints: options?.capabilities?.savepoints ?? true,
17
+ upsert: options?.capabilities?.upsert ?? true,
18
+ };
19
+ }
20
+ async execute(request) {
21
+ if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
22
+ return {
23
+ affectedRows: 0,
24
+ insertId: undefined,
25
+ rows: request.statement.returning ? [] : undefined,
26
+ };
27
+ }
28
+ let statement = compileSqliteStatement(request.statement);
29
+ let prepared = this.#database.prepare(statement.text);
30
+ if (prepared.reader) {
31
+ let rows = normalizeRows(prepared.all(...statement.values));
32
+ if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
33
+ rows = normalizeCountRows(rows);
34
+ }
35
+ return {
36
+ rows,
37
+ affectedRows: normalizeAffectedRowsForReader(request.statement.kind, rows),
38
+ insertId: normalizeInsertIdForReader(request.statement.kind, request.statement, rows),
39
+ };
40
+ }
41
+ let result = prepared.run(...statement.values);
42
+ return {
43
+ affectedRows: normalizeAffectedRowsForRun(request.statement.kind, result),
44
+ insertId: normalizeInsertIdForRun(request.statement.kind, request.statement, result),
45
+ };
46
+ }
47
+ async beginTransaction(options) {
48
+ if (options?.isolationLevel === 'read uncommitted') {
49
+ this.#database.pragma('read_uncommitted = true');
50
+ }
51
+ this.#database.exec('begin');
52
+ this.#transactionCounter += 1;
53
+ let token = { id: 'tx_' + String(this.#transactionCounter) };
54
+ this.#transactions.add(token.id);
55
+ return token;
56
+ }
57
+ async commitTransaction(token) {
58
+ this.#assertTransaction(token);
59
+ this.#database.exec('commit');
60
+ this.#transactions.delete(token.id);
61
+ }
62
+ async rollbackTransaction(token) {
63
+ this.#assertTransaction(token);
64
+ this.#database.exec('rollback');
65
+ this.#transactions.delete(token.id);
66
+ }
67
+ async createSavepoint(token, name) {
68
+ this.#assertTransaction(token);
69
+ this.#database.exec('savepoint ' + quoteIdentifier(name));
70
+ }
71
+ async rollbackToSavepoint(token, name) {
72
+ this.#assertTransaction(token);
73
+ this.#database.exec('rollback to savepoint ' + quoteIdentifier(name));
74
+ }
75
+ async releaseSavepoint(token, name) {
76
+ this.#assertTransaction(token);
77
+ this.#database.exec('release savepoint ' + quoteIdentifier(name));
78
+ }
79
+ #assertTransaction(token) {
80
+ if (!this.#transactions.has(token.id)) {
81
+ throw new Error('Unknown transaction token: ' + token.id);
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * Creates a sqlite `DatabaseAdapter`.
87
+ * @param database Better SQLite3 database instance.
88
+ * @param options Optional adapter capability overrides.
89
+ * @returns A configured sqlite adapter.
90
+ */
91
+ export function createSqliteDatabaseAdapter(database, options) {
92
+ return new SqliteDatabaseAdapter(database, options);
93
+ }
94
+ function normalizeRows(rows) {
95
+ return rows.map((row) => {
96
+ if (typeof row !== 'object' || row === null) {
97
+ return {};
98
+ }
99
+ return { ...row };
100
+ });
101
+ }
102
+ function normalizeCountRows(rows) {
103
+ return rows.map((row) => {
104
+ let count = row.count;
105
+ if (typeof count === 'string') {
106
+ let numeric = Number(count);
107
+ if (!Number.isNaN(numeric)) {
108
+ return {
109
+ ...row,
110
+ count: numeric,
111
+ };
112
+ }
113
+ }
114
+ if (typeof count === 'bigint') {
115
+ return {
116
+ ...row,
117
+ count: Number(count),
118
+ };
119
+ }
120
+ return row;
121
+ });
122
+ }
123
+ function normalizeAffectedRowsForReader(kind, rows) {
124
+ if (isWriteStatementKind(kind)) {
125
+ return rows.length;
126
+ }
127
+ return undefined;
128
+ }
129
+ function normalizeInsertIdForReader(kind, statement, rows) {
130
+ if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
131
+ return undefined;
132
+ }
133
+ let primaryKey = getTablePrimaryKey(statement.table);
134
+ if (primaryKey.length !== 1) {
135
+ return undefined;
136
+ }
137
+ let key = primaryKey[0];
138
+ let row = rows[rows.length - 1];
139
+ return row ? row[key] : undefined;
140
+ }
141
+ function normalizeAffectedRowsForRun(kind, result) {
142
+ if (kind === 'select' || kind === 'count' || kind === 'exists') {
143
+ return undefined;
144
+ }
145
+ return result.changes;
146
+ }
147
+ function normalizeInsertIdForRun(kind, statement, result) {
148
+ if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
149
+ return undefined;
150
+ }
151
+ if (getTablePrimaryKey(statement.table).length !== 1) {
152
+ return undefined;
153
+ }
154
+ return result.lastInsertRowid;
155
+ }
156
+ function quoteIdentifier(value) {
157
+ return '"' + value.replace(/"/g, '""') + '"';
158
+ }
159
+ function isWriteStatementKind(kind) {
160
+ return (kind === 'insert' ||
161
+ kind === 'insertMany' ||
162
+ kind === 'update' ||
163
+ kind === 'delete' ||
164
+ kind === 'upsert');
165
+ }
166
+ function isInsertStatementKind(kind) {
167
+ return kind === 'insert' || kind === 'insertMany' || kind === 'upsert';
168
+ }
169
+ function isInsertStatement(statement) {
170
+ return (statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert');
171
+ }
@@ -0,0 +1,8 @@
1
+ import type { AdapterStatement } from '@remix-run/data-table';
2
+ type CompiledSql = {
3
+ text: string;
4
+ values: unknown[];
5
+ };
6
+ export declare function compileSqliteStatement(statement: AdapterStatement): CompiledSql;
7
+ export {};
8
+ //# sourceMappingURL=sql-compiler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,uBAAuB,CAAA;AAMxE,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,EAAE,CAAA;CAClB,CAAA;AAMD,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,GAAG,WAAW,CAuG/E"}