@fabrika/platform-node 0.3.3 → 0.4.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.
@@ -56,31 +56,35 @@ export class PostgresStatement implements SqlStatement {
56
56
  }
57
57
 
58
58
  async first<T = Record<string, unknown>>(): Promise<T | null> {
59
- const rows = await this.rows<T>(this.sql)
59
+ const statement = this.bound()
60
+ const rows = await onReservedConnection(this.sql, (connection) => statement.rows<T>(connection))
60
61
  return rows[0] ?? null
61
62
  }
62
63
 
63
64
  async all<T = Record<string, unknown>>(): Promise<SqlQueryResult<T>> {
64
- return { results: await this.rows<T>(this.sql) }
65
+ const statement = this.bound()
66
+ return { results: await onReservedConnection(this.sql, (connection) => statement.rows<T>(connection)) }
65
67
  }
66
68
 
67
69
  async run(): Promise<SqlRunResult> {
68
- const result: unknown = await this.execute<Record<string, unknown>>(this.sql)
70
+ const statement = this.bound()
71
+ const result: unknown = await onReservedConnection(this.sql, (connection) => statement.rows<Record<string, unknown>>(connection))
69
72
  return { meta: { changes: changesOf(result) } }
70
73
  }
71
74
 
72
- /**
73
- * Run this statement on a specific handle — the pool, or the transaction-scoped handle `batch()`
74
- * opens. Internal to the package; the port only ever sees `first`/`all`/`run`.
75
- */
76
- async rows<T>(handle: Bun.SQL): Promise<T[]> {
77
- return this.execute<T>(handle)
78
- }
79
-
80
- private execute<T>(handle: Bun.SQL): Promise<T[]> {
75
+ /** This statement, once every placeholder has its value: a wrong count fails before any connection is taken. */
76
+ bound(): PostgresStatement {
81
77
  if (this.values.length !== this.placeholders) {
82
78
  throw new Error(`statement expects ${this.placeholders} bound value(s), got ${this.values.length}`)
83
79
  }
80
+ return this
81
+ }
82
+
83
+ /**
84
+ * Run this statement on a specific handle — a reserved connection of the pool, or the one `batch()` holds its
85
+ * transaction on. Internal to the package; the port only ever sees `first`/`all`/`run`.
86
+ */
87
+ rows<T>(handle: Bun.SQL): Promise<T[]> {
84
88
  return handle.unsafe<T[]>(this.text, [...this.values])
85
89
  }
86
90
  }
@@ -111,19 +115,33 @@ export class PostgresDatabase implements SqlDatabase {
111
115
  return new PostgresStatement(this.sql, rewritten.text, rewritten.count, [])
112
116
  }
113
117
 
