@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.
- package/README.md +73 -1
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +47 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/postgres-client.d.ts +273 -0
- package/dist/client/postgres-client.d.ts.map +1 -0
- package/dist/client/postgres-client.js +389 -0
- package/dist/client/postgres-client.js.map +1 -0
- package/dist/client/types.d.ts +167 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +7 -0
- package/dist/client/types.js.map +1 -0
- package/dist/do/index.d.ts +18 -0
- package/dist/do/index.d.ts.map +1 -0
- package/dist/do/index.js +18 -0
- package/dist/do/index.js.map +1 -0
- package/dist/do/postgres.d.ts +110 -0
- package/dist/do/postgres.d.ts.map +1 -0
- package/dist/do/postgres.js +266 -0
- package/dist/do/postgres.js.map +1 -0
- package/dist/do/sql.d.ts +92 -0
- package/dist/do/sql.d.ts.map +1 -0
- package/dist/do/sql.js +204 -0
- package/dist/do/sql.js.map +1 -0
- package/dist/index.d.ts +25 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp/binding.d.ts +47 -0
- package/dist/mcp/binding.d.ts.map +1 -0
- package/dist/mcp/binding.js +183 -0
- package/dist/mcp/binding.js.map +1 -0
- package/dist/mcp/index.d.ts +92 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +91 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +62 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +278 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +58 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +356 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +139 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/pglite/workers-pglite.d.ts +13 -4
- package/dist/pglite/workers-pglite.d.ts.map +1 -1
- package/dist/pglite/workers-pglite.js +110 -5
- package/dist/pglite/workers-pglite.js.map +1 -1
- package/dist/pglite-assets/pglite.data +0 -0
- package/dist/pglite-assets/pglite.wasm +0 -0
- package/dist/worker/auth.d.ts.map +1 -1
- package/dist/worker/auth.js +16 -6
- package/dist/worker/auth.js.map +1 -1
- package/dist/worker/background-pglite-manager.d.ts +243 -0
- package/dist/worker/background-pglite-manager.d.ts.map +1 -0
- package/dist/worker/background-pglite-manager.js +528 -0
- package/dist/worker/background-pglite-manager.js.map +1 -0
- package/dist/worker/do-pglite-manager.d.ts +77 -0
- package/dist/worker/do-pglite-manager.d.ts.map +1 -1
- package/dist/worker/do-pglite-manager.js +189 -12
- package/dist/worker/do-pglite-manager.js.map +1 -1
- package/dist/worker/entry.d.ts.map +1 -1
- package/dist/worker/entry.js +108 -26
- package/dist/worker/entry.js.map +1 -1
- package/dist/worker/index.d.ts +7 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +19 -1
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/lazy-pglite-manager.d.ts +242 -0
- package/dist/worker/lazy-pglite-manager.d.ts.map +1 -0
- package/dist/worker/lazy-pglite-manager.js +463 -0
- package/dist/worker/lazy-pglite-manager.js.map +1 -0
- package/package.json +20 -6
- package/src/client/index.ts +61 -0
- package/src/client/postgres-client.ts +442 -0
- package/src/client/types.ts +211 -0
- package/src/do/index.ts +18 -0
- package/src/do/postgres.ts +367 -0
- package/src/do/sql.ts +280 -0
- package/src/index.ts +50 -30
- package/src/mcp/binding.ts +236 -0
- package/src/mcp/index.ts +122 -0
- package/src/mcp/server.ts +361 -0
- package/src/mcp/tools.ts +464 -0
- package/src/mcp/types.ts +148 -0
- package/src/pglite/workers-pglite.ts +141 -12
- package/src/pglite-assets/pglite.data +0 -0
- package/src/pglite-assets/pglite.wasm +0 -0
- package/src/worker/auth.ts +17 -6
- package/src/worker/background-pglite-manager.ts +680 -0
- package/src/worker/do-pglite-manager.ts +235 -19
- package/src/worker/entry.ts +112 -30
- package/src/worker/index.ts +71 -1
- package/src/worker/lazy-pglite-manager.ts +595 -0
- package/dist/iceberg/duckdb-wasm.d.ts +0 -447
- package/dist/iceberg/duckdb-wasm.d.ts.map +0 -1
- package/dist/iceberg/duckdb-wasm.js +0 -600
- package/dist/iceberg/duckdb-wasm.js.map +0 -1
- package/dist/iceberg/test-fixtures.d.ts +0 -151
- package/dist/iceberg/test-fixtures.d.ts.map +0 -1
- package/dist/iceberg/test-fixtures.js +0 -446
- package/dist/iceberg/test-fixtures.js.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.d.ts +0 -31
- package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.js +0 -33
- 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'
|
package/src/do/index.ts
ADDED
|
@@ -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
|