@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.
@@ -0,0 +1,263 @@
1
+ import type {
2
+ AdapterCapabilityOverrides,
3
+ AdapterExecuteRequest,
4
+ AdapterResult,
5
+ DatabaseAdapter,
6
+ TransactionOptions,
7
+ TransactionToken,
8
+ } from '@remix-run/data-table'
9
+ import { getTablePrimaryKey } from '@remix-run/data-table'
10
+ import type { Database as BetterSqliteDatabase, RunResult } from 'better-sqlite3'
11
+
12
+ import { compileSqliteStatement } from './sql-compiler.ts'
13
+
14
+ /**
15
+ * Better SQLite3 database handle accepted by the sqlite adapter.
16
+ */
17
+ export type SqliteDatabaseConnection = BetterSqliteDatabase
18
+
19
+ /**
20
+ * Sqlite adapter configuration.
21
+ */
22
+ export type SqliteDatabaseAdapterOptions = {
23
+ capabilities?: AdapterCapabilityOverrides
24
+ }
25
+
26
+ /**
27
+ * `DatabaseAdapter` implementation for Better SQLite3.
28
+ */
29
+ export class SqliteDatabaseAdapter implements DatabaseAdapter {
30
+ dialect = 'sqlite'
31
+ capabilities
32
+
33
+ #database: SqliteDatabaseConnection
34
+ #transactions = new Set<string>()
35
+ #transactionCounter = 0
36
+
37
+ constructor(database: SqliteDatabaseConnection, options?: SqliteDatabaseAdapterOptions) {
38
+ this.#database = database
39
+ this.capabilities = {
40
+ returning: options?.capabilities?.returning ?? true,
41
+ savepoints: options?.capabilities?.savepoints ?? true,
42
+ upsert: options?.capabilities?.upsert ?? true,
43
+ }
44
+ }
45
+
46
+ async execute(request: AdapterExecuteRequest): Promise<AdapterResult> {
47
+ if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
48
+ return {
49
+ affectedRows: 0,
50
+ insertId: undefined,
51
+ rows: request.statement.returning ? [] : undefined,
52
+ }
53
+ }
54
+
55
+ let statement = compileSqliteStatement(request.statement)
56
+ let prepared = this.#database.prepare(statement.text)
57
+
58
+ if (prepared.reader) {
59
+ let rows = normalizeRows(prepared.all(...statement.values))
60
+
61
+ if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
62
+ rows = normalizeCountRows(rows)
63
+ }
64
+
65
+ return {
66
+ rows,
67
+ affectedRows: normalizeAffectedRowsForReader(request.statement.kind, rows),
68
+ insertId: normalizeInsertIdForReader(request.statement.kind, request.statement, rows),
69
+ }
70
+ }
71
+
72
+ let result = prepared.run(...statement.values)
73
+
74
+ return {
75
+ affectedRows: normalizeAffectedRowsForRun(request.statement.kind, result),
76
+ insertId: normalizeInsertIdForRun(request.statement.kind, request.statement, result),
77
+ }
78
+ }
79
+
80
+ async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
81
+ if (options?.isolationLevel === 'read uncommitted') {
82
+ this.#database.pragma('read_uncommitted = true')
83
+ }
84
+
85
+ this.#database.exec('begin')
86
+
87
+ this.#transactionCounter += 1
88
+ let token = { id: 'tx_' + String(this.#transactionCounter) }
89
+ this.#transactions.add(token.id)
90
+
91
+ return token
92
+ }
93
+
94
+ async commitTransaction(token: TransactionToken): Promise<void> {
95
+ this.#assertTransaction(token)
96
+ this.#database.exec('commit')
97
+ this.#transactions.delete(token.id)
98
+ }
99
+
100
+ async rollbackTransaction(token: TransactionToken): Promise<void> {
101
+ this.#assertTransaction(token)
102
+ this.#database.exec('rollback')
103
+ this.#transactions.delete(token.id)
104
+ }
105
+
106
+ async createSavepoint(token: TransactionToken, name: string): Promise<void> {
107
+ this.#assertTransaction(token)
108
+ this.#database.exec('savepoint ' + quoteIdentifier(name))
109
+ }
110
+
111
+ async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {
112
+ this.#assertTransaction(token)
113
+ this.#database.exec('rollback to savepoint ' + quoteIdentifier(name))
114
+ }
115
+
116
+ async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {
117
+ this.#assertTransaction(token)
118
+ this.#database.exec('release savepoint ' + quoteIdentifier(name))
119
+ }
120
+
121
+ #assertTransaction(token: TransactionToken): void {
122
+ if (!this.#transactions.has(token.id)) {
123
+ throw new Error('Unknown transaction token: ' + token.id)
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Creates a sqlite `DatabaseAdapter`.
130
+ * @param database Better SQLite3 database instance.
131
+ * @param options Optional adapter capability overrides.
132
+ * @returns A configured sqlite adapter.
133
+ */
134
+ export function createSqliteDatabaseAdapter(
135
+ database: SqliteDatabaseConnection,
136
+ options?: SqliteDatabaseAdapterOptions,
137
+ ): SqliteDatabaseAdapter {
138
+ return new SqliteDatabaseAdapter(database, options)
139
+ }
140
+
141
+ function normalizeRows(rows: unknown[]): Record<string, unknown>[] {
142
+ return rows.map((row) => {
143
+ if (typeof row !== 'object' || row === null) {
144
+ return {}
145
+ }
146
+
147
+ return { ...(row as Record<string, unknown>) }
148
+ })
149
+ }
150
+
151
+ function normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
152
+ return rows.map((row) => {
153
+ let count = row.count
154
+
155
+ if (typeof count === 'string') {
156
+ let numeric = Number(count)
157
+
158
+ if (!Number.isNaN(numeric)) {
159
+ return {
160
+ ...row,
161
+ count: numeric,
162
+ }
163
+ }
164
+ }
165
+
166
+ if (typeof count === 'bigint') {
167
+ return {
168
+ ...row,
169
+ count: Number(count),
170
+ }
171
+ }
172
+
173
+ return row
174
+ })
175
+ }
176
+
177
+ function normalizeAffectedRowsForReader(
178
+ kind: AdapterExecuteRequest['statement']['kind'],
179
+ rows: Record<string, unknown>[],
180
+ ): number | undefined {
181
+ if (isWriteStatementKind(kind)) {
182
+ return rows.length
183
+ }
184
+
185
+ return undefined
186
+ }
187
+
188
+ function normalizeInsertIdForReader(
189
+ kind: AdapterExecuteRequest['statement']['kind'],
190
+ statement: AdapterExecuteRequest['statement'],
191
+ rows: Record<string, unknown>[],
192
+ ): unknown {
193
+ if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
194
+ return undefined
195
+ }
196
+
197
+ let primaryKey = getTablePrimaryKey(statement.table)
198
+
199
+ if (primaryKey.length !== 1) {
200
+ return undefined
201
+ }
202
+
203
+ let key = primaryKey[0]
204
+ let row = rows[rows.length - 1]
205
+
206
+ return row ? row[key] : undefined
207
+ }
208
+
209
+ function normalizeAffectedRowsForRun(
210
+ kind: AdapterExecuteRequest['statement']['kind'],
211
+ result: RunResult,
212
+ ): number | undefined {
213
+ if (kind === 'select' || kind === 'count' || kind === 'exists') {
214
+ return undefined
215
+ }
216
+
217
+ return result.changes
218
+ }
219
+
220
+ function normalizeInsertIdForRun(
221
+ kind: AdapterExecuteRequest['statement']['kind'],
222
+ statement: AdapterExecuteRequest['statement'],
223
+ result: RunResult,
224
+ ): unknown {
225
+ if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
226
+ return undefined
227
+ }
228
+
229
+ if (getTablePrimaryKey(statement.table).length !== 1) {
230
+ return undefined
231
+ }
232
+
233
+ return result.lastInsertRowid
234
+ }
235
+
236
+ function quoteIdentifier(value: string): string {
237
+ return '"' + value.replace(/"/g, '""') + '"'
238
+ }
239
+
240
+ function isWriteStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
241
+ return (
242
+ kind === 'insert' ||
243
+ kind === 'insertMany' ||
244
+ kind === 'update' ||
245
+ kind === 'delete' ||
246
+ kind === 'upsert'
247
+ )
248
+ }
249
+
250
+ function isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
251
+ return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
252
+ }
253
+
254
+ function isInsertStatement(
255
+ statement: AdapterExecuteRequest['statement'],
256
+ ): statement is Extract<
257
+ AdapterExecuteRequest['statement'],
258
+ { kind: 'insert' | 'insertMany' | 'upsert' }
259
+ > {
260
+ return (
261
+ statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert'
262
+ )
263
+ }