@fabrika/db 0.0.0-stage → 0.0.1
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 +69 -5
- package/src/builtins.ts +94 -0
- package/src/capability.ts +93 -0
- package/src/codegen.ts +302 -0
- package/src/database.ts +413 -0
- package/src/description.ts +309 -0
- package/src/errors.ts +124 -0
- package/src/file-table-sql.ts +133 -0
- package/src/file-tables.ts +577 -0
- package/src/form-admissions.ts +82 -0
- package/src/framework-tables.ts +147 -0
- package/src/framework.ts +89 -0
- package/src/guard.ts +439 -0
- package/src/identifier.ts +43 -0
- package/src/index.ts +129 -0
- package/src/job-leases.ts +373 -0
- package/src/links.ts +61 -0
- package/src/migrations.ts +97 -0
- package/src/nodes.ts +344 -0
- package/src/orm-types.ts +596 -0
- package/src/orm.ts +891 -0
- package/src/policy.ts +1194 -0
- package/src/production.ts +26 -0
- package/src/registry.ts +57 -0
- package/src/sql.ts +732 -0
- package/src/store.ts +1693 -0
- package/src/tables.ts +167 -0
- package/src/temporal.ts +91 -0
- package/src/testing.ts +695 -0
- package/src/vocabulary-tables.ts +206 -0
- package/README.md +0 -3
package/src/database.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { SQL } from 'bun'
|
|
2
|
+
import { CompiledQuery, createQueryId, type Dialect, Kysely, PostgresQueryCompiler, type RootOperationNode, type TransactionBuilder } from 'kysely'
|
|
3
|
+
import { PostgresJSDialect } from 'kysely-postgres-js'
|
|
4
|
+
import { databaseError, QueryRefusedError } from './errors'
|
|
5
|
+
import { assertAllowedTree, type GuardContext } from './guard'
|
|
6
|
+
import { isPlainIdentifier } from './identifier'
|
|
7
|
+
|
|
8
|
+
type LooseDB = Record<string, Record<string, unknown>>
|
|
9
|
+
type Executor = Pick<Kysely<LooseDB>, 'executeQuery'>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* With `prepare: false`, Bun.SQL sends every parameter as text and a Date as `Date#toString()`, which Postgres
|
|
13
|
+
* refuses as a timestamp. Every guarded tree compiles here, so every Date parameter of the layer is bound as ISO text.
|
|
14
|
+
*/
|
|
15
|
+
class ParameterCompiler extends PostgresQueryCompiler {
|
|
16
|
+
protected override addParameter(parameter: unknown): void {
|
|
17
|
+
super.addParameter(parameter instanceof Date ? timestampText(parameter) : parameter)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function timestampText(date: Date): string {
|
|
22
|
+
if (Number.isNaN(date.getTime())) {
|
|
23
|
+
throw new QueryRefusedError('invalid_value', 'an invalid Date cannot be bound')
|
|
24
|
+
}
|
|
25
|
+
return date.toISOString()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const compiler = new ParameterCompiler()
|
|
29
|
+
freezePrototypeChain(compiler)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The compiler turns the guarded tree into SQL, so a patch to it after the guard ran would bypass the guard.
|
|
33
|
+
* Freezing it when this module loads, before application modules, makes such a patch throw instead.
|
|
34
|
+
*/
|
|
35
|
+
function freezePrototypeChain(object: object): void {
|
|
36
|
+
let current: object | null = object
|
|
37
|
+
while (current !== null && current !== Object.prototype) {
|
|
38
|
+
Object.freeze(current)
|
|
39
|
+
current = Object.getPrototypeOf(current)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** IAM's application id alphabet; `-` maps to `_`, which the alphabet lacks, so two ids never share a schema. */
|
|
44
|
+
const APP_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
45
|
+
/** Postgres truncates identifiers to 63 bytes; `app_` takes four of them. */
|
|
46
|
+
const MAX_APP_ID_LENGTH = 59
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `app_<id>`, the schema an application's migrations create (ADR-0017). `@fabrika/platform-node`'s
|
|
50
|
+
* `applicationDatabaseSchema` derives the same name for the migration runner; the two must agree.
|
|
51
|
+
*/
|
|
52
|
+
export function applicationSchema(appId: string): string {
|
|
53
|
+
if (!APP_ID.test(appId) || appId.length > MAX_APP_ID_LENGTH) {
|
|
54
|
+
throw new QueryRefusedError('invalid_name', 'the application id must be lowercase letters, digits and -, at most 59 characters')
|
|
55
|
+
}
|
|
56
|
+
return `app_${appId.replaceAll('-', '_')}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PostgresOptions {
|
|
60
|
+
readonly url: string
|
|
61
|
+
/** The application's schema (`app_<id>`), set as the connection's `search_path`. */
|
|
62
|
+
readonly schema: string
|
|
63
|
+
readonly max?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let openSession: (database: Database, readOnly: boolean) => Session
|
|
67
|
+
let transactionBuilder: (database: Database) => TransactionBuilder<LooseDB>
|
|
68
|
+
|
|
69
|
+
/** The connection the data layer runs on. It has no public query method: everything goes through a store. */
|
|
70
|
+
export class Database {
|
|
71
|
+
readonly #kysely: Kysely<LooseDB>
|
|
72
|
+
readonly #schema: string | undefined
|
|
73
|
+
|
|
74
|
+
private constructor(kysely: Kysely<LooseDB>, schema: string | undefined) {
|
|
75
|
+
this.#kysely = kysely
|
|
76
|
+
this.#schema = schema
|
|
77
|
+
Object.freeze(this)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static {
|
|
81
|
+
openSession = (database, readOnly) => new Session(database.#kysely, database.#kysely, readOnly)
|
|
82
|
+
transactionBuilder = (database) => database.#kysely.transaction()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Over `Bun.SQL` with `prepare: false`: Bun otherwise keeps one prepared statement per distinct SQL text per
|
|
87
|
+
* connection, and `in` lists of different lengths make that grow without bound.
|
|
88
|
+
*/
|
|
89
|
+
static postgres(options: PostgresOptions): Database {
|
|
90
|
+
if (!isPlainIdentifier(options.schema)) {
|
|
91
|
+
throw new QueryRefusedError('invalid_name', 'the schema must match ^[a-z_][a-z0-9_]*$')
|
|
92
|
+
}
|
|
93
|
+
const pool = new SQL({ url: options.url, adapter: 'postgres', prepare: false, max: options.max ?? 10, connection: { search_path: options.schema } })
|
|
94
|
+
return new Database(new Kysely<LooseDB>({ dialect: new PostgresJSDialect({ postgres: pool }) }), options.schema)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Any Kysely Postgres dialect; tests use one that records statements instead of sending them. */
|
|
98
|
+
static fromDialect(dialect: Dialect): Database {
|
|
99
|
+
return new Database(new Kysely<LooseDB>({ dialect }), undefined)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The application's schema, the connection's `search_path`; `undefined` over a bare dialect. */
|
|
103
|
+
get schema(): string | undefined {
|
|
104
|
+
return this.#schema
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async close(): Promise<void> {
|
|
108
|
+
await this.#kysely.destroy()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Application modules load after this one: a replaced `postgres` would receive the database URL.
|
|
112
|
+
Object.freeze(Database)
|
|
113
|
+
Object.freeze(Database.prototype)
|
|
114
|
+
|
|
115
|
+
/** The connection a session runs on, and whether the request running now may only read. */
|
|
116
|
+
export interface SessionSource {
|
|
117
|
+
readonly database: Database
|
|
118
|
+
readonly readOnly?: () => boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The one way to a session: every statement of a request — a store's, a locator's, a framework table's — asks the
|
|
123
|
+
* runtime whether the request is read-only. A read-only session refuses every write and lock before it reaches the
|
|
124
|
+
* database, and reads in `READ ONLY` transactions.
|
|
125
|
+
*/
|
|
126
|
+
export function sessionOf(source: SessionSource): Session {
|
|
127
|
+
return openSession(source.database, source.readOnly?.() === true)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let executeFixed: (session: Session, compiled: CompiledQuery) => Promise<readonly unknown[]>
|
|
131
|
+
|
|
132
|
+
/** What a session reports to, and asks, the transaction it runs in. */
|
|
133
|
+
interface SessionHooks {
|
|
134
|
+
/** Hears of every statement the database refused: in a transaction, Postgres aborts the rest of it. */
|
|
135
|
+
readonly failed?: () => void
|
|
136
|
+
/** Whether the transaction is still the work's: a session kept past it runs nothing on a connection given back. */
|
|
137
|
+
readonly open?: () => boolean
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `work`, or the abort of `signal`, whichever comes first; a `work` left behind still has its rejection handled. */
|
|
141
|
+
function untilAborted<T>(work: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
142
|
+
if (signal === undefined) {
|
|
143
|
+
return work
|
|
144
|
+
}
|
|
145
|
+
work.then(undefined, () => undefined)
|
|
146
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
147
|
+
if (signal.aborted) {
|
|
148
|
+
reject(signal.reason)
|
|
149
|
+
}
|
|
150
|
+
signal.addEventListener('abort', () => reject(signal.reason), { once: true })
|
|
151
|
+
})
|
|
152
|
+
return Promise.race([work, aborted])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Runs guarded operation trees; the only path from the layer to the connection. A driver failure leaves it as a
|
|
157
|
+
* `DatabaseError`, which carries the SQLSTATE and nothing the driver said about the row.
|
|
158
|
+
*/
|
|
159
|
+
export class Session {
|
|
160
|
+
readonly #executor: Executor
|
|
161
|
+
readonly #kysely: Kysely<LooseDB> | undefined
|
|
162
|
+
readonly #readOnly: boolean
|
|
163
|
+
readonly #hooks: SessionHooks
|
|
164
|
+
|
|
165
|
+
constructor(executor: Executor, kysely: Kysely<LooseDB> | undefined, readOnly: boolean, hooks: SessionHooks = {}) {
|
|
166
|
+
this.#executor = executor
|
|
167
|
+
this.#kysely = kysely
|
|
168
|
+
this.#readOnly = readOnly
|
|
169
|
+
this.#hooks = hooks
|
|
170
|
+
Object.freeze(this)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static {
|
|
174
|
+
executeFixed = async (session, compiled) => (await session.#execute(compiled)).rows
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get readOnly(): boolean {
|
|
178
|
+
return this.#readOnly
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Fails the transaction this session runs in, so it commits nothing: a statement refused before it was sent. */
|
|
182
|
+
fail(): void {
|
|
183
|
+
this.#hooks.failed?.()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async #execute<O>(compiled: CompiledQuery<O>): Promise<{ readonly rows: O[] }> {
|
|
187
|
+
if (this.#hooks.open?.() === false) {
|
|
188
|
+
throw new QueryRefusedError('ended', 'the transaction of this session has ended; it runs nothing more')
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return await this.#executor.executeQuery(compiled)
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.#hooks.failed?.()
|
|
194
|
+
throw databaseError(error)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async run<O>(node: RootOperationNode, context: GuardContext): Promise<O[]> {
|
|
199
|
+
assertAllowedTree(node, context)
|
|
200
|
+
if (this.#readOnly && (context.root !== 'SelectQueryNode' || context.lock !== undefined)) {
|
|
201
|
+
throw new QueryRefusedError('read_only', 'this request is read-only: it reads, and no operation may write or lock a row')
|
|
202
|
+
}
|
|
203
|
+
// CompiledQuery's row type is a phantom: this is the one place where generated row types meet the wire.
|
|
204
|
+
const compiled: CompiledQuery<O> = compiler.compileQuery(node, createQueryId())
|
|
205
|
+
const kysely = this.#kysely
|
|
206
|
+
if (this.#readOnly && kysely !== undefined) {
|
|
207
|
+
try {
|
|
208
|
+
const result = await kysely.transaction().setAccessMode('read only').execute((transaction) => transaction.executeQuery(compiled))
|
|
209
|
+
return result.rows
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw databaseError(error)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const result = await this.#execute(compiled)
|
|
215
|
+
return result.rows
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Runs `work` in one transaction (`READ ONLY` in a read-only session); inside one, it runs in the transaction already
|
|
220
|
+
* open. When `signal` aborts, the transaction rolls back and its connection goes back to the pool at once, whether
|
|
221
|
+
* `work` returned or not; its session then runs nothing more.
|
|
222
|
+
*/
|
|
223
|
+
async transaction<T>(work: (session: Session) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
224
|
+
if (this.#kysely === undefined) {
|
|
225
|
+
return work(this)
|
|
226
|
+
}
|
|
227
|
+
const builder = this.#kysely.transaction()
|
|
228
|
+
let open = true
|
|
229
|
+
let failed = false
|
|
230
|
+
let failure: { readonly error: unknown } | undefined
|
|
231
|
+
try {
|
|
232
|
+
return await (this.#readOnly ? builder.setAccessMode('read only') : builder).execute(async (transaction) => {
|
|
233
|
+
try {
|
|
234
|
+
const hooks: SessionHooks = {
|
|
235
|
+
open: () => open,
|
|
236
|
+
failed: () => {
|
|
237
|
+
failed = true
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
const result = await untilAborted(work(new Session(transaction, undefined, this.#readOnly, hooks)), signal)
|
|
241
|
+
// A failure the work caught: Postgres would answer the COMMIT of an aborted transaction with a silent ROLLBACK.
|
|
242
|
+
if (failed) {
|
|
243
|
+
throw new QueryRefusedError('statement_failed', 'a statement of this transaction failed, so nothing it wrote commits')
|
|
244
|
+
}
|
|
245
|
+
return result
|
|
246
|
+
} catch (error) {
|
|
247
|
+
failure = { error }
|
|
248
|
+
throw error
|
|
249
|
+
} finally {
|
|
250
|
+
open = false
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// The work's own error passes through; only BEGIN, COMMIT and ROLLBACK failures are the driver's.
|
|
255
|
+
throw failure !== undefined && failure.error === error ? error : databaseError(error)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
Object.freeze(Session.prototype)
|
|
260
|
+
|
|
261
|
+
/** How long an invocation's transaction may sit idle before Postgres ends it, and its connection with it. */
|
|
262
|
+
export const INVOCATION_IDLE_TIMEOUT_MS = 30_000
|
|
263
|
+
|
|
264
|
+
const SQL_IDLE_TIMEOUT = "select set_config('idle_in_transaction_session_timeout', $1, true)"
|
|
265
|
+
|
|
266
|
+
/** What the invocation's transaction body throws to roll back; never seen outside this module. */
|
|
267
|
+
class RolledBack extends Error {}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The one transaction of an invocation (a request, a webhook delivery): opened by its first statement, then ended
|
|
271
|
+
* once by the framework — committed after the handler returned and its output passed validation, rolled back
|
|
272
|
+
* otherwise. Every handle of the invocation runs in `session()`. Ending it and giving its connection back are two
|
|
273
|
+
* states: once ended no statement runs in it, and the connection is back in the pool whether the COMMIT succeeded,
|
|
274
|
+
* failed, or the connection died.
|
|
275
|
+
*/
|
|
276
|
+
export class InvocationTransaction {
|
|
277
|
+
readonly #database: Database
|
|
278
|
+
readonly #readOnly: boolean
|
|
279
|
+
readonly #decision = Promise.withResolvers<'commit' | 'rollback'>()
|
|
280
|
+
#opened: Promise<Session> | undefined
|
|
281
|
+
#run: Promise<void> | undefined
|
|
282
|
+
#released: Promise<void> = Promise.resolve()
|
|
283
|
+
#isReleased = true
|
|
284
|
+
#ended = false
|
|
285
|
+
#failed = false
|
|
286
|
+
|
|
287
|
+
constructor(database: Database, readOnly: boolean) {
|
|
288
|
+
this.#database = database
|
|
289
|
+
this.#readOnly = readOnly
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A `READ ONLY` transaction: a query's or an export's. */
|
|
293
|
+
get readOnly(): boolean {
|
|
294
|
+
return this.#readOnly
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Whether the transaction was opened: an invocation that reached no data commits nothing. */
|
|
298
|
+
get opened(): boolean {
|
|
299
|
+
return this.#opened !== undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Marks it failed, so it commits nothing: a statement refused before it was sent, even when the handler caught it. */
|
|
303
|
+
fail(): void {
|
|
304
|
+
this.#failed = true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** No statement runs in it any more. */
|
|
308
|
+
get ended(): boolean {
|
|
309
|
+
return this.#ended
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Its connection is back in the pool (or it never took one). */
|
|
313
|
+
get released(): boolean {
|
|
314
|
+
return this.#isReleased
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The session the invocation's statements run in; the first call opens the transaction. */
|
|
318
|
+
session(): Promise<Session> {
|
|
319
|
+
if (this.#ended) {
|
|
320
|
+
return Promise.reject(new QueryRefusedError('ended', 'this invocation has ended; its data handles run only while its handler runs'))
|
|
321
|
+
}
|
|
322
|
+
this.#opened ??= this.#open()
|
|
323
|
+
return this.#opened
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#open(): Promise<Session> {
|
|
327
|
+
const ready = Promise.withResolvers<Session>()
|
|
328
|
+
this.#isReleased = false
|
|
329
|
+
const builder = transactionBuilder(this.#database)
|
|
330
|
+
const run = (this.#readOnly ? builder.setAccessMode('read only') : builder).execute(async (transaction) => {
|
|
331
|
+
const session = new Session(transaction, undefined, this.#readOnly, {
|
|
332
|
+
failed: () => {
|
|
333
|
+
this.#failed = true
|
|
334
|
+
},
|
|
335
|
+
open: () => !this.#ended,
|
|
336
|
+
})
|
|
337
|
+
await executeFixed(session, CompiledQuery.raw(SQL_IDLE_TIMEOUT, [String(INVOCATION_IDLE_TIMEOUT_MS)]))
|
|
338
|
+
ready.resolve(session)
|
|
339
|
+
if ((await this.#decision.promise) === 'rollback') {
|
|
340
|
+
throw new RolledBack()
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
this.#run = run
|
|
344
|
+
this.#released = run.then(() => undefined, () => undefined).finally(() => {
|
|
345
|
+
this.#isReleased = true
|
|
346
|
+
})
|
|
347
|
+
run.catch((error: unknown) => ready.reject(databaseError(error)))
|
|
348
|
+
return ready.promise
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Commits what the invocation wrote. A statement the database refused aborted the transaction, so it rolls back
|
|
353
|
+
* and throws instead: Postgres would answer the COMMIT with a silent ROLLBACK. A failed COMMIT throws a
|
|
354
|
+
* `DatabaseError`, after the connection is back in the pool.
|
|
355
|
+
*/
|
|
356
|
+
async commit(): Promise<void> {
|
|
357
|
+
if (this.#ended) {
|
|
358
|
+
throw new QueryRefusedError('ended', 'this invocation has already ended')
|
|
359
|
+
}
|
|
360
|
+
this.#ended = true
|
|
361
|
+
const run = this.#run
|
|
362
|
+
if (this.#failed) {
|
|
363
|
+
this.#decision.resolve('rollback')
|
|
364
|
+
await this.#released
|
|
365
|
+
throw new QueryRefusedError('statement_failed', 'a statement of this invocation failed, so nothing it wrote commits')
|
|
366
|
+
}
|
|
367
|
+
if (run === undefined) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
this.#decision.resolve('commit')
|
|
371
|
+
try {
|
|
372
|
+
await run
|
|
373
|
+
} catch (error) {
|
|
374
|
+
throw databaseError(error)
|
|
375
|
+
} finally {
|
|
376
|
+
await this.#released
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Discards what the invocation wrote; idempotent, never throws, and returns once the connection is back. */
|
|
381
|
+
async rollback(): Promise<void> {
|
|
382
|
+
this.#ended = true
|
|
383
|
+
this.#decision.resolve('rollback')
|
|
384
|
+
await this.#released
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
Object.freeze(InvocationTransaction.prototype)
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A scalar parameter of a framework statement; Bun.SQL mis-binds arrays and JSON strings. No Date: these texts skip
|
|
391
|
+
* the compiler that binds one, and a lease time is the database's clock, never the process's.
|
|
392
|
+
*/
|
|
393
|
+
export type FixedParameter = string | number | null
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* One of the framework's own fixed statements (`job-leases.ts`): a constant text with scalar parameters. They need
|
|
397
|
+
* what the guard refuses on purpose (`now()`, `set_config`, `pg_backend_pid()`, `on conflict`), so they bypass it;
|
|
398
|
+
* no subpath exports this, and no text is ever built from a caller's value.
|
|
399
|
+
*/
|
|
400
|
+
export function runFixedStatement(session: Session, text: string, parameters: readonly FixedParameter[]): Promise<readonly unknown[]> {
|
|
401
|
+
return executeFixed(session, CompiledQuery.raw(text, [...parameters]))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** A bound parameter of an application's own statement (`sql.ts`), after its value was encoded for the driver. */
|
|
405
|
+
export type SqlParameter = string | number | boolean | bigint | null
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* One `ctx.sql` statement, whose text `sql.ts` classified and whose values are bound parameters only. It runs through the
|
|
409
|
+
* session's one path to the connection, so a statement the database refuses fails the invocation it runs in.
|
|
410
|
+
*/
|
|
411
|
+
export function runApplicationStatement(session: Session, text: string, parameters: readonly SqlParameter[]): Promise<readonly unknown[]> {
|
|
412
|
+
return executeFixed(session, CompiledQuery.raw(text, [...parameters]))
|
|
413
|
+
}
|