@fabrika/db 0.8.2 → 0.9.0-cloudflare.2
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/package.json +44 -12
- package/src/admissions.ts +78 -12
- package/src/application-rows.ts +108 -0
- package/src/capability.ts +5 -0
- package/src/codegen-sqlite.ts +154 -0
- package/src/codegen.ts +59 -9
- package/src/database.ts +208 -56
- package/src/errors.ts +1 -0
- package/src/file-columns.ts +4 -1
- package/src/framework-tables.ts +28 -8
- package/src/framework.ts +10 -2
- package/src/guard.ts +8 -0
- package/src/index.ts +9 -1
- package/src/job-leases.ts +157 -37
- package/src/links.ts +8 -1
- package/src/migrations-sqlite.ts +132 -0
- package/src/nodes.ts +45 -9
- package/src/orm.ts +25 -15
- package/src/policy.ts +8 -2
- package/src/postgres.ts +26 -0
- package/src/registry.ts +8 -2
- package/src/sql.ts +15 -7
- package/src/sqlite-bun.ts +122 -0
- package/src/sqlite-migrate.ts +242 -0
- package/src/sqlite.ts +224 -0
- package/src/store.ts +336 -62
- package/src/stored-files.ts +149 -46
- package/src/tables.ts +16 -0
- package/src/temporal.ts +115 -9
- package/src/testing-shared.ts +733 -0
- package/src/testing-sqlite.ts +513 -0
- package/src/testing.ts +170 -482
- package/types/src/admissions.d.ts +21 -0
- package/types/src/application-rows.d.ts +58 -0
- package/types/src/builtins.d.ts +42 -0
- package/types/src/capability.d.ts +119 -0
- package/types/src/codegen-sqlite.d.ts +6 -0
- package/types/src/codegen.d.ts +82 -0
- package/types/src/database.d.ts +179 -0
- package/types/src/description.d.ts +109 -0
- package/types/src/errors.d.ts +98 -0
- package/types/src/file-columns.d.ts +37 -0
- package/types/src/file-references.d.ts +27 -0
- package/types/src/framework-tables.d.ts +59 -0
- package/types/src/framework.d.ts +37 -0
- package/types/src/guard.d.ts +23 -0
- package/types/src/identifier.d.ts +4 -0
- package/types/src/index.d.ts +10 -0
- package/types/src/job-leases.d.ts +93 -0
- package/types/src/links.d.ts +13 -0
- package/types/src/migrations-sqlite.d.ts +4 -0
- package/types/src/migrations.d.ts +25 -0
- package/types/src/nodes.d.ts +63 -0
- package/types/src/orm-types.d.ts +638 -0
- package/types/src/orm.d.ts +77 -0
- package/types/src/policy.d.ts +354 -0
- package/types/src/postgres.d.ts +12 -0
- package/types/src/production.d.ts +5 -0
- package/types/src/registry.d.ts +40 -0
- package/types/src/sql.d.ts +59 -0
- package/types/src/sqlite-bun.d.ts +13 -0
- package/types/src/sqlite-migrate.d.ts +28 -0
- package/types/src/sqlite.d.ts +24 -0
- package/types/src/store.d.ts +345 -0
- package/types/src/stored-files.d.ts +98 -0
- package/types/src/tables.d.ts +21 -0
- package/types/src/temporal.d.ts +33 -0
- package/types/src/testing-shared.d.ts +201 -0
- package/types/src/testing-sqlite.d.ts +22 -0
- package/types/src/testing.d.ts +35 -0
- package/types/src/vocabulary-tables.d.ts +225 -0
package/src/sqlite.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// @fabrika/db/sqlite — the data layer's engine over a Cloudflare Durable Object's SQLite (ADR-0065). Worker-safe: no
|
|
2
|
+
// Bun import. `sqlite-bun.ts` gives the tests a `bun:sqlite` database of the same shape.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type BinaryOperationNode,
|
|
6
|
+
type CompiledQuery,
|
|
7
|
+
type DatabaseConnection,
|
|
8
|
+
type DatabaseIntrospector,
|
|
9
|
+
type Dialect,
|
|
10
|
+
type DialectAdapter,
|
|
11
|
+
type Driver,
|
|
12
|
+
Kysely,
|
|
13
|
+
OperatorNode,
|
|
14
|
+
type QueryResult,
|
|
15
|
+
type SelectModifierNode,
|
|
16
|
+
type SelectQueryNode,
|
|
17
|
+
SqliteAdapter,
|
|
18
|
+
SqliteIntrospector,
|
|
19
|
+
SqliteQueryCompiler,
|
|
20
|
+
} from 'kysely'
|
|
21
|
+
import { Database, freezePrototypeChain, type LooseDB, type TransactionRunner } from './database'
|
|
22
|
+
import { DatabaseError, QueryRefusedError } from './errors'
|
|
23
|
+
|
|
24
|
+
/** A value `sql.exec` binds and answers; a Durable Object's `SqlStorageValue`. */
|
|
25
|
+
export type SqliteValue = ArrayBuffer | string | number | null
|
|
26
|
+
|
|
27
|
+
export interface SqliteCursor {
|
|
28
|
+
toArray(): Record<string, SqliteValue>[]
|
|
29
|
+
readonly rowsWritten: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What the engine needs of a Durable Object's `ctx.storage`; `DurableObjectStorage` is assignable to it. */
|
|
33
|
+
export interface SqliteStorage {
|
|
34
|
+
readonly sql: {
|
|
35
|
+
exec(query: string, ...bindings: SqliteValue[]): SqliteCursor
|
|
36
|
+
}
|
|
37
|
+
/** Commits when `closure` resolves, rolls back when it rejects; the object serves no other event meanwhile. */
|
|
38
|
+
transaction<T>(closure: () => Promise<T>): Promise<T>
|
|
39
|
+
/** Runs `closure` in one transaction of its own, nested in a savepoint inside an open one; rolls back when it throws. */
|
|
40
|
+
transactionSync<T>(closure: () => T): T
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const LIKE_OPERATORS: ReadonlyMap<string, string> = new Map([
|
|
44
|
+
['like', 'like'],
|
|
45
|
+
['ilike', 'like'],
|
|
46
|
+
['not like', 'not like'],
|
|
47
|
+
['not ilike', 'not like'],
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
/** The modifiers a Durable Object needs none of: while a transaction is open it serves no other event. */
|
|
51
|
+
const LOCK_MODIFIERS: ReadonlySet<string> = new Set(['ForUpdate', 'ForShare', 'ForKeyShare', 'ForNoKeyUpdate', 'NoWait', 'SkipLocked'])
|
|
52
|
+
|
|
53
|
+
function isLockModifier(modifier: SelectModifierNode): boolean {
|
|
54
|
+
return modifier.modifier !== undefined && LOCK_MODIFIERS.has(modifier.modifier)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* SQLite's forms of what the layer binds and builds. LIKE takes `\` as its escape character only when told, so every
|
|
59
|
+
* pattern gets `escape '\'`, as Postgres has by default; `ilike` is `like`, case-insensitive for ASCII only.
|
|
60
|
+
*/
|
|
61
|
+
class SqliteCompiler extends SqliteQueryCompiler {
|
|
62
|
+
protected override addParameter(parameter: unknown): void {
|
|
63
|
+
super.addParameter(sqliteParameter(parameter))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected override visitBinaryOperation(node: BinaryOperationNode): void {
|
|
67
|
+
const pattern = OperatorNode.is(node.operator) ? LIKE_OPERATORS.get(node.operator.operator) : undefined
|
|
68
|
+
if (pattern === undefined) {
|
|
69
|
+
super.visitBinaryOperation(node)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
this.visitNode(node.leftOperand)
|
|
73
|
+
this.append(` ${pattern} `)
|
|
74
|
+
this.visitNode(node.rightOperand)
|
|
75
|
+
this.append(" escape '\\'")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected override visitSelectQuery(node: SelectQueryNode): void {
|
|
79
|
+
const modifiers = node.endModifiers?.filter((modifier) => !isLockModifier(modifier))
|
|
80
|
+
super.visitSelectQuery({ ...node, endModifiers: modifiers === undefined || modifiers.length === 0 ? undefined : modifiers })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sqliteParameter(parameter: unknown): unknown {
|
|
85
|
+
if (parameter instanceof Date) {
|
|
86
|
+
if (Number.isNaN(parameter.getTime())) {
|
|
87
|
+
throw new QueryRefusedError('invalid_value', 'an invalid Date cannot be bound')
|
|
88
|
+
}
|
|
89
|
+
return parameter.toISOString()
|
|
90
|
+
}
|
|
91
|
+
if (typeof parameter === 'boolean') {
|
|
92
|
+
return parameter ? 1 : 0
|
|
93
|
+
}
|
|
94
|
+
if (typeof parameter === 'bigint') {
|
|
95
|
+
const value = Number(parameter)
|
|
96
|
+
if (!Number.isSafeInteger(value)) {
|
|
97
|
+
throw new QueryRefusedError('invalid_value', 'a bigint outside the safe integer range cannot be bound on SQLite')
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
return parameter
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sqliteCompiler = new SqliteCompiler()
|
|
105
|
+
freezePrototypeChain(sqliteCompiler)
|
|
106
|
+
|
|
107
|
+
function binding(value: unknown): SqliteValue {
|
|
108
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || value instanceof ArrayBuffer) {
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
111
|
+
throw new QueryRefusedError('invalid_value', 'a value SQLite cannot bind: string, number, ArrayBuffer or null')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The SQLSTATE of each constraint failure, by the start of the message workerd and bun:sqlite both give. */
|
|
115
|
+
const CONSTRAINT_STATES: readonly (readonly [string, string])[] = Object.freeze([
|
|
116
|
+
['UNIQUE constraint failed', '23505'],
|
|
117
|
+
['FOREIGN KEY constraint failed', '23503'],
|
|
118
|
+
['CHECK constraint failed', '23514'],
|
|
119
|
+
['NOT NULL constraint failed', '23502'],
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
/** The driver's failure as a `DatabaseError`; its message can quote row values, so none of it is kept. */
|
|
123
|
+
function sqliteError(error: unknown): DatabaseError {
|
|
124
|
+
if (error instanceof DatabaseError) {
|
|
125
|
+
return error
|
|
126
|
+
}
|
|
127
|
+
const message = error instanceof Error ? error.message : ''
|
|
128
|
+
const match = CONSTRAINT_STATES.find(([prefix]) => message.startsWith(prefix))
|
|
129
|
+
return new DatabaseError(match === undefined ? null : match[1])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Postgres's SQLSTATE for a write in a `READ ONLY` transaction, which the backstop answers too. */
|
|
133
|
+
const READ_ONLY_TRANSACTION = '25006'
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A read-only statement's rows, run in a `transactionSync` of its own that holds nothing across an await. A statement
|
|
137
|
+
* that wrote throws, which rolls back what it wrote: the backstop below the layer's refusal of a write node.
|
|
138
|
+
*/
|
|
139
|
+
function readRows(storage: SqliteStorage, query: string, bindings: SqliteValue[]): Record<string, SqliteValue>[] {
|
|
140
|
+
return storage.transactionSync(() => {
|
|
141
|
+
const cursor = storage.sql.exec(query, ...bindings)
|
|
142
|
+
// workerd counts `rowsWritten` as the cursor is read: only a consumed cursor reports the whole statement.
|
|
143
|
+
const rows = cursor.toArray()
|
|
144
|
+
if (cursor.rowsWritten > 0) {
|
|
145
|
+
throw new DatabaseError(READ_ONLY_TRANSACTION)
|
|
146
|
+
}
|
|
147
|
+
return rows
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function connectionOf(storage: SqliteStorage, readOnly: boolean): DatabaseConnection {
|
|
152
|
+
return {
|
|
153
|
+
async executeQuery<R>(compiled: CompiledQuery): Promise<QueryResult<R>> {
|
|
154
|
+
const bindings = compiled.parameters.map(binding)
|
|
155
|
+
let rows: Record<string, SqliteValue>[]
|
|
156
|
+
try {
|
|
157
|
+
rows = readOnly ? readRows(storage, compiled.sql, bindings) : storage.sql.exec(compiled.sql, ...bindings).toArray()
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw sqliteError(error)
|
|
160
|
+
}
|
|
161
|
+
// Untyped rows become the caller's row type, as Bun.SQL's `any` rows do on Postgres.
|
|
162
|
+
return { rows: rows as R[] }
|
|
163
|
+
},
|
|
164
|
+
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
|
165
|
+
throw new QueryRefusedError('statement_shape', 'a SQLite database does not stream a query')
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function refuseKyselyTransaction(): Promise<never> {
|
|
171
|
+
return Promise.reject(new QueryRefusedError('statement_shape', "a SQLite transaction runs through the storage's transaction, never Kysely's"))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class SqliteDialect implements Dialect {
|
|
175
|
+
readonly #storage: SqliteStorage
|
|
176
|
+
readonly #readOnly: boolean
|
|
177
|
+
|
|
178
|
+
constructor(storage: SqliteStorage, readOnly: boolean) {
|
|
179
|
+
this.#storage = storage
|
|
180
|
+
this.#readOnly = readOnly
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
createAdapter(): DialectAdapter {
|
|
184
|
+
return new SqliteAdapter()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
createDriver(): Driver {
|
|
188
|
+
const connection = connectionOf(this.#storage, this.#readOnly)
|
|
189
|
+
return {
|
|
190
|
+
async init() {},
|
|
191
|
+
async acquireConnection() {
|
|
192
|
+
return connection
|
|
193
|
+
},
|
|
194
|
+
beginTransaction: refuseKyselyTransaction,
|
|
195
|
+
commitTransaction: refuseKyselyTransaction,
|
|
196
|
+
rollbackTransaction: refuseKyselyTransaction,
|
|
197
|
+
async releaseConnection() {},
|
|
198
|
+
async destroy() {},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
createQueryCompiler(): SqliteQueryCompiler {
|
|
203
|
+
return sqliteCompiler
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
createIntrospector(db: Kysely<LooseDB>): DatabaseIntrospector {
|
|
207
|
+
return new SqliteIntrospector(db)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The data layer's database over a Durable Object's storage. A read-only transaction opens no storage transaction,
|
|
213
|
+
* which would hold back every other event of the object across the work's awaits: each of its statements runs in one
|
|
214
|
+
* of its own, so two of them may see different committed states, as under Postgres's READ COMMITTED. The idle timeout
|
|
215
|
+
* has no SQLite form.
|
|
216
|
+
*/
|
|
217
|
+
export function openSqlite(storage: SqliteStorage): Database {
|
|
218
|
+
const kysely = new Kysely<LooseDB>({ dialect: new SqliteDialect(storage, false) })
|
|
219
|
+
const reads = new Kysely<LooseDB>({ dialect: new SqliteDialect(storage, true) })
|
|
220
|
+
const transaction: TransactionRunner = (options, body) => options.readOnly ? body(reads) : storage.transaction(() => body(kysely))
|
|
221
|
+
return Database.fromEngine({ dialect: 'sqlite', kysely, compiler: sqliteCompiler, transaction })
|
|
222
|
+
}
|
|
223
|
+
// Application modules load after this one: a replaced opener would receive the object's storage.
|
|
224
|
+
Object.freeze(openSqlite)
|