@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
@@ -4,6 +4,18 @@
4
4
  * Handles PGLite instance creation, initialization, and lifecycle.
5
5
  * Extracted from PostgresDO to reduce class size and separate concerns.
6
6
  *
7
+ * ## WASM Hoisting Pattern
8
+ *
9
+ * This module implements WASM hoisting to reduce "warm start" time from ~1200ms to ~30ms.
10
+ *
11
+ * The problem: In Cloudflare Workers, when a DO class is recreated (but the isolate stays alive),
12
+ * the PGLite WASM needs to be reloaded. WASM initialization takes ~1200ms.
13
+ *
14
+ * The solution: Cache the PGLite instance at module level. The module loads once per isolate,
15
+ * so the PGLite instance survives DO class reinstantiation. This means:
16
+ * - Cold start: ~1200ms (full WASM initialization)
17
+ * - Warm start (isolate alive, DO recreated): ~30ms (reuse cached WASM)
18
+ *
7
19
  * @module worker/do-pglite-manager
8
20
  */
9
21
 
@@ -16,7 +28,131 @@ import pgliteWasm from '../pglite-assets/pglite.wasm'
16
28
  import pgliteData from '../pglite-assets/pglite.data'
17
29
 
18
30
  // Import Workers-compatible PGLite wrapper
