@fabrika/platform-node 0.9.0-dev.2 → 0.9.0-dev.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/platform-node",
3
- "version": "0.9.0-dev.2",
3
+ "version": "0.9.0-dev.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,6 +38,6 @@
38
38
  "test": "bun test"
39
39
  },
40
40
  "dependencies": {
41
- "@fabrika/platform": "0.9.0-dev.2"
41
+ "@fabrika/platform": "0.9.0-dev.4"
42
42
  }
43
43
  }
@@ -26,8 +26,11 @@
26
26
  // already coerce `undefined` → null, so this matches what the services are tested against.
27
27
  // * SQLite-only SQL still fails, obviously: `unixepoch()` does not exist in Postgres. Several
28
28
  // statements in `@fabrika/control` and `@fabrika/iam` use it today.
29
- // * Prepared statements are cached per connection by default. Behind a transaction-mode pooler
30
- // (pgbouncer) construct the client with `{ prepare: false }`.
29
+ // * NO NAMED PREPARED STATEMENTS. A Zerops PostgreSQL service hands one server session to the IAM, Control and
30
+ // Operations processes in turn, so a statement prepared before a migration's ADD COLUMN outlives the
31
+ // process that prepared it, and `SELECT *` then fails with `cached plan must not change result type`
32
+ // (0A000). The same happens to one client that runs a migration between two reads. `connect` therefore
33
+ // defaults to `{ prepare: false }`: every statement is parsed where it runs.
31
34
 
32
35
  import type { SqlDatabase, SqlQueryResult, SqlRunResult, SqlStatement } from '@fabrika/platform'
33
36
  import { rewritePlaceholders, type RewrittenSql } from './placeholders'
@@ -36,7 +39,7 @@ import { rewritePlaceholders, type RewrittenSql } from './placeholders'
36
39
  const MUTATING_COMMANDS = new Set(['INSERT', 'UPDATE', 'DELETE', 'MERGE'])
37
40
 
38
41
  /** What a bound value may be. Anything else is a programming error and throws at bind time, as D1 does. */
39
- export type SqlBindValue = string | number | bigint | boolean | Date | Uint8Array | null
42
+ export type SqlBindValue = string | number | bigint | boolean | Uint8Array | null
40
43
 
41
44
  /**
42
45
  * One prepared statement. IMMUTABLE: `bind()` returns a NEW statement carrying the values and leaves
@@ -99,11 +102,11 @@ export class PostgresDatabase implements SqlDatabase {
99
102
  constructor(private readonly sql: Bun.SQL) {}
100
103
 
101
104
  /**
102
- * Open a pooled client. The URL is a CREDENTIAL — it is never logged, and never included in an
103
- * error message raised here.
105
+ * Open a pooled client that prepares no named statement (see the header). The URL is a CREDENTIAL — it is
106
+ * never logged, and never included in an error message raised here.
104
107
  */
105
- static connect(url: string, options: Omit<Bun.SQL.PostgresOrMySQLOptions, 'url' | 'adapter'> = {}): PostgresDatabase {
106
- return new PostgresDatabase(new Bun.SQL({ ...options, url, adapter: 'postgres' }))
108
+ static connect(url: string, options: Omit<Bun.SQL.PostgresOrMySQLOptions, 'url' | 'adapter' | 'prepare'> = {}): PostgresDatabase {
109
+ return new PostgresDatabase(new Bun.SQL({ ...options, prepare: false, url, adapter: 'postgres' }))
107
110
  }
108
111
 
109
112
  prepare(sql: string): SqlStatement {
@@ -198,11 +201,14 @@ function toBindValue(value: unknown): SqlBindValue {
198
201
  || typeof value === 'number'
199
202
  || typeof value === 'bigint'
200
203
  || typeof value === 'boolean'
201
- || value instanceof Date
202
204
  || value instanceof Uint8Array
203
205
  ) {
204
206
  return value
205
207
  }
208
+ // Without a prepared statement Bun sends a parameter as text, and a Date as `Date#toString()`, which Postgres refuses.
209
+ if (value instanceof Date) {
210
+ return value.toISOString()
211
+ }
206
212
  if (value instanceof ArrayBuffer) {
207
213
  return new Uint8Array(value)
208
214
  }
@@ -1,6 +1,6 @@
1
1
  import type { SqlDatabase, SqlQueryResult, SqlRunResult, SqlStatement } from '@fabrika/platform';
2
2
  /** What a bound value may be. Anything else is a programming error and throws at bind time, as D1 does. */
3
- export type SqlBindValue = string | number | bigint | boolean | Date | Uint8Array | null;
3
+ export type SqlBindValue = string | number | bigint | boolean | Uint8Array | null;
4
4
  /**
5
5
  * One prepared statement. IMMUTABLE: `bind()` returns a NEW statement carrying the values and leaves
6
6
  * the receiver untouched, which is D1's contract and what call sites rely on when they prepare once
@@ -33,10 +33,10 @@ export declare class PostgresDatabase implements SqlDatabase {
33
33
  private readonly rewrites;
34
34
  constructor(sql: Bun.SQL);
35
35
  /**
36
- * Open a pooled client. The URL is a CREDENTIAL — it is never logged, and never included in an
37
- * error message raised here.
36
+ * Open a pooled client that prepares no named statement (see the header). The URL is a CREDENTIAL — it is
37
+ * never logged, and never included in an error message raised here.
38
38
  */
39
- static connect(url: string, options?: Omit<Bun.SQL.PostgresOrMySQLOptions, 'url' | 'adapter'>): PostgresDatabase;
39
+ static connect(url: string, options?: Omit<Bun.SQL.PostgresOrMySQLOptions, 'url' | 'adapter' | 'prepare'>): PostgresDatabase;
40
40
  prepare(sql: string): SqlStatement;
41
41
  /**
42
42
  * Run every statement in ONE transaction, in order. A throw rolls the whole batch back. The transaction is sent by