@dotdo/postgres 0.1.1 → 0.1.3

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.
Files changed (111) hide show
  1. package/README.md +73 -1
  2. package/dist/client/index.d.ts +47 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +47 -0
  5. package/dist/client/index.js.map +1 -0
  6. package/dist/client/postgres-client.d.ts +273 -0
  7. package/dist/client/postgres-client.d.ts.map +1 -0
  8. package/dist/client/postgres-client.js +389 -0
  9. package/dist/client/postgres-client.js.map +1 -0
  10. package/dist/client/types.d.ts +167 -0
  11. package/dist/client/types.d.ts.map +1 -0
  12. package/dist/client/types.js +7 -0
  13. package/dist/client/types.js.map +1 -0
  14. package/dist/do/index.d.ts +18 -0
  15. package/dist/do/index.d.ts.map +1 -0
  16. package/dist/do/index.js +18 -0
  17. package/dist/do/index.js.map +1 -0
  18. package/dist/do/postgres.d.ts +110 -0
  19. package/dist/do/postgres.d.ts.map +1 -0
  20. package/dist/do/postgres.js +266 -0
  21. package/dist/do/postgres.js.map +1 -0
  22. package/dist/do/sql.d.ts +92 -0
  23. package/dist/do/sql.d.ts.map +1 -0
  24. package/dist/do/sql.js +204 -0
  25. package/dist/do/sql.js.map +1 -0
  26. package/dist/index.d.ts +25 -30
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +29 -30
  29. package/dist/index.js.map +1 -1
  30. package/dist/mcp/binding.d.ts +47 -0
  31. package/dist/mcp/binding.d.ts.map +1 -0
  32. package/dist/mcp/binding.js +183 -0
  33. package/dist/mcp/binding.js.map +1 -0
  34. package/dist/mcp/index.d.ts +92 -0
  35. package/dist/mcp/index.d.ts.map +1 -0
  36. package/dist/mcp/index.js +91 -0
  37. package/dist/mcp/index.js.map +1 -0
  38. package/dist/mcp/server.d.ts +62 -0
  39. package/dist/mcp/server.d.ts.map +1 -0
  40. package/dist/mcp/server.js +278 -0
  41. package/dist/mcp/server.js.map +1 -0
  42. package/dist/mcp/tools.d.ts +58 -0
  43. package/dist/mcp/tools.d.ts.map +1 -0
  44. package/dist/mcp/tools.js +356 -0
  45. package/dist/mcp/tools.js.map +1 -0
  46. package/dist/mcp/types.d.ts +139 -0
  47. package/dist/mcp/types.d.ts.map +1 -0
  48. package/dist/mcp/types.js +7 -0
  49. package/dist/mcp/types.js.map +1 -0
  50. package/dist/pglite/workers-pglite.d.ts +13 -4
  51. package/dist/pglite/workers-pglite.d.ts.map +1 -1
  52. package/dist/pglite/workers-pglite.js +110 -5
  53. package/dist/pglite/workers-pglite.js.map +1 -1
  54. package/dist/pglite-assets/pglite.data +0 -0
  55. package/dist/pglite-assets/pglite.wasm +0 -0
  56. package/dist/worker/auth.d.ts.map +1 -1
  57. package/dist/worker/auth.js +16 -6
  58. package/dist/worker/auth.js.map +1 -1
  59. package/dist/worker/background-pglite-manager.d.ts +243 -0
  60. package/dist/worker/background-pglite-manager.d.ts.map +1 -0
  61. package/dist/worker/background-pglite-manager.js +528 -0
  62. package/dist/worker/background-pglite-manager.js.map +1 -0
  63. package/dist/worker/do-pglite-manager.d.ts +77 -0
  64. package/dist/worker/do-pglite-manager.d.ts.map +1 -1
  65. package/dist/worker/do-pglite-manager.js +189 -12
  66. package/dist/worker/do-pglite-manager.js.map +1 -1
  67. package/dist/worker/entry.d.ts.map +1 -1
  68. package/dist/worker/entry.js +108 -26
  69. package/dist/worker/entry.js.map +1 -1
  70. package/dist/worker/index.d.ts +7 -1
  71. package/dist/worker/index.d.ts.map +1 -1
  72. package/dist/worker/index.js +19 -1
  73. package/dist/worker/index.js.map +1 -1
  74. package/dist/worker/lazy-pglite-manager.d.ts +242 -0
  75. package/dist/worker/lazy-pglite-manager.d.ts.map +1 -0
  76. package/dist/worker/lazy-pglite-manager.js +463 -0
  77. package/dist/worker/lazy-pglite-manager.js.map +1 -0
  78. package/package.json +20 -6
  79. package/src/client/index.ts +61 -0
  80. package/src/client/postgres-client.ts +442 -0
  81. package/src/client/types.ts +211 -0
  82. package/src/do/index.ts +18 -0
  83. package/src/do/postgres.ts +367 -0
  84. package/src/do/sql.ts +280 -0
  85. package/src/index.ts +50 -30
  86. package/src/mcp/binding.ts +236 -0
  87. package/src/mcp/index.ts +122 -0
  88. package/src/mcp/server.ts +361 -0
  89. package/src/mcp/tools.ts +464 -0
  90. package/src/mcp/types.ts +148 -0
  91. package/src/pglite/workers-pglite.ts +141 -12
  92. package/src/pglite-assets/pglite.data +0 -0
  93. package/src/pglite-assets/pglite.wasm +0 -0
  94. package/src/worker/auth.ts +17 -6
  95. package/src/worker/background-pglite-manager.ts +680 -0
  96. package/src/worker/do-pglite-manager.ts +235 -19
  97. package/src/worker/entry.ts +112 -30
  98. package/src/worker/index.ts +71 -1
  99. package/src/worker/lazy-pglite-manager.ts +595 -0
  100. package/dist/iceberg/duckdb-wasm.d.ts +0 -447
  101. package/dist/iceberg/duckdb-wasm.d.ts.map +0 -1
  102. package/dist/iceberg/duckdb-wasm.js +0 -600
  103. package/dist/iceberg/duckdb-wasm.js.map +0 -1
  104. package/dist/iceberg/test-fixtures.d.ts +0 -151
  105. package/dist/iceberg/test-fixtures.d.ts.map +0 -1
  106. package/dist/iceberg/test-fixtures.js +0 -446
  107. package/dist/iceberg/test-fixtures.js.map +0 -1
  108. package/dist/worker/__mocks__/cloudflare-workers.d.ts +0 -31
  109. package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +0 -1
  110. package/dist/worker/__mocks__/cloudflare-workers.js +0 -33
  111. package/dist/worker/__mocks__/cloudflare-workers.js.map +0 -1
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Type definitions for the PostgresDO WebSocket client
3
+ *
4
+ * @module client/types
5
+ */
6
+
7
+ import type { QueryResultRow, QueryField } from '../worker/types'
8
+
9
+ /**
10
+ * Query result from PostgresDO RPC
11
+ */
12
+ export interface RpcQueryResult<T extends QueryResultRow = QueryResultRow> {
13
+ rows: T[]
14
+ fields: QueryField[]
15
+ rowCount: number
16
+ durationMs: number
17
+ }
18
+
19
+ /**
20
+ * Batch query input
21
+ */
22
+ export interface RpcBatchQuery {
23
+ sql: string
24
+ params?: unknown[]
25
+ }
26
+
27
+ /**
28
+ * Batch result from PostgresDO RPC
29
+ */
30
+ export interface RpcBatchResult {
31
+ results: RpcQueryResult[]
32
+ durationMs: number
33
+ }
34
+
35
+ /**
36
+ * Transaction options for RPC
37
+ */
38
+ export interface RpcTransactionOptions {
39
+ isolationLevel?: 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'
40
+ readOnly?: boolean
41
+ }
42
+
43
+ /**
44
+ * Table column information
45
+ */
46
+ export interface ColumnInfo {
47
+ column_name: string
48
+ data_type: string
49
+ is_nullable: boolean
50
+ column_default: string | null
51
+ }
52
+
53
+ /**
54
+ * Database statistics
55
+ */
56
+ export interface DatabaseStats {
57
+ queryCount: number
58
+ totalDurationMs: number
59
+ avgDurationMs: number
60
+ lastQueryAt: Date | null
61
+ uptime: number
62
+ shutdownStatus: 'running' | 'draining' | 'shutdown'
63
+ }
64
+
65
+ /**
66
+ * PostgresDO RPC API interface
67
+ *
68
+ * This interface defines all the RPC methods available on PostgresDO.
69
+ * Used for type-safe client generation with rpc.do.
70
+ */
71
+ export interface PostgresDORpcApi {
72
+ // Query methods
73
+ rpcQuery<T extends QueryResultRow = QueryResultRow>(
74
+ sql: string,
75
+ params?: unknown[]
76
+ ): Promise<RpcQueryResult<T>>
77
+
78
+ rpcQueryOne<T extends QueryResultRow = QueryResultRow>(
79
+ sql: string,
80
+ params?: unknown[]
81
+ ): Promise<T | null>
82
+
83
+ rpcQueryScalar<T = unknown>(sql: string, params?: unknown[]): Promise<T | null>
84
+
85
+ rpcExecute(sql: string, params?: unknown[]): Promise<{ rowCount: number; durationMs: number }>
86
+
87
+ // Batch methods
88
+ rpcBatch(queries: RpcBatchQuery[]): Promise<RpcBatchResult>
89
+
90
+ rpcBatchTransaction(queries: RpcBatchQuery[]): Promise<RpcBatchResult>
91
+
92
+ // Transaction methods
93
+ rpcTransaction(options?: RpcTransactionOptions): Promise<TransactionApi>
94
+
95
+ // Utility methods
96
+ rpcPing(): Promise<{ ok: true; durationMs: number }>
97
+
98
+ rpcVersion(): Promise<string>
99
+
100
+ rpcListTables(): Promise<string[]>
101
+
102
+ rpcDescribeTable(tableName: string): Promise<ColumnInfo[]>
103
+
104
+ rpcGetStats(): Promise<DatabaseStats>
105
+ }
106
+
107
+ /**
108
+ * Transaction API returned by rpcTransaction
109
+ */
110
+ export interface TransactionApi {
111
+ query<T extends QueryResultRow = QueryResultRow>(
112
+ sql: string,
113
+ params?: unknown[]
114
+ ): Promise<RpcQueryResult<T>>
115
+
116
+ queryOne<T extends QueryResultRow = QueryResultRow>(
117
+ sql: string,
118
+ params?: unknown[]
119
+ ): Promise<T | null>
120
+
121
+ execute(sql: string, params?: unknown[]): Promise<{ rowCount: number; durationMs: number }>
122
+
123
+ commit(): Promise<void>
124
+
125
+ rollback(): Promise<void>
126
+ }
127
+
128
+ /**
129
+ * Client configuration options
130
+ */
131
+ export interface PostgresClientConfig {
132
+ /**
133
+ * WebSocket URL for the PostgresDO endpoint
134
+ * @example 'wss://postgres.example.com/ws'
135
+ */
136
+ url: string
137
+
138
+ /**
139
+ * Authentication token or provider function
140
+ * Can be a static token string or an async function that returns a token
141
+ */
142
+ auth?: string | (() => string | null | Promise<string | null>)
143
+
144
+ /**
145
+ * Enable automatic reconnection on disconnect
146
+ * @default true
147
+ */
148
+ autoReconnect?: boolean
149
+
150
+ /**
151
+ * Maximum number of reconnection attempts
152
+ * @default Infinity
153
+ */
154
+ maxReconnectAttempts?: number
155
+
156
+ /**
157
+ * Initial backoff delay in ms for reconnection
158
+ * @default 1000
159
+ */
160
+ reconnectBackoff?: number
161
+
162
+ /**
163
+ * Maximum backoff delay in ms for reconnection
164
+ * @default 30000
165
+ */
166
+ maxReconnectBackoff?: number
167
+
168
+ /**
169
+ * Heartbeat interval in ms (0 to disable)
170
+ * @default 30000
171
+ */
172
+ heartbeatInterval?: number
173
+
174
+ /**
175
+ * Callback when connection is established
176
+ */
177
+ onConnect?: () => void
178
+
179
+ /**
180
+ * Callback when connection is lost
181
+ */
182
+ onDisconnect?: (reason: string) => void
183
+
184
+ /**
185
+ * Callback when attempting to reconnect
186
+ */
187
+ onReconnecting?: (attempt: number, maxAttempts: number) => void
188
+
189
+ /**
190
+ * Callback on any error
191
+ */
192
+ onError?: (error: Error) => void
193
+
194
+ /**
195
+ * Allow auth over insecure ws:// connections
196
+ * WARNING: Only for local development
197
+ * @default false
198
+ */
199
+ allowInsecureAuth?: boolean
200
+
201
+ /**
202
+ * Enable debug logging
203
+ * @default false
204
+ */
205
+ debug?: boolean
206
+ }
207
+
208
+ /**
209
+ * Connection state for the client
210
+ */
211
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed'
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Postgres Durable Object Module
3
+ *
4
+ * Minimal API for PostgreSQL in Cloudflare Workers.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { sql, db, Postgres } from '@dotdo/postgres'
9
+ * export { Postgres }
10
+ *
11
+ * export default {
12
+ * fetch: async () => Response.json(await sql`SELECT * FROM posts`)
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ export { Postgres, type PostgresEnv, type QueryResult } from './postgres'
18
+ export { sql, db, createSql, createDb, type SqlConfig, type SqlResult } from './sql'
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Postgres Durable Object
3
+ *
4
+ * A PostgreSQL database running in a Cloudflare Durable Object with:
5
+ * - DO SQLite for durable persistence (survives evictions)
6
+ * - PGLite for full PostgreSQL features (arrays, JSON, window functions, etc.)
7
+ * - Automatic sync between the two layers
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { sql, Postgres } from '@dotdo/postgres'
12
+ * export { Postgres }
13
+ *
14
+ * export default {
15
+ * fetch: () => Response.json(await sql`SELECT * FROM posts`)
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import { PGlite } from '@dotdo/pglite'
21
+
22
+ // Types for DO SQLite (Cloudflare's built-in SQL storage)
23
+ interface SqlStorage {
24
+ exec(query: string, ...bindings: unknown[]): SqlStorageCursor
25
+ }
26
+
27
+ interface SqlStorageCursor {
28
+ [Symbol.iterator](): Iterator<Record<string, SqlStorageValue>>
29
+ toArray(): Record<string, SqlStorageValue>[]
30
+ one(): Record<string, SqlStorageValue> | null
31
+ raw<T>(): T[][]
32
+ columnNames: string[]
33
+ rowsRead: number
34
+ rowsWritten: number
35
+ }
36
+
37
+ type SqlStorageValue = string | number | null | ArrayBuffer
38
+
39
+ interface DurableObjectState {
40
+ storage: {
41
+ sql: SqlStorage
42
+ get<T>(key: string): Promise<T | undefined>
43
+ put<T>(key: string, value: T): Promise<void>
44
+ delete(key: string): Promise<boolean>
45
+ }
46
+ waitUntil(promise: Promise<unknown>): void
47
+ id: { toString(): string }
48
+ }
49
+
50
+ export interface PostgresEnv {
51
+ [key: string]: unknown
52
+ }
53
+
54
+ export interface QueryResult<T = Record<string, unknown>> {
55
+ rows: T[]
56
+ rowCount: number
57
+ fields?: { name: string; dataTypeID: number }[]
58
+ }
59
+
60
+
61
+ /**
62
+ * Postgres Durable Object
63
+ *
64
+ * Provides a full PostgreSQL database in each Durable Object instance.
65
+ * Data persists in DO SQLite and is loaded into PGLite on initialization.
66
+ */
67
+ export class Postgres implements DurableObject {
68
+ private pglite: PGlite | null = null
69
+ private initPromise: Promise<void> | null = null
70
+ private initialized = false
71
+ private sql: SqlStorage
72
+
73
+ constructor(
74
+ private state: DurableObjectState,
75
+ _env: PostgresEnv
76
+ ) {
77
+ this.sql = state.storage.sql
78
+ this.initStorage()
79
+ }
80
+
81
+ /**
82
+ * Initialize DO SQLite schema for persistence
83
+ */
84
+ private initStorage(): void {
85
+ // Core metadata table
86
+ this.sql.exec(`
87
+ CREATE TABLE IF NOT EXISTS __pg_metadata (
88
+ key TEXT PRIMARY KEY,
89
+ value TEXT NOT NULL,
90
+ updated_at TEXT DEFAULT (datetime('now'))
91
+ )
92
+ `)
93
+
94
+ // Table registry - tracks all tables and their schemas
95
+ this.sql.exec(`
96
+ CREATE TABLE IF NOT EXISTS __pg_tables (
97
+ name TEXT PRIMARY KEY,
98
+ schema TEXT NOT NULL,
99
+ created_at TEXT DEFAULT (datetime('now'))
100
+ )
101
+ `)
102
+
103
+ // Generic data storage - stores all table data as JSON
104
+ this.sql.exec(`
105
+ CREATE TABLE IF NOT EXISTS __pg_data (
106
+ table_name TEXT NOT NULL,
107
+ row_id TEXT NOT NULL,
108
+ data TEXT NOT NULL,
109
+ created_at TEXT DEFAULT (datetime('now')),
110
+ updated_at TEXT DEFAULT (datetime('now')),
111
+ PRIMARY KEY (table_name, row_id)
112
+ )
113
+ `)
114
+
115
+ // Create index for faster queries
116
+ this.sql.exec(`
117
+ CREATE INDEX IF NOT EXISTS idx_pg_data_table ON __pg_data(table_name)
118
+ `)
119
+ }
120
+
121
+ /**
122
+ * Initialize PGLite and restore data from DO SQLite
123
+ */
124
+ private async init(): Promise<void> {
125
+ if (this.initialized && this.pglite) {
126
+ return
127
+ }
128
+
129
+ if (this.initPromise) {
130
+ return this.initPromise
131
+ }
132
+
133
+ this.initPromise = (async () => {
134
+ // Initialize PGLite (in-memory PostgreSQL)
135
+ this.pglite = new PGlite()
136
+ await this.pglite.waitReady
137
+
138
+ // Restore tables and data from DO SQLite
139
+ await this.restoreFromStorage()
140
+
141
+ this.initialized = true
142
+ })()
143
+
144
+ return this.initPromise
145
+ }
146
+
147
+ /**
148
+ * Restore all tables and data from DO SQLite into PGLite
149
+ */
150
+ private async restoreFromStorage(): Promise<void> {
151
+ if (!this.pglite) return
152
+
153
+ // Get all registered tables
154
+ const tables = [...this.sql.exec('SELECT name, schema FROM __pg_tables')]
155
+
156
+ for (const table of tables) {
157
+ const tableName = table.name as string
158
+ const schema = table.schema as string
159
+
160
+ // Create table in PGLite
161
+ try {
162
+ await this.pglite.exec(schema)
163
+ } catch (e) {
164
+ // Table might already exist, ignore
165
+ }
166
+
167
+ // Restore data for this table
168
+ const rows = [...this.sql.exec(
169
+ 'SELECT row_id, data FROM __pg_data WHERE table_name = ?',
170
+ tableName
171
+ )]
172
+
173
+ for (const row of rows) {
174
+ const data = JSON.parse(row.data as string)
175
+ const columns = Object.keys(data)
176
+ const values = columns.map(c => data[c])
177
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ')
178
+
179
+ try {
180
+ await this.pglite.query(
181
+ `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) ON CONFLICT DO NOTHING`,
182
+ values
183
+ )
184
+ } catch (e) {
185
+ // Row might already exist or schema mismatch, continue
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Register a table schema for persistence
193
+ */
194
+ private registerTable(tableName: string, createStatement: string): void {
195
+ this.sql.exec(
196
+ 'INSERT OR REPLACE INTO __pg_tables (name, schema) VALUES (?, ?)',
197
+ tableName,
198
+ createStatement
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Persist a row to DO SQLite
204
+ */
205
+ private persistRow(tableName: string, rowId: string, data: Record<string, unknown>): void {
206
+ this.sql.exec(
207
+ `INSERT OR REPLACE INTO __pg_data (table_name, row_id, data, updated_at)
208
+ VALUES (?, ?, ?, datetime('now'))`,
209
+ tableName,
210
+ rowId,
211
+ JSON.stringify(data)
212
+ )
213
+ }
214
+
215
+ /**
216
+ * Delete a row from DO SQLite
217
+ */
218
+ private deleteRowFromStorage(tableName: string, rowId: string): void {
219
+ this.sql.exec(
220
+ 'DELETE FROM __pg_data WHERE table_name = ? AND row_id = ?',
221
+ tableName,
222
+ rowId
223
+ )
224
+ }
225
+
226
+ /**
227
+ * Delete all rows from a table in DO SQLite
228
+ */
229
+ private deleteAllRowsFromStorage(tableName: string): void {
230
+ this.sql.exec(
231
+ 'DELETE FROM __pg_data WHERE table_name = ?',
232
+ tableName
233
+ )
234
+ }
235
+
236
+ /**
237
+ * Execute a SQL query
238
+ */
239
+ async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>> {
240
+ await this.init()
241
+
242
+ if (!this.pglite) {
243
+ throw new Error('PGLite not initialized')
244
+ }
245
+
246
+ // Detect and handle DDL statements
247
+ const upperSql = sql.trim().toUpperCase()
248
+
249
+ if (upperSql.startsWith('CREATE TABLE')) {
250
+ // Extract table name and register for persistence
251
+ const match = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i)
252
+ if (match) {
253
+ this.registerTable(match[1], sql)
254
+ }
255
+ }
256
+
257
+ // Execute on PGLite
258
+ const result = await this.pglite.query<T>(sql, params)
259
+
260
+ // Handle INSERT/UPDATE/DELETE - persist changes
261
+ if (upperSql.startsWith('INSERT') || upperSql.startsWith('UPDATE') || upperSql.startsWith('DELETE')) {
262
+ // Extract table name
263
+ let tableName: string | null = null
264
+
265
+ if (upperSql.startsWith('INSERT')) {
266
+ const match = sql.match(/INSERT\s+INTO\s+["']?(\w+)["']?/i)
267
+ tableName = match?.[1] || null
268
+ } else if (upperSql.startsWith('UPDATE')) {
269
+ const match = sql.match(/UPDATE\s+["']?(\w+)["']?/i)
270
+ tableName = match?.[1] || null
271
+ } else if (upperSql.startsWith('DELETE')) {
272
+ const match = sql.match(/DELETE\s+FROM\s+["']?(\w+)["']?/i)
273
+ tableName = match?.[1] || null
274
+ }
275
+
276
+ // For RETURNING clauses, persist the returned rows (INSERT/UPDATE)
277
+ if (tableName && (upperSql.startsWith('INSERT') || upperSql.startsWith('UPDATE'))) {
278
+ if (result.rows && result.rows.length > 0) {
279
+ for (const row of result.rows as Record<string, unknown>[]) {
280
+ const rowId = (row.id as string) || (row._id as string) || JSON.stringify(row)
281
+ this.persistRow(tableName, rowId, row)
282
+ }
283
+ }
284
+ }
285
+
286
+ // For DELETE with RETURNING, remove the deleted rows
287
+ if (tableName && upperSql.startsWith('DELETE')) {
288
+ if (result.rows && result.rows.length > 0) {
289
+ for (const row of result.rows as Record<string, unknown>[]) {
290
+ const rowId = (row.id as string) || (row._id as string) || JSON.stringify(row)
291
+ this.deleteRowFromStorage(tableName, rowId)
292
+ }
293
+ } else {
294
+ // DELETE without RETURNING - sync entire table
295
+ // This is expensive but ensures consistency
296
+ this.deleteAllRowsFromStorage(tableName)
297
+ // Re-persist all remaining rows
298
+ const allRows = await this.pglite!.query(`SELECT * FROM ${tableName}`)
299
+ for (const row of allRows.rows as Record<string, unknown>[]) {
300
+ const rowId = (row.id as string) || (row._id as string) || JSON.stringify(row)
301
+ this.persistRow(tableName, rowId, row)
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ return {
308
+ rows: result.rows as T[],
309
+ rowCount: result.rows?.length || 0,
310
+ fields: result.fields?.map(f => ({ name: f.name, dataTypeID: f.dataTypeID })),
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Execute SQL without returning results
316
+ */
317
+ async exec(sql: string): Promise<void> {
318
+ await this.query(sql)
319
+ }
320
+
321
+ /**
322
+ * Handle HTTP requests to the Durable Object
323
+ */
324
+ async fetch(request: Request): Promise<Response> {
325
+ try {
326
+ await this.init()
327
+
328
+ const url = new URL(request.url)
329
+
330
+ // POST /query - Execute SQL
331
+ if (url.pathname === '/query' && request.method === 'POST') {
332
+ const body = await request.json() as { sql: string; params?: unknown[] }
333
+
334
+ if (!body.sql) {
335
+ return Response.json({ error: 'Missing sql' }, { status: 400 })
336
+ }
337
+
338
+ const result = await this.query(body.sql, body.params)
339
+ return Response.json(result)
340
+ }
341
+
342
+ // GET /health - Health check
343
+ if (url.pathname === '/health') {
344
+ return Response.json({
345
+ ok: true,
346
+ initialized: this.initialized,
347
+ id: this.state.id.toString(),
348
+ })
349
+ }
350
+
351
+ // GET /tables - List tables
352
+ if (url.pathname === '/tables') {
353
+ const tables = [...this.sql.exec('SELECT name FROM __pg_tables')]
354
+ return Response.json({ tables: tables.map(t => t.name) })
355
+ }
356
+
357
+ return Response.json({ error: 'Not found' }, { status: 404 })
358
+ } catch (error) {
359
+ return Response.json(
360
+ { error: error instanceof Error ? error.message : 'Unknown error' },
361
+ { status: 500 }
362
+ )
363
+ }
364
+ }
365
+ }
366
+
367
+ export default Postgres