19
- import { createWorkersPGLite } from '../pglite/workers-pglite'
31
+ import { createWorkersPGLite, type WorkersPGLite } from '../pglite/workers-pglite'
32
+
33
+ // =============================================================================
34
+ // Module-Level WASM Hoisting (outside class)
35
+ // =============================================================================
36
+ // These persist as long as the isolate lives - helps reduce warm start time.
37
+ // The module loads once per isolate, so the PGLite instance survives
38
+ // across DO class reinstantiations within the same isolate.
39
+
40
+ /**
41
+ * Hoisted PGLite instance - survives DO class reinstantiation within same isolate.
42
+ * This is the key to reducing warm start time from ~1200ms to ~30ms.
43
+ */
44
+ let hoistedPglite: WorkersPGLite | null = null
45
+
46
+ /**
47
+ * Promise for in-progress PGLite initialization.
48
+ * Prevents duplicate WASM loading when multiple DO instances initialize concurrently.
49
+ */
50
+ let hoistedPglitePromise: Promise<WorkersPGLite> | null = null
51
+
52
+ /**
53
+ * Module load time for diagnostics.
54
+ * Note: Date.now() at module evaluation time in Workers returns 0,
55
+ * so we capture the first access time instead.
56
+ */
57
+ let MODULE_LOAD_TIME: number | null = null
58
+
59
+ /**
60
+ * Unique identifier for this module instance (for debugging/diagnostics).
61
+ */
62
+ const MODULE_INSTANCE_ID = Math.random().toString(36).slice(2, 10)
63
+
64
+ /**
65
+ * Get the module load time (captures on first call since Date.now() is 0 at module load).
66
+ */
67
+ function getModuleLoadTime(): number {
68
+ if (MODULE_LOAD_TIME === null) {
69
+ MODULE_LOAD_TIME = Date.now()
70
+ }
71
+ return MODULE_LOAD_TIME
72
+ }
73
+
74
+ /**
75
+ * Get or create the hoisted PGLite instance.
76
+ *
77
+ * This function implements the singleton pattern with promise deduplication:
78
+ * - If an instance exists, return it immediately
79
+ * - If initialization is in progress, return the existing promise
80
+ * - Otherwise, start initialization and cache both the promise and the result
81
+ *
82
+ * @param options - PGLite creation options (only used if creating new instance)
83
+ * @returns Promise resolving to the PGLite instance
84
+ */
85
+ async function getOrCreateHoistedPglite(options: {
86
+ database?: string
87
+ debug?: 0 | 1
88
+ }): Promise<WorkersPGLite> {
89
+ // Fast path: already initialized
90
+ if (hoistedPglite) {
91
+ return hoistedPglite
92
+ }
93
+
94
+ // Deduplication: return existing promise if initialization in progress
95
+ if (hoistedPglitePromise) {
96
+ return hoistedPglitePromise
97
+ }
98
+
99
+ // Start initialization
100
+ hoistedPglitePromise = createWorkersPGLite({
101
+ wasmModule: pgliteWasm,
102
+ fsBundle: pgliteData,
103
+ database: options.database ?? 'postgres',
104
+ debug: options.debug ?? 0,
105
+ })
106
+
107
+ try {
108
+ hoistedPglite = await hoistedPglitePromise
109
+ return hoistedPglite
110
+ } finally {
111
+ // Clear the promise after resolution (success or failure)
112
+ hoistedPglitePromise = null
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Check if the hoisted PGLite instance exists.
118
+ * Useful for diagnostics and testing.
119
+ */
120
+ export function hasHoistedPglite(): boolean {
121
+ return hoistedPglite !== null
122
+ }
123
+
124
+ /**
125
+ * Get diagnostics about the hoisted WASM state.
126
+ * Useful for debugging warm start performance.
127
+ */
128
+ export function getHoistedPgliteDiagnostics(): {
129
+ hasInstance: boolean
130
+ hasPendingPromise: boolean
131
+ moduleInstanceId: string
132
+ moduleLoadTime: number
133
+ moduleAgeMs: number
134
+ } {
135
+ const now = Date.now()
136
+ const loadTime = getModuleLoadTime()
137
+ return {
138
+ hasInstance: hoistedPglite !== null,
139
+ hasPendingPromise: hoistedPglitePromise !== null,
140
+ moduleInstanceId: MODULE_INSTANCE_ID,
141
+ moduleLoadTime: loadTime,
142
+ moduleAgeMs: now - loadTime,
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Reset the hoisted PGLite instance.
148
+ * WARNING: This should only be used in tests. In production, the hoisted
149
+ * instance should persist for the lifetime of the isolate.
150
+ * @internal
151
+ */
152
+ export function resetHoistedPglite(): void {
153
+ hoistedPglite = null
154
+ hoistedPglitePromise = null
155
+ }
20
156
 
21
157
  /**
22
158
  * PGlite-like interface
@@ -50,6 +186,14 @@ export interface PGLiteManagerConfig {
50
186
  plugins?: PostgresConfig['plugins']
51
187
  /** Custom PGLite factory for testing */
52
188
  createPGLite?: () => Promise<PGliteLike>
189
+ /**
190
+ * Disable WASM hoisting optimization.
191
+ * When false (default), the PGLite WASM instance is cached at module level
192
+ * and survives DO class reinstantiation for optimal warm start performance.
193
+ * Set to true to create a new WASM instance for each PGLiteManager.
194
+ * @default false
195
+ */
196
+ disableHoisting?: boolean
53
197
  }
54
198
 
55
199
  /**
@@ -85,6 +229,9 @@ export class PGLiteManager {
85
229
  private config: PGLiteManagerConfig
86
230
  private pluginManager: PluginManager
87
231
 
232
+ /** Track whether this manager is using the hoisted instance */
233
+ private usingHoistedInstance = false
234
+
88
235
  constructor(config: PGLiteManagerConfig = {}) {
89
236
  // Build config without undefined values for exactOptionalPropertyTypes
90
237
  this.config = {
@@ -97,6 +244,9 @@ export class PGLiteManager {
97
244
  if (config.createPGLite !== undefined) {
98
245
  this.config.createPGLite = config.createPGLite
99
246
  }
247
+ if (config.disableHoisting !== undefined) {
248
+ this.config.disableHoisting = config.disableHoisting
249
+ }
100
250
 
101
251
  // Build plugin manager config without undefined values for exactOptionalPropertyTypes
102
252
  const pluginManagerConfig: PluginManagerConfig = {}
@@ -166,10 +316,15 @@ export class PGLiteManager {
166
316
  /**
167
317
  * Create the PGLite instance
168
318
  * Can be overridden for testing
319
+ *
320
+ * By default, uses the module-level hoisted PGLite instance for optimal
321
+ * warm start performance. The hoisted instance survives DO class
322
+ * reinstantiation within the same isolate.
169
323
  */
170
324
  protected async createPGLiteInstance(): Promise<PGliteLike> {
171
- // Use custom factory if provided
325
+ // Use custom factory if provided (for testing)
172
326
  if (this.config.createPGLite) {
327
+ this.usingHoistedInstance = false
173
328
  return this.config.createPGLite()
174
329
  }
175
330
 
@@ -180,25 +335,37 @@ export class PGLiteManager {
180
335
  // as WorkersPGLite doesn't support extension loading yet
181
336
  void extensions // Mark as intentionally unused
182
337
 
183
- // Build options without undefined values for exactOptionalPropertyTypes
184
- const pgliteOptions: {
185
- wasmModule: WebAssembly.Module
186
- fsBundle: ArrayBuffer
187
- database?: string
188
- debug: 0 | 1
189
- } = {
190
- // Pre-compiled WASM module for Workers
191
- wasmModule: pgliteWasm,
192
- // Filesystem bundle with PostgreSQL data files
193
- fsBundle: pgliteData,
194
- // Database configuration
195
- debug: this.config.debug ? 1 : 0,
196
- }
197
- if (this.config.database !== undefined) {
198
- pgliteOptions.database = this.config.database
338
+ // If hoisting is disabled, create a new instance each time
339
+ if (this.config.disableHoisting) {
340
+ this.usingHoistedInstance = false
341
+ console.log(`[PGLiteManager] WASM hoisting disabled - creating new instance`)
342
+ return createWorkersPGLite({
343
+ wasmModule: pgliteWasm,
344
+ fsBundle: pgliteData,
345
+ database: this.config.database ?? 'postgres',
346
+ debug: this.config.debug ? 1 : 0,
347
+ })
199
348
  }
200
349
 
201
- const pg = await createWorkersPGLite(pgliteOptions)
350
+ // Use the hoisted PGLite instance for optimal warm start performance.
351
+ // This is the key optimization: when the DO class is recreated but the
352
+ // isolate stays alive, we reuse the already-initialized WASM instance
353
+ // instead of loading it again (~1200ms -> ~30ms).
354
+ const wasmWasAlreadyLoaded = hoistedPglite !== null
355
+ const pg = await getOrCreateHoistedPglite({
356
+ database: this.config.database,
357
+ debug: this.config.debug ? 1 : 0,
358
+ })
359
+
360
+ this.usingHoistedInstance = true
361
+
362
+ // Log whether we reused the WASM instance (helpful for debugging warm starts)
363
+ if (wasmWasAlreadyLoaded) {
364
+ const diagnostics = getHoistedPgliteDiagnostics()
365
+ console.log(`[PGLiteManager] WASM REUSED - module age: ${diagnostics.moduleAgeMs}ms, instance: ${diagnostics.moduleInstanceId}`)
366
+ } else {
367
+ console.log(`[PGLiteManager] WASM LOADED (cold start) - instance: ${MODULE_INSTANCE_ID}`)
368
+ }
202
369
 
203
370
  return pg
204
371
  }
@@ -261,9 +428,28 @@ export class PGLiteManager {
261
428
  /**
262
429
  * Close PGLite instance
263
430
  *
431
+ * Note: When using the hoisted WASM instance (default), this method does NOT
432
+ * close the underlying PGLite instance, as it's shared across DO instances.
433
+ * It only clears the local reference. The hoisted instance persists for the
434
+ * lifetime of the isolate for optimal warm start performance.
435
+ *
436
+ * To actually close the hoisted instance, use resetHoistedPglite() followed
437
+ * by close() on the next manager that initializes. This should only be done
438
+ * in tests or when the isolate is shutting down.
439
+ *
264
440
  * @param timeoutMs - Maximum time to wait for close (default: 5000ms)
265
441
  */
266
442
  async close(timeoutMs: number = 5000): Promise<void> {
443
+ // If using hoisted instance, don't actually close it - just clear our reference
444
+ // The hoisted instance should survive for the lifetime of the isolate
445
+ if (this.usingHoistedInstance) {
446
+ console.log(`[PGLiteManager] Skipping close of hoisted WASM instance (will be reused)`)
447
+ this.pglite = null
448
+ this.initialized = false
449
+ this.usingHoistedInstance = false
450
+ return
451
+ }
452
+
267
453
  if (!this.pglite?.close) {
268
454
  this.pglite = null
269
455
  this.initialized = false
@@ -291,6 +477,36 @@ export class PGLiteManager {
291
477
  reset(): void {
292
478
  this.pglite = null
293
479
  this.initialized = false
480
+ this.usingHoistedInstance = false
481
+ }
482
+
483
+ /**
484
+ * Check if this manager is using the hoisted WASM instance
485
+ */
486
+ isUsingHoistedInstance(): boolean {
487
+ return this.usingHoistedInstance
488
+ }
489
+
490
+ /**
491
+ * Get diagnostics about WASM hoisting and initialization state.
492
+ * Useful for debugging warm start performance.
493
+ */
494
+ getDiagnostics(): {
495
+ initialized: boolean
496
+ usingHoistedInstance: boolean
497
+ hoisted: {
498
+ hasInstance: boolean
499
+ hasPendingPromise: boolean
500
+ moduleInstanceId: string
501
+ moduleLoadTime: number
502
+ moduleAgeMs: number
503
+ }
504
+ } {
505
+ return {
506
+ initialized: this.initialized,
507
+ usingHoistedInstance: this.usingHoistedInstance,
508
+ hoisted: getHoistedPgliteDiagnostics(),
509
+ }
294
510
  }
295
511
  }
296
512
 
@@ -152,42 +152,44 @@ function createApp() {
152
152
  app.post('/query', async (c) => {
153
153
  const dbId = c.req.query('db') || 'default'
154
154
 
155
- // Get the Durable Object stub
156
- const doId = c.env.POSTGRES_DO.idFromName(dbId)
157
- const stub = c.env.POSTGRES_DO.get(doId)
158
-
159
- // Forward to DO's /query endpoint
160
- const url = new URL(c.req.url)
161
- url.pathname = '/query'
162
- url.searchParams.delete('db')
163
-
164
- const doRequest = new Request(url.toString(), {
165
- method: 'POST',
166
- headers: c.req.raw.headers,
167
- body: c.req.raw.body,
168
- })
155
+ try {
156
+ const body = await c.req.json<{ sql: string; params?: unknown[] }>()
157
+ if (!body.sql) {
158
+ return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing sql field' }, 400)
159
+ }
169
160
 
170
- return stub.fetch(doRequest)
161
+ // Get the Durable Object stub and call RPC method directly
162
+ const doId = c.env.POSTGRES_DO.idFromName(dbId)
163
+ const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
164
+ const result = await stub.rpcQuery(body.sql, body.params)
165
+ return c.json(result)
166
+ } catch (error) {
167
+ const message = error instanceof Error ? error.message : 'Unknown error'
168
+ return c.json({ error: true, code: 'QUERY_ERROR', message }, 500)
169
+ }
171
170
  })
172
171
 
173
172
  // Batch queries
174
173
  app.post('/batch', async (c) => {
175
174
  const dbId = c.req.query('db') || 'default'
176
175
 
177
- const doId = c.env.POSTGRES_DO.idFromName(dbId)
178
- const stub = c.env.POSTGRES_DO.get(doId)
179
-
180
- const url = new URL(c.req.url)
181
- url.pathname = '/batch'
182
- url.searchParams.delete('db')
183
-
184
- const doRequest = new Request(url.toString(), {
185
- method: 'POST',
186
- headers: c.req.raw.headers,
187
- body: c.req.raw.body,
188
- })
176
+ try {
177
+ const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; transaction?: boolean }>()
178
+ if (!body.queries || !Array.isArray(body.queries)) {
179
+ return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
180
+ }
189
181
 
190
- return stub.fetch(doRequest)
182
+ // Get the Durable Object stub and call RPC method directly
183
+ const doId = c.env.POSTGRES_DO.idFromName(dbId)
184
+ const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
185
+ const result = body.transaction
186
+ ? await stub.rpcBatchTransaction(body.queries)
187
+ : await stub.rpcBatch(body.queries)
188
+ return c.json(result)
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : 'Unknown error'
191
+ return c.json({ error: true, code: 'BATCH_ERROR', message }, 500)
192
+ }
191
193
  })
192
194
 
193
195
  // ==========================================================================
@@ -383,8 +385,88 @@ function createApp() {
383
385
  }
384
386
  })
385
387
 
386
- // 404 handler
387
- app.notFound((c) => {
388
+ // ==========================================================================
389
+ // Dynamic database routes (for postgres.do client compatibility)
390
+ // ==========================================================================
391
+ // This handles URLs like https://db.example.com.ai/neon-example/query
392
+ // where the first path segment is the database ID
393
+ // We use notFound to handle these since Hono matches more specific routes first
394
+
395
+ const RESERVED_PATHS = new Set(['ping', 'health', 'user', 'db', 'rpc', 'query', 'batch'])
396
+
397
+ // 404 handler - also handles dynamic database routes
398
+ // Supports postgres.do client URLs like https://db.api.qa/mydb/query
399
+ app.notFound(async (c) => {
400
+ // Try to handle as dynamic database route: /:dbId/...
401
+ const pathParts = c.req.path.split('/').filter(Boolean)
402
+ if (pathParts.length >= 1) {
403
+ const dbId = pathParts[0]
404
+
405
+ // Skip if it's a reserved path
406
+ if (!RESERVED_PATHS.has(dbId)) {
407
+ const remainingPath = '/' + pathParts.slice(1).join('/')
408
+
409
+ // Get the Durable Object stub
410
+ const doId = c.env.POSTGRES_DO.idFromName(dbId)
411
+ const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
412
+
413
+ // Handle specific paths via RPC
414
+ if (remainingPath === '/query' && c.req.method === 'POST') {
415
+ try {
416
+ const body = await c.req.json<{ sql: string; params?: unknown[] }>()
417
+ if (!body.sql) {
418
+ return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing sql field' }, 400)
419
+ }
420
+ const result = await stub.rpcQuery(body.sql, body.params)
421
+ return c.json(result)
422
+ } catch (error) {
423
+ const message = error instanceof Error ? error.message : 'Unknown error'
424
+ return c.json({ error: true, code: 'QUERY_ERROR', message }, 500)
425
+ }
426
+ }
427
+
428
+ if (remainingPath === '/batch' && c.req.method === 'POST') {
429
+ try {
430
+ const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; transaction?: boolean }>()
431
+ if (!body.queries || !Array.isArray(body.queries)) {
432
+ return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
433
+ }
434
+ const result = body.transaction
435
+ ? await stub.rpcBatchTransaction(body.queries)
436
+ : await stub.rpcBatch(body.queries)
437
+ return c.json(result)
438
+ } catch (error) {
439
+ const message = error instanceof Error ? error.message : 'Unknown error'
440
+ return c.json({ error: true, code: 'BATCH_ERROR', message }, 500)
441
+ }
442
+ }
443
+
444
+ if (remainingPath === '/transaction' && c.req.method === 'POST') {
445
+ try {
446
+ const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; options?: unknown }>()
447
+ if (!body.queries || !Array.isArray(body.queries)) {
448
+ return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
449
+ }
450
+ const result = await stub.rpcBatchTransaction(body.queries)
451
+ return c.json(result)
452
+ } catch (error) {
453
+ const message = error instanceof Error ? error.message : 'Unknown error'
454
+ return c.json({ error: true, code: 'TRANSACTION_ERROR', message }, 500)
455
+ }
456
+ }
457
+
458
+ // For other paths, return database info
459
+ if (remainingPath === '/' || remainingPath === '') {
460
+ return c.json({
461
+ service: 'postgres.do',
462
+ database: dbId,
463
+ status: 'ready',
464
+ timestamp: new Date().toISOString(),
465
+ })
466
+ }
467
+ }
468
+ }
469
+
388
470
  return c.json({
389
471
  error: true,
390
472
  code: 'NOT_FOUND',
@@ -373,9 +373,45 @@ export { QueryStatsManager, createQueryStatsManager } from './query-stats-manage
373
373
  export type { QueryStats, ExtendedQueryStats, QueryStatsConfig } from './query-stats-manager'
374
374
 
375
375
  // PGLite Manager (extracted from PostgresDO for reuse)
376
- export { PGLiteManager, createPGLiteManager } from './do-pglite-manager'
376
+ // Includes WASM hoisting for optimal warm start performance (~1200ms -> ~30ms)
377
+ export {
378
+ PGLiteManager,
379
+ createPGLiteManager,
380
+ // WASM Hoisting utilities
381
+ hasHoistedPglite,
382
+ getHoistedPgliteDiagnostics,
383
+ resetHoistedPglite,
384
+ } from './do-pglite-manager'
377
385
  export type { PGLiteManagerConfig } from './do-pglite-manager'
378
386
 
387
+ // Lazy PGLite Manager (SPIKE: Lazy WASM loading experiment)
388
+ // Defers WASM loading until first query for improved cold start perception
389
+ export {
390
+ LazyPGLiteManager,
391
+ createLazyPGLiteManager,
392
+ // Lazy WASM Hoisting utilities
393
+ hasLazyHoistedPglite,
394
+ getLazyHoistedPgliteDiagnostics,
395
+ resetLazyHoistedPglite,
396
+ } from './lazy-pglite-manager'
397
+ export type { LazyPGLiteManagerConfig, WASMLoadingState } from './lazy-pglite-manager'
398
+
399
+ // Background PGLite Manager (RECOMMENDED: Eager-but-non-blocking pattern)
400
+ // Starts WASM loading immediately but doesn't block - best of both worlds:
401
+ // - Non-query endpoints respond instantly
402
+ // - WASM loads in background (uses ctx.waitUntil)
403
+ // - First query only waits for remaining load time (often zero)
404
+ export {
405
+ BackgroundPGLiteManager,
406
+ createBackgroundPGLiteManager,
407
+ // Background WASM Hoisting utilities
408
+ hasBgHoistedPglite,
409
+ isBgWasmLoading,
410
+ getBgHoistedPgliteDiagnostics,
411
+ resetBgHoistedPglite,
412
+ } from './background-pglite-manager'
413
+ export type { BackgroundPGLiteManagerConfig, BGWASMLoadingState } from './background-pglite-manager'
414
+
379
415
  // Query Execution Manager (extracted from PostgresDO for reuse - Task: postgres-an18)
380
416
  export { QueryExecutionManager, createQueryExecutionManager } from './query-execution-manager'
381
417
  export type { QueryExecutionDependencies, QueryExecutorPGlite } from './query-execution-manager'
@@ -387,3 +423,37 @@ export type { HealthCheckDependencies, HealthCheckPGlite, ColumnDescription, Dat
387
423
  // RPC Methods Manager (extracted from PostgresDO for reuse - Task: postgres-an18)
388
424
  export { RpcMethodsManager, createRpcMethodsManager } from './rpc-methods-manager'
389
425
  export type { RpcMethodsDependencies, RpcMethodsPGlite } from './rpc-methods-manager'
426
+
427
+ // MCP Server (Model Context Protocol)
428
+ export {
429
+ createMCPServer,
430
+ createMCPRoutes,
431
+ createPGBinding,
432
+ PG_BINDING_TYPES,
433
+ searchTool,
434
+ fetchTool,
435
+ doTool,
436
+ getToolDefinitions,
437
+ createToolHandlers,
438
+ createSearchHandler,
439
+ createFetchHandler,
440
+ createDoHandler,
441
+ } from '../mcp'
442
+ export type {
443
+ MCPServer,
444
+ MCPServerConfig,
445
+ MCPAuthContext,
446
+ QueryResult as MCPQueryResult,
447
+ TableInfo,
448
+ ColumnInfo,
449
+ MCPSearchResult,
450
+ MCPFetchResult,
451
+ PGBinding,
452
+ PGTransaction,
453
+ ToolResponse,
454
+ SearchInput,
455
+ FetchInput,
456
+ DoInput,
457
+ Tool as MCPTool,
458
+ QueryExecutor as MCPQueryExecutor,
459
+ } from '../mcp'