@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
package/src/do/sql.ts ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * SQL Tagged Template & Database Client
3
+ *
4
+ * Execute PostgreSQL queries with a simple API.
5
+ * Automatically routes to the Postgres Durable Object using cloudflare:workers env.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { sql, db } from '@dotdo/postgres'
10
+ *
11
+ * // Tagged template (safe from SQL injection)
12
+ * const posts = await sql`SELECT * FROM posts WHERE id = ${id}`
13
+ *
14
+ * // db.query for dynamic queries
15
+ * const result = await db.query('SELECT * FROM posts')
16
+ * ```
17
+ */
18
+
19
+ export interface SqlConfig {
20
+ /** DO binding name (default: 'POSTGRES') */
21
+ binding?: string
22
+ /** DO instance name (default: 'default') */
23
+ instance?: string
24
+ }
25
+
26
+ export interface SqlResult<T = Record<string, unknown>> {
27
+ rows: T[]
28
+ rowCount: number
29
+ fields?: { name: string; dataTypeID: number }[]
30
+ }
31
+
32
+ type PostgresNamespace = {
33
+ idFromName(name: string): { toString(): string }
34
+ get(id: { toString(): string }): {
35
+ fetch(request: Request): Promise<Response>
36
+ }
37
+ }
38
+
39
+ type DOStub = {
40
+ fetch(request: Request): Promise<Response>
41
+ }
42
+
43
+ /**
44
+ * Build a parameterized SQL query from template literals
45
+ */
46
+ function buildQuery(strings: TemplateStringsArray, values: unknown[]): { sql: string; params: unknown[] } {
47
+ let sql = ''
48
+ const params: unknown[] = []
49
+
50
+ for (let i = 0; i < strings.length; i++) {
51
+ sql += strings[i]
52
+ if (i < values.length) {
53
+ params.push(values[i])
54
+ sql += `$${params.length}`
55
+ }
56
+ }
57
+
58
+ return { sql, params }
59
+ }
60
+
61
+ // Cached env and stub
62
+ let cachedEnv: Record<string, unknown> | null = null
63
+ let cachedStub: DOStub | null = null
64
+
65
+ async function getStub(config: SqlConfig = {}): Promise<DOStub> {
66
+ if (cachedStub) return cachedStub
67
+
68
+ if (!cachedEnv) {
69
+ try {
70
+ const mod = await import('cloudflare:workers')
71
+ cachedEnv = mod.env as Record<string, unknown>
72
+ } catch {
73
+ throw new Error(
74
+ 'Could not import cloudflare:workers. ' +
75
+ 'Ensure you are running in Cloudflare Workers with nodejs_compat.'
76
+ )
77
+ }
78
+ }
79
+
80
+ const binding = config.binding || 'POSTGRES'
81
+ const instance = config.instance || 'default'
82
+
83
+ const namespace = cachedEnv[binding] as PostgresNamespace | undefined
84
+ if (!namespace) {
85
+ throw new Error(`Postgres binding "${binding}" not found. Add it to wrangler.toml.`)
86
+ }
87
+
88
+ cachedStub = namespace.get(namespace.idFromName(instance))
89
+ return cachedStub
90
+ }
91
+
92
+ async function executeQuery<T>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
93
+ const stub = await getStub()
94
+
95
+ const response = await stub.fetch(new Request('http://do/query', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ sql, params }),
99
+ }))
100
+
101
+ if (!response.ok) {
102
+ const error = await response.json() as { error: string }
103
+ throw new Error(error.error || 'Query failed')
104
+ }
105
+
106
+ return response.json() as Promise<SqlResult<T>>
107
+ }
108
+
109
+ // =============================================================================
110
+ // sql - Tagged Template API
111
+ // =============================================================================
112
+
113
+ /**
114
+ * SQL tagged template - safe parameterized queries
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * import { sql } from '@dotdo/postgres'
119
+ *
120
+ * const posts = await sql`SELECT * FROM posts`
121
+ * const post = await sql`SELECT * FROM posts WHERE id = ${id}`
122
+ * const newPost = await sql`INSERT INTO posts (title) VALUES (${title}) RETURNING *`
123
+ * ```
124
+ */
125
+ export async function sql<T = Record<string, unknown>>(
126
+ strings: TemplateStringsArray,
127
+ ...values: unknown[]
128
+ ): Promise<SqlResult<T>> {
129
+ const { sql: query, params } = buildQuery(strings, values)
130
+ return executeQuery<T>(query, params)
131
+ }
132
+
133
+ /**
134
+ * Execute raw SQL (use with caution - ensure input is sanitized)
135
+ */
136
+ sql.unsafe = async function<T = Record<string, unknown>>(
137
+ query: string,
138
+ params?: unknown[]
139
+ ): Promise<SqlResult<T>> {
140
+ return executeQuery<T>(query, params)
141
+ }
142
+
143
+ // =============================================================================
144
+ // db - Database Client API
145
+ // =============================================================================
146
+
147
+ /**
148
+ * Database client for programmatic access
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * import { db } from '@dotdo/postgres'
153
+ *
154
+ * const result = await db.query('SELECT * FROM posts')
155
+ * await db.exec('CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT)')
156
+ * ```
157
+ */
158
+ export const db = {
159
+ /**
160
+ * Execute a query and return results
161
+ */
162
+ async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
163
+ return executeQuery<T>(sql, params)
164
+ },
165
+
166
+ /**
167
+ * Execute SQL without returning results (DDL, etc.)
168
+ */
169
+ async exec(sql: string): Promise<void> {
170
+ await executeQuery(sql)
171
+ },
172
+
173
+ /**
174
+ * Get raw access to the DO stub for advanced use cases
175
+ */
176
+ async getStub(): Promise<DOStub> {
177
+ return getStub()
178
+ },
179
+ }
180
+
181
+ // =============================================================================
182
+ // Factory Functions (for custom binding/instance)
183
+ // =============================================================================
184
+
185
+ /**
186
+ * Create a sql function bound to a specific binding/instance
187
+ */
188
+ export function createSql(env: Record<string, unknown>, config: SqlConfig = {}) {
189
+ const binding = config.binding || 'POSTGRES'
190
+ const instance = config.instance || 'default'
191
+
192
+ const namespace = env[binding] as PostgresNamespace | undefined
193
+ if (!namespace) {
194
+ throw new Error(`Postgres binding "${binding}" not found in env.`)
195
+ }
196
+
197
+ const stub = namespace.get(namespace.idFromName(instance))
198
+
199
+ async function boundSql<T = Record<string, unknown>>(
200
+ strings: TemplateStringsArray,
201
+ ...values: unknown[]
202
+ ): Promise<SqlResult<T>> {
203
+ const { sql: query, params } = buildQuery(strings, values)
204
+
205
+ const response = await stub.fetch(new Request('http://do/query', {
206
+ method: 'POST',
207
+ headers: { 'Content-Type': 'application/json' },
208
+ body: JSON.stringify({ sql: query, params }),
209
+ }))
210
+
211
+ if (!response.ok) {
212
+ const error = await response.json() as { error: string }
213
+ throw new Error(error.error || 'Query failed')
214
+ }
215
+
216
+ return response.json() as Promise<SqlResult<T>>
217
+ }
218
+
219
+ boundSql.unsafe = async function<T = Record<string, unknown>>(
220
+ query: string,
221
+ params?: unknown[]
222
+ ): Promise<SqlResult<T>> {
223
+ const response = await stub.fetch(new Request('http://do/query', {
224
+ method: 'POST',
225
+ headers: { 'Content-Type': 'application/json' },
226
+ body: JSON.stringify({ sql: query, params }),
227
+ }))
228
+
229
+ if (!response.ok) {
230
+ const error = await response.json() as { error: string }
231
+ throw new Error(error.error || 'Query failed')
232
+ }
233
+
234
+ return response.json() as Promise<SqlResult<T>>
235
+ }
236
+
237
+ return boundSql
238
+ }
239
+
240
+ /**
241
+ * Create a db client bound to a specific binding/instance
242
+ */
243
+ export function createDb(env: Record<string, unknown>, config: SqlConfig = {}) {
244
+ const binding = config.binding || 'POSTGRES'
245
+ const instance = config.instance || 'default'
246
+
247
+ const namespace = env[binding] as PostgresNamespace | undefined
248
+ if (!namespace) {
249
+ throw new Error(`Postgres binding "${binding}" not found in env.`)
250
+ }
251
+
252
+ const stub = namespace.get(namespace.idFromName(instance))
253
+
254
+ return {
255
+ async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
256
+ const response = await stub.fetch(new Request('http://do/query', {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({ sql, params }),
260
+ }))
261
+
262
+ if (!response.ok) {
263
+ const error = await response.json() as { error: string }
264
+ throw new Error(error.error || 'Query failed')
265
+ }
266
+
267
+ return response.json() as Promise<SqlResult<T>>
268
+ },
269
+
270
+ async exec(sql: string): Promise<void> {
271
+ await this.query(sql)
272
+ },
273
+
274
+ getStub(): DOStub {
275
+ return stub
276
+ },
277
+ }
278
+ }
279
+
280
+ export default sql
package/src/index.ts CHANGED
@@ -1,48 +1,46 @@
1
1
  /**
2
2
  * @module @dotdo/postgres
3
3
  *
4
- * PostgreSQL server for Cloudflare Workers/DOs with PGLite WASM and tiered storage.
4
+ * PostgreSQL for Cloudflare Workers - Simple, Persistent, Powerful.
5
5
  *
6
- * This package provides a complete PostgreSQL-compatible database solution
7
- * optimized for Cloudflare's edge infrastructure:
8
- *
9
- * - **Worker Module**: PostgresDO Durable Object with HTTP/WebSocket APIs
10
- * - **PGLite Module**: Tiered storage system (Cache/DO/R2) for PGLite
11
- * - **Extensions Module**: PGLite extension management and pgvector support
12
- * - **Routing Module**: Intelligent query routing based on CPU cost estimation
13
- * - **Read-Only Module**: Security-focused read-only deployment mode
14
- * - **Middleware Module**: Rate limiting with sliding window and token bucket algorithms
15
- *
16
- * @example
6
+ * @example Minimal API
17
7
  * ```typescript
18
- * import {
19
- * PostgresDO,
20
- * createRoutes,
21
- * WebSocketHandler,
22
- * } from '@dotdo/postgres'
23
- *
24
- * // Export the Durable Object
25
- * export { PostgresDO }
26
- *
27
- * // Create Hono routes
28
- * const app = new Hono()
29
- * app.route('/api/sql', createRoutes(env))
8
+ * import { sql, Postgres } from '@dotdo/postgres'
9
+ * export { Postgres }
30
10
  *
31
11
  * export default {
32
- * fetch: app.fetch,
12
+ * fetch: () => Response.json(await sql`SELECT * FROM posts`)
33
13
  * }
34
14
  * ```
35
15
  *
36
- * @example
16
+ * @example With Hono
37
17
  * ```typescript
38
- * // Use tiered storage for PGLite
39
- * import { createDOVFS, TieredVFS, CacheLayer } from '@dotdo/postgres/pglite'
18
+ * import { Hono } from 'hono'
19
+ * import { sql, Postgres } from '@dotdo/postgres'
20
+ * export { Postgres }
40
21
  *
41
- * const vfs = createDOVFS(ctx.storage)
42
- * const pglite = await PGlite.create({ vfs })
22
+ * const app = new Hono()
23
+ * app.get('/posts', c => sql`SELECT * FROM posts`.then(r => c.json(r)))
24
+ * app.get('/posts/:id', c => sql`SELECT * FROM posts WHERE id = ${c.req.param('id')}`.then(r => c.json(r)))
25
+ *
26
+ * export default app
43
27
  * ```
28
+ *
29
+ * The Postgres Durable Object provides:
30
+ * - Full PostgreSQL via PGLite WASM
31
+ * - Automatic persistence via DO SQLite
32
+ * - Safe parameterized queries via tagged templates
33
+ *
34
+ * For advanced features (tiered storage, migrations, observability), see submodules.
44
35
  */
