@bytebase/dbhub 0.20.0 → 0.21.0

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.
@@ -0,0 +1,480 @@
1
+ import {
2
+ quoteIdentifier
3
+ } from "./chunk-JFWX35TB.js";
4
+ import {
5
+ SQLRowLimiter
6
+ } from "./chunk-BRXZ5ZQB.js";
7
+ import {
8
+ ConnectorRegistry,
9
+ SafeURL,
10
+ obfuscateDSNPassword,
11
+ splitSQLStatements
12
+ } from "./chunk-C7WEAPX4.js";
13
+
14
+ // src/connectors/postgres/index.ts
15
+ import pg from "pg";
16
+ var { Pool } = pg;
17
+ var PostgresDSNParser = class {
18
+ async parse(dsn, config) {
19
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
20
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
21
+ if (!this.isValidDSN(dsn)) {
22
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
23
+ const expectedFormat = this.getSampleDSN();
24
+ throw new Error(
25
+ `Invalid PostgreSQL DSN format.
26
+ Provided: ${obfuscatedDSN}
27
+ Expected: ${expectedFormat}`
28
+ );
29
+ }
30
+ try {
31
+ const url = new SafeURL(dsn);
32
+ const poolConfig = {
33
+ host: url.hostname,
34
+ port: url.port ? parseInt(url.port) : 5432,
35
+ database: url.pathname ? url.pathname.substring(1) : "",
36
+ // Remove leading '/' if exists
37
+ user: url.username,
38
+ password: url.password
39
+ };
40
+ url.forEachSearchParam((value, key) => {
41
+ if (key === "sslmode") {
42
+ if (value === "disable") {
43
+ poolConfig.ssl = false;
44
+ } else if (value === "require") {
45
+ poolConfig.ssl = { rejectUnauthorized: false };
46
+ } else {
47
+ poolConfig.ssl = true;
48
+ }
49
+ }
50
+ });
51
+ if (connectionTimeoutSeconds !== void 0) {
52
+ poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
53
+ }
54
+ if (queryTimeoutSeconds !== void 0) {
55
+ poolConfig.query_timeout = queryTimeoutSeconds * 1e3;
56
+ }
57
+ return poolConfig;
58
+ } catch (error) {
59
+ throw new Error(
60
+ `Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
61
+ );
62
+ }
63
+ }
64
+ getSampleDSN() {
65
+ return "postgres://postgres:password@localhost:5432/postgres?sslmode=require";
66
+ }
67
+ isValidDSN(dsn) {
68
+ try {
69
+ return dsn.startsWith("postgres://") || dsn.startsWith("postgresql://");
70
+ } catch (error) {
71
+ return false;
72
+ }
73
+ }
74
+ };
75
+ var PostgresConnector = class _PostgresConnector {
76
+ constructor() {
77
+ this.id = "postgres";
78
+ this.name = "PostgreSQL";
79
+ this.dsnParser = new PostgresDSNParser();
80
+ this.pool = null;
81
+ // Source ID is set by ConnectorManager after cloning
82
+ this.sourceId = "default";
83
+ // Default schema for discovery methods (first entry from search_path, or "public")
84
+ this.defaultSchema = "public";
85
+ }
86
+ getId() {
87
+ return this.sourceId;
88
+ }
89
+ clone() {
90
+ return new _PostgresConnector();
91
+ }
92
+ async connect(dsn, initScript, config) {
93
+ this.defaultSchema = "public";
94
+ try {
95
+ const poolConfig = await this.dsnParser.parse(dsn, config);
96
+ if (config?.readonly) {
97
+ poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
98
+ }
99
+ if (config?.searchPath) {
100
+ const schemas = config.searchPath.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
101
+ if (schemas.length > 0) {
102
+ this.defaultSchema = schemas[0];
103
+ const quotedSchemas = schemas.map((s) => quoteIdentifier(s, "postgres"));
104
+ const optionsValue = quotedSchemas.join(",").replace(/\\/g, "\\\\").replace(/ /g, "\\ ");
105
+ poolConfig.options = (poolConfig.options || "") + ` -c search_path=${optionsValue}`;
106
+ }
107
+ }
108
+ this.pool = new Pool(poolConfig);
109
+ const client = await this.pool.connect();
110
+ client.release();
111
+ } catch (err) {
112
+ console.error("Failed to connect to PostgreSQL database:", err);
113
+ throw err;
114
+ }
115
+ }
116
+ async disconnect() {
117
+ if (this.pool) {
118
+ await this.pool.end();
119
+ this.pool = null;
120
+ }
121
+ }
122
+ async getSchemas() {
123
+ if (!this.pool) {
124
+ throw new Error("Not connected to database");
125
+ }
126
+ const client = await this.pool.connect();
127
+ try {
128
+ const result = await client.query(`
129
+ SELECT schema_name
130
+ FROM information_schema.schemata
131
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
132
+ ORDER BY schema_name
133
+ `);
134
+ return result.rows.map((row) => row.schema_name);
135
+ } finally {
136
+ client.release();
137
+ }
138
+ }
139
+ async getTables(schema) {
140
+ if (!this.pool) {
141
+ throw new Error("Not connected to database");
142
+ }
143
+ const client = await this.pool.connect();
144
+ try {
145
+ const schemaToUse = schema || this.defaultSchema;
146
+ const result = await client.query(
147
+ `
148
+ SELECT table_name
149
+ FROM information_schema.tables
150
+ WHERE table_schema = $1
151
+ ORDER BY table_name
152
+ `,
153
+ [schemaToUse]
154
+ );
155
+ return result.rows.map((row) => row.table_name);
156
+ } finally {
157
+ client.release();
158
+ }
159
+ }
160
+ async tableExists(tableName, schema) {
161
+ if (!this.pool) {
162
+ throw new Error("Not connected to database");
163
+ }
164
+ const client = await this.pool.connect();
165
+ try {
166
+ const schemaToUse = schema || this.defaultSchema;
167
+ const result = await client.query(
168
+ `
169
+ SELECT EXISTS (
170
+ SELECT FROM information_schema.tables
171
+ WHERE table_schema = $1
172
+ AND table_name = $2
173
+ )
174
+ `,
175
+ [schemaToUse, tableName]
176
+ );
177
+ return result.rows[0].exists;
178
+ } finally {
179
+ client.release();
180
+ }
181
+ }
182
+ async getTableIndexes(tableName, schema) {
183
+ if (!this.pool) {
184
+ throw new Error("Not connected to database");
185
+ }
186
+ const client = await this.pool.connect();
187
+ try {
188
+ const schemaToUse = schema || this.defaultSchema;
189
+ const result = await client.query(
190
+ `
191
+ SELECT
192
+ i.relname as index_name,
193
+ array_agg(a.attname) as column_names,
194
+ ix.indisunique as is_unique,
195
+ ix.indisprimary as is_primary
196
+ FROM
197
+ pg_class t,
198
+ pg_class i,
199
+ pg_index ix,
200
+ pg_attribute a,
201
+ pg_namespace ns
202
+ WHERE
203
+ t.oid = ix.indrelid
204
+ AND i.oid = ix.indexrelid
205
+ AND a.attrelid = t.oid
206
+ AND a.attnum = ANY(ix.indkey)
207
+ AND t.relkind = 'r'
208
+ AND t.relname = $1
209
+ AND ns.oid = t.relnamespace
210
+ AND ns.nspname = $2
211
+ GROUP BY
212
+ i.relname,
213
+ ix.indisunique,
214
+ ix.indisprimary
215
+ ORDER BY
216
+ i.relname
217
+ `,
218
+ [tableName, schemaToUse]
219
+ );
220
+ return result.rows.map((row) => ({
221
+ index_name: row.index_name,
222
+ column_names: row.column_names,
223
+ is_unique: row.is_unique,
224
+ is_primary: row.is_primary
225
+ }));
226
+ } finally {
227
+ client.release();
228
+ }
229
+ }
230
+ async getTableSchema(tableName, schema) {
231
+ if (!this.pool) {
232
+ throw new Error("Not connected to database");
233
+ }
234
+ const client = await this.pool.connect();
235
+ try {
236
+ const schemaToUse = schema || this.defaultSchema;
237
+ const result = await client.query(
238
+ `
239
+ SELECT
240
+ c.column_name,
241
+ c.data_type,
242
+ c.is_nullable,
243
+ c.column_default,
244
+ pgd.description
245
+ FROM information_schema.columns c
246
+ LEFT JOIN pg_catalog.pg_namespace nsp
247
+ ON nsp.nspname = c.table_schema
248
+ LEFT JOIN pg_catalog.pg_class cls
249
+ ON cls.relnamespace = nsp.oid
250
+ AND cls.relname = c.table_name
251
+ LEFT JOIN pg_catalog.pg_description pgd
252
+ ON pgd.objoid = cls.oid
253
+ AND pgd.objsubid = c.ordinal_position
254
+ WHERE c.table_schema = $1
255
+ AND c.table_name = $2
256
+ ORDER BY c.ordinal_position
257
+ `,
258
+ [schemaToUse, tableName]
259
+ );
260
+ return result.rows;
261
+ } finally {
262
+ client.release();
263
+ }
264
+ }
265
+ async getTableRowCount(tableName, schema) {
266
+ if (!this.pool) {
267
+ throw new Error("Not connected to database");
268
+ }
269
+ const client = await this.pool.connect();
270
+ try {
271
+ const schemaToUse = schema || this.defaultSchema;
272
+ const result = await client.query(
273
+ `
274
+ SELECT c.reltuples::bigint as count
275
+ FROM pg_class c
276
+ JOIN pg_namespace n ON n.oid = c.relnamespace
277
+ WHERE c.relname = $1
278
+ AND n.nspname = $2
279
+ AND c.relkind IN ('r','p','m','f')
280
+ `,
281
+ [tableName, schemaToUse]
282
+ );
283
+ if (result.rows.length > 0) {
284
+ const count = Number(result.rows[0].count);
285
+ return count >= 0 ? count : null;
286
+ }
287
+ return null;
288
+ } finally {
289
+ client.release();
290
+ }
291
+ }
292
+ async getTableComment(tableName, schema) {
293
+ if (!this.pool) {
294
+ throw new Error("Not connected to database");
295
+ }
296
+ const client = await this.pool.connect();
297
+ try {
298
+ const schemaToUse = schema || this.defaultSchema;
299
+ const result = await client.query(
300
+ `
301
+ SELECT obj_description(c.oid) as table_comment
302
+ FROM pg_catalog.pg_class c
303
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
304
+ WHERE c.relname = $1
305
+ AND n.nspname = $2
306
+ AND c.relkind IN ('r','p','m','f')
307
+ `,
308
+ [tableName, schemaToUse]
309
+ );
310
+ if (result.rows.length > 0) {
311
+ return result.rows[0].table_comment || null;
312
+ }
313
+ return null;
314
+ } finally {
315
+ client.release();
316
+ }
317
+ }
318
+ async getStoredProcedures(schema, routineType) {
319
+ if (!this.pool) {
320
+ throw new Error("Not connected to database");
321
+ }
322
+ const client = await this.pool.connect();
323
+ try {
324
+ const schemaToUse = schema || this.defaultSchema;
325
+ const params = [schemaToUse];
326
+ let typeFilter = "";
327
+ if (routineType === "function") {
328
+ typeFilter = " AND routine_type = 'FUNCTION'";
329
+ } else if (routineType === "procedure") {
330
+ typeFilter = " AND routine_type = 'PROCEDURE'";
331
+ }
332
+ const result = await client.query(
333
+ `
334
+ SELECT
335
+ routine_name
336
+ FROM information_schema.routines
337
+ WHERE routine_schema = $1${typeFilter}
338
+ ORDER BY routine_name
339
+ `,
340
+ params
341
+ );
342
+ return result.rows.map((row) => row.routine_name);
343
+ } finally {
344
+ client.release();
345
+ }
346
+ }
347
+ async getStoredProcedureDetail(procedureName, schema) {
348
+ if (!this.pool) {
349
+ throw new Error("Not connected to database");
350
+ }
351
+ const client = await this.pool.connect();
352
+ try {
353
+ const schemaToUse = schema || this.defaultSchema;
354
+ const result = await client.query(
355
+ `
356
+ SELECT
357
+ routine_name as procedure_name,
358
+ routine_type,
359
+ CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
360
+ external_language as language,
361
+ data_type as return_type,
362
+ routine_definition as definition,
363
+ (
364
+ SELECT string_agg(
365
+ parameter_name || ' ' ||
366
+ parameter_mode || ' ' ||
367
+ data_type,
368
+ ', '
369
+ )
370
+ FROM information_schema.parameters
371
+ WHERE specific_schema = $1
372
+ AND specific_name = $2
373
+ AND parameter_name IS NOT NULL
374
+ ) as parameter_list
375
+ FROM information_schema.routines
376
+ WHERE routine_schema = $1
377
+ AND routine_name = $2
378
+ `,
379
+ [schemaToUse, procedureName]
380
+ );
381
+ if (result.rows.length === 0) {
382
+ throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
383
+ }
384
+ const procedure = result.rows[0];
385
+ let definition = procedure.definition;
386
+ try {
387
+ const oidResult = await client.query(
388
+ `
389
+ SELECT p.oid, p.prosrc
390
+ FROM pg_proc p
391
+ JOIN pg_namespace n ON p.pronamespace = n.oid
392
+ WHERE p.proname = $1
393
+ AND n.nspname = $2
394
+ `,
395
+ [procedureName, schemaToUse]
396
+ );
397
+ if (oidResult.rows.length > 0) {
398
+ if (!definition) {
399
+ const oid = oidResult.rows[0].oid;
400
+ const defResult = await client.query(`SELECT pg_get_functiondef($1)`, [oid]);
401
+ if (defResult.rows.length > 0) {
402
+ definition = defResult.rows[0].pg_get_functiondef;
403
+ } else {
404
+ definition = oidResult.rows[0].prosrc;
405
+ }
406
+ }
407
+ }
408
+ } catch (err) {
409
+ console.error(`Error getting procedure definition: ${err}`);
410
+ }
411
+ return {
412
+ procedure_name: procedure.procedure_name,
413
+ procedure_type: procedure.procedure_type,
414
+ language: procedure.language || "sql",
415
+ parameter_list: procedure.parameter_list || "",
416
+ return_type: procedure.return_type !== "void" ? procedure.return_type : void 0,
417
+ definition: definition || void 0
418
+ };
419
+ } finally {
420
+ client.release();
421
+ }
422
+ }
423
+ async executeSQL(sql, options, parameters) {
424
+ if (!this.pool) {
425
+ throw new Error("Not connected to database");
426
+ }
427
+ const client = await this.pool.connect();
428
+ try {
429
+ const statements = splitSQLStatements(sql, "postgres");
430
+ if (statements.length === 1) {
431
+ const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
432
+ let result;
433
+ if (parameters && parameters.length > 0) {
434
+ try {
435
+ result = await client.query(processedStatement, parameters);
436
+ } catch (error) {
437
+ console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
438
+ console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
439
+ console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
440
+ throw error;
441
+ }
442
+ } else {
443
+ result = await client.query(processedStatement);
444
+ }
445
+ return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
446
+ } else {
447
+ if (parameters && parameters.length > 0) {
448
+ throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
449
+ }
450
+ let allRows = [];
451
+ let totalRowCount = 0;
452
+ await client.query("BEGIN");
453
+ try {
454
+ for (let statement of statements) {
455
+ const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
456
+ const result = await client.query(processedStatement);
457
+ if (result.rows && result.rows.length > 0) {
458
+ allRows.push(...result.rows);
459
+ }
460
+ if (result.rowCount) {
461
+ totalRowCount += result.rowCount;
462
+ }
463
+ }
464
+ await client.query("COMMIT");
465
+ } catch (error) {
466
+ await client.query("ROLLBACK");
467
+ throw error;
468
+ }
469
+ return { rows: allRows, rowCount: totalRowCount };
470
+ }
471
+ } finally {
472
+ client.release();
473
+ }
474
+ }
475
+ };
476
+ var postgresConnector = new PostgresConnector();
477
+ ConnectorRegistry.register(postgresConnector);
478
+ export {
479
+ PostgresConnector
480
+ };
@@ -2,9 +2,8 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-25VMLRAQ.js";
5
+ } from "./chunk-GRGEI5QT.js";
6
6
  import "./chunk-C7WEAPX4.js";
7
- import "./chunk-WWAWV7DQ.js";
8
7
  export {
9
8
  ToolRegistry,
10
9
  getToolRegistry,