114
- /** Run every statement in ONE transaction, in order. A throw rolls the whole batch back. */
118
+ /**
119
+ * Run every statement in ONE transaction, in order. A throw rolls the whole batch back. The transaction is sent by
120
+ * hand on the reserved connection: Bun 1.3.14's `begin()` on a reserved connection rejects a failed statement twice,
121
+ * once unhandled.
122
+ */
115
123
  async batch<T = Record<string, unknown>>(statements: SqlStatement[]): Promise<SqlQueryResult<T>[]> {
116
- const run = async (tx: Bun.SQL): Promise<SqlQueryResult<T>[]> => {
117
- const out: SqlQueryResult<T>[] = []
118
- for (const statement of statements) {
119
- if (!(statement instanceof PostgresStatement)) {
120
- throw new Error('batch() accepts only statements created by this database')
124
+ const own = statements.map((statement) => {
125
+ if (!(statement instanceof PostgresStatement)) {
126
+ throw new Error('batch() accepts only statements created by this database')
127
+ }
128
+ return statement.bound()
129
+ })
130
+ return onReservedConnection(this.sql, async (connection) => {
131
+ await connection.unsafe('BEGIN')
132
+ try {
133
+ const out: SqlQueryResult<T>[] = []
134
+ for (const statement of own) {
135
+ out.push({ results: await statement.rows<T>(connection) })
121
136
  }
122
- out.push({ results: await statement.rows<T>(tx) })
137
+ await connection.unsafe('COMMIT')
138
+ return out
139
+ } catch (error) {
140
+ // ROLLBACK fails only on a connection that is gone; the statement's own error is the one to report.
141
+ await connection.unsafe('ROLLBACK').catch(() => undefined)
142
+ throw error
123
143
  }
124
- return out
125
- }
126
- return this.sql.begin(run)
144
+ })
127
145
  }
128
146
 
129
147
  /** Close the pool. Waits for in-flight queries unless a timeout is given. */
@@ -132,6 +150,22 @@ export class PostgresDatabase implements SqlDatabase {
132
150
  }
133
151
  }
134
152
 
153
+ /**
154
+ * Runs `work` on a connection reserved from `pool` and releases it afterwards; every statement and every batch goes
155
+ * through here. Bun 1.3.14's pool sends an unreserved query onto a connection that holds another caller's
156
+ * transaction, where it waits for that transaction or runs inside it, and a burst of batches larger than the pool
157
+ * hangs. Once every use is reserved, the pool has no unreserved query to send.
158
+ */
159
+ async function onReservedConnection<T>(pool: Bun.SQL, work: (connection: Bun.SQL) => Promise<T>): Promise<T> {
160
+ const connection = await pool.reserve()
161
+ try {
162
+ return await work(connection)
163
+ } finally {
164
+ // Typed `void`, `release()` answers a promise that rejects once the pool has closed the connection.
165
+ Promise.resolve(connection.release()).catch(() => undefined)
166
+ }
167
+ }
168
+
135
169
  /** The rows-affected count D1 would report, or 0 for a statement that changes nothing. */
136
170
  function changesOf(result: unknown): number {
137
171
  const command: unknown = readProperty(result, 'command')
package/src/sql.ts ADDED
@@ -0,0 +1,52 @@
1
+ // @fabrika/platform-node/sql — the SQL lexer and the migration classifier, RUNTIME-NEUTRAL: no Bun, no Node, no
2
+ // driver, so a Cloudflare Worker (Control reading a commit) loads it as the runner does. `sql-subpath.test.ts` holds
3
+ // the import graph to that.
4
+
5
+ export { applicationDatabaseSchema } from './application-schema'
6
+ export {
7
+ FILE_SCAN_COLUMNS,
8
+ FILE_STATUSES,
9
+ FILE_TABLE_PREFIX,
10
+ fileScanAlter,
11
+ fileScanAlterTable,
12
+ fileScanIndex,
13
+ fileStatusCheck,
14
+ fileTableScanReady,
15
+ legacyStatusCheckName,
16
+ } from './file-table-ddl'
17
+ export {
18
+ classifyMigration,
19
+ type ClassifyMigrationOptions,
20
+ classifyMigrations,
21
+ type DatabaseObject,
22
+ type DatabaseObjectKind,
23
+ declaredFunctions,
24
+ type DeviationKind,
25
+ type ManifestChange,
26
+ type MigrationClassification,
27
+ type MigrationDeviation,
28
+ type MigrationFile,
29
+ migrationManifest,
30
+ type MigrationRefusal,
31
+ type MigrationSetClassification,
32
+ } from './migration-classify'
33
+ export { lexSql, type SqlToken, sqlTokens, type Statement, topLevelStatements, upperWord } from './migration-sql'
34
+ export { PG_CATALOG_FUNCTIONS, SQL_KEYWORDS } from './pg-catalog-names'
35
+ export {
36
+ ddlForeignSchema,
37
+ dmlForeignSchema,
38
+ dmlRelations,
39
+ frameworkReference,
40
+ type FunctionUse,
41
+ functionUse,
42
+ isFrameworkName,
43
+ type NameReference,
44
+ nameReferences,
45
+ type NameRole,
46
+ nameText,
47
+ nestedStatement,
48
+ parenthesizedTarget,
49
+ refusedFunctionCall,
50
+ statementKind,
51
+ unicodeIdentifier,
52
+ } from './sql-names'