45
36
 
37
+ // ============================================================================
38
+ // Core API - The Minimal Interface
39
+ // ============================================================================
40
+
41
+ export { sql, db, createSql, createDb, Postgres } from './do'
42
+ export type { SqlConfig, SqlResult, PostgresEnv, QueryResult } from './do'
43
+
46
44
  // ============================================================================
47
45
  // Configuration Module - Centralized Constants
48
46
  // ============================================================================
@@ -210,3 +208,25 @@ export type {
210
208
  // Discriminated union
211
209
  DiscriminatedMessage,
212
210
  } from './types/utilities'
211
+
212
+ // ============================================================================
213
+ // Client Module - WebSocket client with rpc.do integration
214
+ // ============================================================================
215
+
216
+ export {
217
+ PostgresClient,
218
+ createPostgresClient,
219
+ } from './client'
220
+
221
+ export type {
222
+ PostgresClientConfig,
223
+ PostgresDORpcApi,
224
+ RpcQueryResult,
225
+ RpcBatchQuery,
226
+ RpcBatchResult,
227
+ RpcTransactionOptions,
228
+ TransactionApi,
229
+ ColumnInfo,
230
+ DatabaseStats,
231
+ ConnectionState,
232
+ } from './client'
@@ -0,0 +1,236 @@
1
+ /**
2
+ * PostgreSQL Binding for MCP Do Tool
3
+ *
4
+ * Creates the `pg` binding that is available in the sandboxed code execution
5
+ * environment. Wraps PostgresDO RPC calls in a clean API.
6
+ */
7
+
8
+ import type {
9
+ PGBinding,
10
+ PGTransaction,
11
+ QueryResult,
12
+ TableInfo,
13
+ ColumnInfo,
14
+ } from './types.js'
15
+
16
+ /**
17
+ * Interface for the query executor (PostgresDO stub)
18
+ */
19
+ export interface QueryExecutor {
20
+ rpcQuery(sql: string, params?: unknown[]): Promise<{
21
+ rows: Record<string, unknown>[]
22
+ fields: Array<{ name: string; dataTypeID: number }>
23
+ rowCount: number
24
+ }>
25
+ rpcBatchTransaction(
26
+ queries: Array<{ sql: string; params?: unknown[] }>
27
+ ): Promise<{
28
+ results: Array<{
29
+ rows: Record<string, unknown>[]
30
+ fields: Array<{ name: string; dataTypeID: number }>
31
+ rowCount: number
32
+ }>
33
+ success: boolean
34
+ }>
35
+ }
36
+
37
+ /**
38
+ * Create the pg binding for the do tool
39
+ *
40
+ * @param executor - Query executor (PostgresDO stub)
41
+ * @returns The pg binding object
42
+ */
43
+ export function createPGBinding(executor: QueryExecutor): PGBinding {
44
+ return {
45
+ async query<T = Record<string, unknown>>(
46
+ sql: string,
47
+ params?: unknown[]
48
+ ): Promise<QueryResult<T>> {
49
+ const result = await executor.rpcQuery(sql, params)
50
+ return {
51
+ rows: result.rows as T[],
52
+ rowCount: result.rowCount,
53
+ fields: result.fields,
54
+ }
55
+ },
56
+
57
+ async execute(
58
+ sql: string,
59
+ params?: unknown[]
60
+ ): Promise<{ rowCount: number }> {
61
+ const result = await executor.rpcQuery(sql, params)
62
+ return { rowCount: result.rowCount }
63
+ },
64
+
65
+ async transaction<T>(fn: (tx: PGTransaction) => Promise<T>): Promise<T> {
66
+ // Execute BEGIN, run function with transaction context, execute COMMIT/ROLLBACK
67
+ await executor.rpcQuery('BEGIN')
68
+
69
+ try {
70
+ const result = await fn({
71
+ async query<R = Record<string, unknown>>(
72
+ sql: string,
73
+ params?: unknown[]
74
+ ): Promise<QueryResult<R>> {
75
+ const res = await executor.rpcQuery(sql, params)
76
+ return {
77
+ rows: res.rows as R[],
78
+ rowCount: res.rowCount,
79
+ fields: res.fields,
80
+ }
81
+ },
82
+ async execute(
83
+ sql: string,
84
+ params?: unknown[]
85
+ ): Promise<{ rowCount: number }> {
86
+ const res = await executor.rpcQuery(sql, params)
87
+ return { rowCount: res.rowCount }
88
+ },
89
+ })
90
+
91
+ await executor.rpcQuery('COMMIT')
92
+ return result
93
+ } catch (error) {
94
+ await executor.rpcQuery('ROLLBACK')
95
+ throw error
96
+ }
97
+ },
98
+
99
+ async tables(): Promise<TableInfo[]> {
100
+ const result = await executor.rpcQuery(`
101
+ SELECT
102
+ table_name as name,
103
+ table_schema as schema,
104
+ CASE table_type
105
+ WHEN 'BASE TABLE' THEN 'table'
106
+ WHEN 'VIEW' THEN 'view'
107
+ ELSE 'table'
108
+ END as type
109
+ FROM information_schema.tables
110
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
111
+ ORDER BY table_schema, table_name
112
+ `)
113
+
114
+ return result.rows.map((row) => ({
115
+ name: row.name as string,
116
+ schema: row.schema as string,
117
+ type: row.type as 'table' | 'view' | 'materialized_view',
118
+ }))
119
+ },
120
+
121
+ async schema(tableName: string): Promise<ColumnInfo[]> {
122
+ // Validate table name to prevent SQL injection
123
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
124
+ throw new Error('Invalid table name')
125
+ }
126
+
127
+ const result = await executor.rpcQuery(
128
+ `
129
+ SELECT
130
+ c.column_name as name,
131
+ c.data_type as type,
132
+ c.is_nullable = 'YES' as nullable,
133
+ c.column_default as "default",
134
+ COALESCE(
135
+ (SELECT true FROM information_schema.key_column_usage kcu
136
+ JOIN information_schema.table_constraints tc
137
+ ON kcu.constraint_name = tc.constraint_name
138
+ WHERE tc.constraint_type = 'PRIMARY KEY'
139
+ AND kcu.table_name = c.table_name
140
+ AND kcu.column_name = c.column_name
141
+ LIMIT 1),
142
+ false
143
+ ) as "primaryKey"
144
+ FROM information_schema.columns c
145
+ WHERE c.table_name = $1
146
+ AND c.table_schema NOT IN ('pg_catalog', 'information_schema')
147
+ ORDER BY c.ordinal_position
148
+ `,
149
+ [tableName]
150
+ )
151
+
152
+ return result.rows.map((row) => ({
153
+ name: row.name as string,
154
+ type: row.type as string,
155
+ nullable: Boolean(row.nullable),
156
+ ...(row.default !== null && { default: row.default as string }),
157
+ primaryKey: Boolean(row.primaryKey),
158
+ }))
159
+ },
160
+ }
161
+ }
162
+
163
+ /**
164
+ * TypeScript type definitions for the pg binding
165
+ * Used by the do tool to provide type information to LLMs
166
+ */
167
+ export const PG_BINDING_TYPES = `
168
+ interface QueryResult<T = Record<string, unknown>> {
169
+ rows: T[]
170
+ rowCount: number
171
+ fields: Array<{ name: string; dataTypeID: number }>
172
+ }
173
+
174
+ interface TableInfo {
175
+ name: string
176
+ schema: string
177
+ type: 'table' | 'view' | 'materialized_view'
178
+ }
179
+
180
+ interface ColumnInfo {
181
+ name: string
182
+ type: string
183
+ nullable: boolean
184
+ default?: string
185
+ primaryKey: boolean
186
+ }
187
+
188
+ interface PGTransaction {
189
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
190
+ execute(sql: string, params?: unknown[]): Promise<{ rowCount: number }>
191
+ }
192
+
193
+ declare const pg: {
194
+ /**
195
+ * Execute a SQL query with optional parameters
196
+ * @example
197
+ * const users = await pg.query('SELECT * FROM users WHERE age > $1', [18])
198
+ * console.log(users.rows)
199
+ */
200
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
201
+
202
+ /**
203
+ * Execute a SQL statement (INSERT, UPDATE, DELETE)
204
+ * @example
205
+ * const result = await pg.execute('UPDATE users SET active = true WHERE id = $1', [1])
206
+ * console.log(result.rowCount)
207
+ */
208
+ execute(sql: string, params?: unknown[]): Promise<{ rowCount: number }>
209
+
210
+ /**
211
+ * Execute multiple statements in a transaction
212
+ * @example
213
+ * await pg.transaction(async (tx) => {
214
+ * await tx.execute('INSERT INTO orders (user_id) VALUES ($1)', [1])
215
+ * await tx.execute('UPDATE users SET order_count = order_count + 1 WHERE id = $1', [1])
216
+ * })
217
+ */
218
+ transaction<T>(fn: (tx: PGTransaction) => Promise<T>): Promise<T>
219
+
220
+ /**
221
+ * List all tables in the database
222
+ * @example
223
+ * const tables = await pg.tables()
224
+ * tables.forEach(t => console.log(t.name))
225
+ */
226
+ tables(): Promise<TableInfo[]>
227
+
228
+ /**
229
+ * Get schema information for a table
230
+ * @example
231
+ * const columns = await pg.schema('users')
232
+ * columns.forEach(c => console.log(\`\${c.name}: \${c.type}\`))
233
+ */
234
+ schema(tableName: string): Promise<ColumnInfo[]>
235
+ }
236
+ `