tide-commander 0.52.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/assets/characters/Textures/colormap.png +0 -0
  4. package/dist/assets/characters/character-female-a.glb +0 -0
  5. package/dist/assets/characters/character-female-b.glb +0 -0
  6. package/dist/assets/characters/character-female-c.glb +0 -0
  7. package/dist/assets/characters/character-female-d.glb +0 -0
  8. package/dist/assets/characters/character-female-e.glb +0 -0
  9. package/dist/assets/characters/character-female-f.glb +0 -0
  10. package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
  11. package/dist/assets/characters/character-male-a.glb +0 -0
  12. package/dist/assets/characters/character-male-b.glb +0 -0
  13. package/dist/assets/characters/character-male-c.glb +0 -0
  14. package/dist/assets/characters/character-male-d.glb +0 -0
  15. package/dist/assets/characters/character-male-e.glb +0 -0
  16. package/dist/assets/characters/character-male-f.glb +0 -0
  17. package/dist/assets/icons/icon-192.png +0 -0
  18. package/dist/assets/icons/icon-512.png +0 -0
  19. package/dist/assets/landing-Cc0MDBAK.css +1 -0
  20. package/dist/assets/main-BIpLsrUu.css +1 -0
  21. package/dist/assets/main-DMTRw3br.js +276 -0
  22. package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
  23. package/dist/assets/textures/logo-blanco.png +0 -0
  24. package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
  25. package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
  26. package/dist/assets/web-BZdi2lG9.js +1 -0
  27. package/dist/assets/web-yHsOO1Qb.js +1 -0
  28. package/dist/index.html +38 -0
  29. package/dist/manifest.json +39 -0
  30. package/dist/src/packages/landing/index.html +463 -0
  31. package/dist/src/packages/server/app.js +87 -0
  32. package/dist/src/packages/server/auth/index.js +121 -0
  33. package/dist/src/packages/server/claude/backend.js +578 -0
  34. package/dist/src/packages/server/claude/index.js +8 -0
  35. package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
  36. package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
  37. package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
  38. package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
  39. package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
  40. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
  41. package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
  42. package/dist/src/packages/server/claude/runner.js +310 -0
  43. package/dist/src/packages/server/claude/session-loader.js +898 -0
  44. package/dist/src/packages/server/claude/types.js +5 -0
  45. package/dist/src/packages/server/cli.js +113 -0
  46. package/dist/src/packages/server/codex/backend.js +119 -0
  47. package/dist/src/packages/server/codex/index.js +2 -0
  48. package/dist/src/packages/server/codex/json-event-parser.js +612 -0
  49. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
  50. package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
  51. package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
  52. package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
  53. package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
  54. package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
  55. package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
  56. package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
  57. package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
  58. package/dist/src/packages/server/data/builtin-skills.js +6 -0
  59. package/dist/src/packages/server/data/index.js +890 -0
  60. package/dist/src/packages/server/data/snapshots.js +371 -0
  61. package/dist/src/packages/server/index.js +96 -0
  62. package/dist/src/packages/server/prompts/tide-commander.js +13 -0
  63. package/dist/src/packages/server/routes/agents.js +406 -0
  64. package/dist/src/packages/server/routes/config.js +347 -0
  65. package/dist/src/packages/server/routes/custom-models.js +170 -0
  66. package/dist/src/packages/server/routes/exec.js +269 -0
  67. package/dist/src/packages/server/routes/files.js +995 -0
  68. package/dist/src/packages/server/routes/index.js +38 -0
  69. package/dist/src/packages/server/routes/notifications.js +81 -0
  70. package/dist/src/packages/server/routes/permissions.js +115 -0
  71. package/dist/src/packages/server/routes/snapshots.js +224 -0
  72. package/dist/src/packages/server/routes/stt.js +99 -0
  73. package/dist/src/packages/server/routes/tts.js +166 -0
  74. package/dist/src/packages/server/routes/voice-assistant.js +310 -0
  75. package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
  76. package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
  77. package/dist/src/packages/server/runtime/index.js +2 -0
  78. package/dist/src/packages/server/runtime/types.js +6 -0
  79. package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
  80. package/dist/src/packages/server/services/agent-service.js +410 -0
  81. package/dist/src/packages/server/services/boss-message-service.js +430 -0
  82. package/dist/src/packages/server/services/boss-service.js +553 -0
  83. package/dist/src/packages/server/services/building-service.js +867 -0
  84. package/dist/src/packages/server/services/claude-service.js +5 -0
  85. package/dist/src/packages/server/services/custom-class-service.js +323 -0
  86. package/dist/src/packages/server/services/database-service.js +914 -0
  87. package/dist/src/packages/server/services/docker-service.js +865 -0
  88. package/dist/src/packages/server/services/fileTracker.js +242 -0
  89. package/dist/src/packages/server/services/index.js +21 -0
  90. package/dist/src/packages/server/services/permission-service.js +258 -0
  91. package/dist/src/packages/server/services/pm2-service.js +435 -0
  92. package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
  93. package/dist/src/packages/server/services/runtime-events.js +357 -0
  94. package/dist/src/packages/server/services/runtime-service.js +308 -0
  95. package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
  96. package/dist/src/packages/server/services/runtime-subagents.js +50 -0
  97. package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
  98. package/dist/src/packages/server/services/secrets-service.js +206 -0
  99. package/dist/src/packages/server/services/skill-service.js +508 -0
  100. package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
  101. package/dist/src/packages/server/services/supervisor-claude.js +132 -0
  102. package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
  103. package/dist/src/packages/server/services/supervisor-service.js +659 -0
  104. package/dist/src/packages/server/services/work-plan-service.js +476 -0
  105. package/dist/src/packages/server/setup.js +86 -0
  106. package/dist/src/packages/server/utils/index.js +4 -0
  107. package/dist/src/packages/server/utils/logger.js +302 -0
  108. package/dist/src/packages/server/utils/string.js +39 -0
  109. package/dist/src/packages/server/utils/tool-formatting.js +139 -0
  110. package/dist/src/packages/server/utils/unicode.js +46 -0
  111. package/dist/src/packages/server/websocket/handler.js +290 -0
  112. package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
  113. package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
  114. package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
  115. package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
  116. package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
  117. package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
  118. package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
  119. package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
  120. package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
  121. package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
  122. package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
  123. package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
  124. package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
  125. package/dist/src/packages/server/websocket/handlers/types.js +4 -0
  126. package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
  127. package/dist/src/packages/server/websocket/listeners/index.js +32 -0
  128. package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
  129. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
  130. package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
  131. package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
  132. package/dist/src/packages/shared/agent-types.js +54 -0
  133. package/dist/src/packages/shared/building-types.js +43 -0
  134. package/dist/src/packages/shared/common-types.js +1 -0
  135. package/dist/src/packages/shared/database-types.js +8 -0
  136. package/dist/src/packages/shared/types/snapshot.js +7 -0
  137. package/dist/src/packages/shared/types.js +12 -0
  138. package/dist/src/packages/shared/websocket-messages.js +1 -0
  139. package/dist/sw.js +37 -0
  140. package/package.json +90 -0
@@ -0,0 +1,914 @@
1
+ /**
2
+ * Database Service
3
+ * Handles database connections and query execution for MySQL and PostgreSQL
4
+ */
5
+ import mysql from 'mysql2/promise';
6
+ import pg from 'pg';
7
+ import oracledb from 'oracledb';
8
+ import { loadQueryHistory, saveQueryHistory } from '../data/index.js';
9
+ // Connection pool storage
10
+ const mysqlPools = new Map();
11
+ const pgPools = new Map();
12
+ const oraclePools = new Map();
13
+ // Note: oracledb 6.0+ uses thin mode by default, which doesn't require Oracle Instant Client
14
+ // In-memory query history cache
15
+ const queryHistoryCache = new Map();
16
+ /**
17
+ * Generate a unique key for connection pooling
18
+ */
19
+ function getConnectionKey(connection, database) {
20
+ return `${connection.id}:${database || connection.database || 'default'}`;
21
+ }
22
+ /**
23
+ * Get or create a MySQL connection pool
24
+ */
25
+ async function getMySQLPool(connection, database) {
26
+ const key = getConnectionKey(connection, database);
27
+ if (mysqlPools.has(key)) {
28
+ return mysqlPools.get(key);
29
+ }
30
+ const pool = mysql.createPool({
31
+ host: connection.host,
32
+ port: connection.port,
33
+ user: connection.username,
34
+ password: connection.password,
35
+ database: database || connection.database,
36
+ ssl: connection.ssl ? {
37
+ rejectUnauthorized: connection.sslConfig?.rejectUnauthorized ?? true,
38
+ ca: connection.sslConfig?.ca,
39
+ cert: connection.sslConfig?.cert,
40
+ key: connection.sslConfig?.key,
41
+ } : undefined,
42
+ waitForConnections: true,
43
+ connectionLimit: 5,
44
+ queueLimit: 0,
45
+ enableKeepAlive: true,
46
+ keepAliveInitialDelay: 10000,
47
+ });
48
+ mysqlPools.set(key, pool);
49
+ return pool;
50
+ }
51
+ /**
52
+ * Get or create a PostgreSQL connection pool
53
+ */
54
+ async function getPgPool(connection, database) {
55
+ const key = getConnectionKey(connection, database);
56
+ if (pgPools.has(key)) {
57
+ return pgPools.get(key);
58
+ }
59
+ const pool = new pg.Pool({
60
+ host: connection.host,
61
+ port: connection.port,
62
+ user: connection.username,
63
+ password: connection.password,
64
+ database: database || connection.database || 'postgres',
65
+ ssl: connection.ssl ? {
66
+ rejectUnauthorized: connection.sslConfig?.rejectUnauthorized ?? true,
67
+ ca: connection.sslConfig?.ca,
68
+ cert: connection.sslConfig?.cert,
69
+ key: connection.sslConfig?.key,
70
+ } : undefined,
71
+ max: 5,
72
+ idleTimeoutMillis: 30000,
73
+ connectionTimeoutMillis: 10000,
74
+ });
75
+ pgPools.set(key, pool);
76
+ return pool;
77
+ }
78
+ /**
79
+ * Get or create an Oracle connection pool
80
+ * Note: For Oracle, we always connect to the same service (from connection.database).
81
+ * The "database" parameter in other functions represents the schema/owner to query,
82
+ * not a different database to connect to.
83
+ */
84
+ async function getOraclePool(connection) {
85
+ // For Oracle, we use only the connection ID as the key since we always connect
86
+ // to the same service - the schema is just used in queries, not connection
87
+ const key = connection.id;
88
+ if (oraclePools.has(key)) {
89
+ return oraclePools.get(key);
90
+ }
91
+ // Build connection string - Oracle uses service name or SID
92
+ // Format: host:port/serviceName
93
+ // The service name comes from connection.database (e.g., ORCLPDB1)
94
+ const serviceName = connection.database || 'ORCL';
95
+ const connectString = `${connection.host}:${connection.port}/${serviceName}`;
96
+ const pool = await oracledb.createPool({
97
+ user: connection.username,
98
+ password: connection.password,
99
+ connectString,
100
+ poolMin: 1,
101
+ poolMax: 5,
102
+ poolIncrement: 1,
103
+ poolTimeout: 60,
104
+ });
105
+ oraclePools.set(key, pool);
106
+ return pool;
107
+ }
108
+ /**
109
+ * Test a database connection
110
+ */
111
+ export async function testConnection(connection) {
112
+ try {
113
+ if (connection.engine === 'mysql') {
114
+ const pool = await getMySQLPool(connection);
115
+ const [rows] = await pool.query('SELECT VERSION() as version');
116
+ const version = rows[0]?.version;
117
+ return { success: true, serverVersion: version };
118
+ }
119
+ else if (connection.engine === 'postgresql') {
120
+ const pool = await getPgPool(connection);
121
+ const result = await pool.query('SELECT version()');
122
+ const version = result.rows[0]?.version?.split(' ').slice(0, 2).join(' ');
123
+ return { success: true, serverVersion: version };
124
+ }
125
+ else if (connection.engine === 'oracle') {
126
+ const pool = await getOraclePool(connection);
127
+ const conn = await pool.getConnection();
128
+ try {
129
+ const result = await conn.execute("SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1", [], { outFormat: oracledb.OUT_FORMAT_OBJECT });
130
+ const version = result.rows?.[0]?.BANNER || 'Oracle';
131
+ return { success: true, serverVersion: version };
132
+ }
133
+ finally {
134
+ await conn.close();
135
+ }
136
+ }
137
+ return { success: false, error: 'Unsupported database engine' };
138
+ }
139
+ catch (error) {
140
+ const message = error instanceof Error ? error.message : 'Unknown error';
141
+ return { success: false, error: message };
142
+ }
143
+ }
144
+ /**
145
+ * List all databases available on the connection
146
+ */
147
+ export async function listDatabases(connection) {
148
+ try {
149
+ if (connection.engine === 'mysql') {
150
+ const pool = await getMySQLPool(connection);
151
+ const [rows] = await pool.query('SHOW DATABASES');
152
+ return rows.map(r => r.Database);
153
+ }
154
+ else if (connection.engine === 'postgresql') {
155
+ const pool = await getPgPool(connection);
156
+ const result = await pool.query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname");
157
+ return result.rows.map(r => r.datname);
158
+ }
159
+ else if (connection.engine === 'oracle') {
160
+ // Oracle: list accessible schemas as "databases"
161
+ // Try ALL_USERS first, fall back to just the current user's schema
162
+ const pool = await getOraclePool(connection);
163
+ const conn = await pool.getConnection();
164
+ try {
165
+ const execOptions = { outFormat: oracledb.OUT_FORMAT_OBJECT };
166
+ // First try to get all accessible schemas
167
+ try {
168
+ const result = await conn.execute(`SELECT USERNAME FROM ALL_USERS
169
+ WHERE USERNAME NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'OUTLN', 'XDB', 'WMSYS', 'CTXSYS', 'ANONYMOUS', 'MDSYS', 'OLAPSYS', 'ORDDATA', 'ORDSYS', 'EXFSYS', 'DMSYS', 'APEX_PUBLIC_USER', 'APPQOSSYS', 'AUDSYS', 'DBSFWUSER', 'DIP', 'GGSYS', 'GSMADMIN_INTERNAL', 'GSMCATUSER', 'GSMUSER', 'LBACSYS', 'OJVMSYS', 'REMOTE_SCHEDULER_AGENT', 'SYS$UMF', 'SYSBACKUP', 'SYSDG', 'SYSKM', 'SYSRAC', 'XS$NULL')
170
+ AND ORACLE_MAINTAINED = 'N'
171
+ ORDER BY USERNAME`, [], execOptions);
172
+ if (result.rows && result.rows.length > 0) {
173
+ return result.rows.map(r => r.USERNAME);
174
+ }
175
+ }
176
+ catch {
177
+ // ORACLE_MAINTAINED column might not exist in older versions, try simpler query
178
+ try {
179
+ const result = await conn.execute(`SELECT USERNAME FROM ALL_USERS
180
+ WHERE USERNAME NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'OUTLN', 'XDB', 'WMSYS', 'CTXSYS', 'ANONYMOUS', 'MDSYS', 'OLAPSYS', 'ORDDATA', 'ORDSYS', 'EXFSYS', 'DMSYS', 'APEX_PUBLIC_USER')
181
+ ORDER BY USERNAME`, [], execOptions);
182
+ if (result.rows && result.rows.length > 0) {
183
+ return result.rows.map(r => r.USERNAME);
184
+ }
185
+ }
186
+ catch {
187
+ // Fall through to current user only
188
+ }
189
+ }
190
+ // Fallback: just return the current user's schema
191
+ const userResult = await conn.execute(`SELECT USER as USERNAME FROM DUAL`, [], execOptions);
192
+ return (userResult.rows || []).map(r => r.USERNAME);
193
+ }
194
+ finally {
195
+ await conn.close();
196
+ }
197
+ }
198
+ return [];
199
+ }
200
+ catch (error) {
201
+ console.error('Error listing databases:', error);
202
+ throw error;
203
+ }
204
+ }
205
+ /**
206
+ * List all tables in a database
207
+ */
208
+ export async function listTables(connection, database) {
209
+ try {
210
+ if (connection.engine === 'mysql') {
211
+ const pool = await getMySQLPool(connection, database);
212
+ const [rows] = await pool.query(`
213
+ SELECT
214
+ TABLE_NAME as name,
215
+ TABLE_TYPE as type,
216
+ ENGINE as engine,
217
+ TABLE_ROWS as \`rows\`,
218
+ DATA_LENGTH + INDEX_LENGTH as size,
219
+ TABLE_COMMENT as comment
220
+ FROM information_schema.TABLES
221
+ WHERE TABLE_SCHEMA = ?
222
+ ORDER BY TABLE_NAME
223
+ `, [database]);
224
+ return rows.map(r => ({
225
+ name: r.name,
226
+ type: r.type === 'VIEW' ? 'view' : 'table',
227
+ engine: r.engine,
228
+ rows: r.rows,
229
+ size: r.size,
230
+ comment: r.comment || undefined,
231
+ }));
232
+ }
233
+ else if (connection.engine === 'postgresql') {
234
+ const pool = await getPgPool(connection, database);
235
+ const result = await pool.query(`
236
+ SELECT
237
+ t.tablename as name,
238
+ 'table' as type,
239
+ pg_total_relation_size(quote_ident(t.schemaname) || '.' || quote_ident(t.tablename)) as size,
240
+ obj_description((quote_ident(t.schemaname) || '.' || quote_ident(t.tablename))::regclass) as comment
241
+ FROM pg_tables t
242
+ WHERE t.schemaname = 'public'
243
+ UNION ALL
244
+ SELECT
245
+ v.viewname as name,
246
+ 'view' as type,
247
+ 0 as size,
248
+ obj_description((quote_ident(v.schemaname) || '.' || quote_ident(v.viewname))::regclass) as comment
249
+ FROM pg_views v
250
+ WHERE v.schemaname = 'public'
251
+ ORDER BY name
252
+ `);
253
+ return result.rows.map(r => ({
254
+ name: r.name,
255
+ type: r.type,
256
+ size: parseInt(r.size) || undefined,
257
+ comment: r.comment || undefined,
258
+ }));
259
+ }
260
+ else if (connection.engine === 'oracle') {
261
+ // In Oracle, database parameter is treated as schema/owner
262
+ const pool = await getOraclePool(connection);
263
+ const conn = await pool.getConnection();
264
+ try {
265
+ // Simple query without joins - more compatible with restricted permissions
266
+ const result = await conn.execute(`
267
+ SELECT TABLE_NAME as NAME, 'table' as TYPE, NUM_ROWS
268
+ FROM ALL_TABLES
269
+ WHERE OWNER = :owner
270
+ UNION ALL
271
+ SELECT VIEW_NAME as NAME, 'view' as TYPE, NULL as NUM_ROWS
272
+ FROM ALL_VIEWS
273
+ WHERE OWNER = :owner
274
+ ORDER BY NAME
275
+ `, { owner: database.toUpperCase() }, { outFormat: oracledb.OUT_FORMAT_OBJECT });
276
+ return (result.rows || []).map(r => ({
277
+ name: r.NAME,
278
+ type: r.TYPE,
279
+ rows: r.NUM_ROWS || undefined,
280
+ }));
281
+ }
282
+ finally {
283
+ await conn.close();
284
+ }
285
+ }
286
+ return [];
287
+ }
288
+ catch (error) {
289
+ console.error('Error listing tables:', error);
290
+ throw error;
291
+ }
292
+ }
293
+ /**
294
+ * Get table schema (columns, indexes, foreign keys)
295
+ */
296
+ export async function getTableSchema(connection, database, table) {
297
+ const columns = [];
298
+ const indexes = [];
299
+ const foreignKeys = [];
300
+ try {
301
+ if (connection.engine === 'mysql') {
302
+ const pool = await getMySQLPool(connection, database);
303
+ // Get columns
304
+ const [columnRows] = await pool.query(`
305
+ SELECT
306
+ COLUMN_NAME as name,
307
+ COLUMN_TYPE as type,
308
+ IS_NULLABLE as nullable,
309
+ COLUMN_DEFAULT as defaultValue,
310
+ COLUMN_KEY as columnKey,
311
+ EXTRA as extra,
312
+ COLUMN_COMMENT as comment
313
+ FROM information_schema.COLUMNS
314
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
315
+ ORDER BY ORDINAL_POSITION
316
+ `, [database, table]);
317
+ for (const col of columnRows) {
318
+ columns.push({
319
+ name: col.name,
320
+ type: col.type,
321
+ nullable: col.nullable === 'YES',
322
+ defaultValue: col.defaultValue ?? undefined,
323
+ primaryKey: col.columnKey === 'PRI',
324
+ autoIncrement: col.extra.includes('auto_increment'),
325
+ comment: col.comment || undefined,
326
+ });
327
+ }
328
+ // Get indexes
329
+ const [indexRows] = await pool.query(`
330
+ SELECT
331
+ INDEX_NAME as name,
332
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
333
+ NOT NON_UNIQUE as isUnique,
334
+ INDEX_TYPE as type
335
+ FROM information_schema.STATISTICS
336
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
337
+ GROUP BY INDEX_NAME, NON_UNIQUE, INDEX_TYPE
338
+ `, [database, table]);
339
+ for (const idx of indexRows) {
340
+ indexes.push({
341
+ name: idx.name,
342
+ columns: idx.columns.split(','),
343
+ unique: Boolean(idx.isUnique),
344
+ type: idx.type,
345
+ });
346
+ }
347
+ // Get foreign keys
348
+ const [fkRows] = await pool.query(`
349
+ SELECT
350
+ kcu.CONSTRAINT_NAME as name,
351
+ GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as columns,
352
+ kcu.REFERENCED_TABLE_NAME as referencedTable,
353
+ GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as referencedColumns,
354
+ rc.DELETE_RULE as onDelete,
355
+ rc.UPDATE_RULE as onUpdate
356
+ FROM information_schema.KEY_COLUMN_USAGE kcu
357
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
358
+ ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
359
+ AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
360
+ WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
361
+ GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE, rc.UPDATE_RULE
362
+ `, [database, table]);
363
+ for (const fk of fkRows) {
364
+ foreignKeys.push({
365
+ name: fk.name,
366
+ columns: fk.columns.split(','),
367
+ referencedTable: fk.referencedTable,
368
+ referencedColumns: fk.referencedColumns.split(','),
369
+ onDelete: fk.onDelete,
370
+ onUpdate: fk.onUpdate,
371
+ });
372
+ }
373
+ }
374
+ else if (connection.engine === 'postgresql') {
375
+ const pool = await getPgPool(connection, database);
376
+ // Get columns
377
+ const columnResult = await pool.query(`
378
+ SELECT
379
+ a.attname as name,
380
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
381
+ NOT a.attnotnull as nullable,
382
+ pg_get_expr(d.adbin, d.adrelid) as "defaultValue",
383
+ COALESCE(pk.is_pk, false) as "primaryKey",
384
+ a.attidentity != '' OR COALESCE(s.is_serial, false) as "autoIncrement",
385
+ col_description(c.oid, a.attnum) as comment
386
+ FROM pg_class c
387
+ JOIN pg_attribute a ON a.attrelid = c.oid
388
+ LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
389
+ LEFT JOIN (
390
+ SELECT kcu.column_name, true as is_pk
391
+ FROM information_schema.table_constraints tc
392
+ JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
393
+ WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY'
394
+ ) pk ON pk.column_name = a.attname
395
+ LEFT JOIN (
396
+ SELECT column_name, true as is_serial
397
+ FROM information_schema.columns
398
+ WHERE table_name = $1 AND column_default LIKE 'nextval%'
399
+ ) s ON s.column_name = a.attname
400
+ WHERE c.relname = $1 AND a.attnum > 0 AND NOT a.attisdropped
401
+ ORDER BY a.attnum
402
+ `, [table]);
403
+ for (const col of columnResult.rows) {
404
+ columns.push({
405
+ name: col.name,
406
+ type: col.type,
407
+ nullable: col.nullable,
408
+ defaultValue: col.defaultValue ?? undefined,
409
+ primaryKey: col.primaryKey,
410
+ autoIncrement: col.autoIncrement,
411
+ comment: col.comment || undefined,
412
+ });
413
+ }
414
+ // Get indexes
415
+ const indexResult = await pool.query(`
416
+ SELECT
417
+ i.relname as name,
418
+ array_agg(a.attname ORDER BY x.n) as columns,
419
+ ix.indisunique as "unique",
420
+ am.amname as type
421
+ FROM pg_index ix
422
+ JOIN pg_class t ON t.oid = ix.indrelid
423
+ JOIN pg_class i ON i.oid = ix.indexrelid
424
+ JOIN pg_am am ON am.oid = i.relam
425
+ CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS x(attnum, n)
426
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
427
+ WHERE t.relname = $1 AND t.relkind = 'r'
428
+ GROUP BY i.relname, ix.indisunique, am.amname
429
+ `, [table]);
430
+ for (const idx of indexResult.rows) {
431
+ indexes.push({
432
+ name: idx.name,
433
+ columns: idx.columns,
434
+ unique: idx.unique,
435
+ type: idx.type,
436
+ });
437
+ }
438
+ // Get foreign keys
439
+ const fkResult = await pool.query(`
440
+ SELECT
441
+ conname as name,
442
+ array_agg(a.attname ORDER BY x.n) as columns,
443
+ confrelid::regclass::text as "referencedTable",
444
+ array_agg(af.attname ORDER BY x.n) as "referencedColumns",
445
+ CASE confdeltype
446
+ WHEN 'a' THEN 'NO ACTION'
447
+ WHEN 'r' THEN 'RESTRICT'
448
+ WHEN 'c' THEN 'CASCADE'
449
+ WHEN 'n' THEN 'SET NULL'
450
+ WHEN 'd' THEN 'SET DEFAULT'
451
+ END as "onDelete",
452
+ CASE confupdtype
453
+ WHEN 'a' THEN 'NO ACTION'
454
+ WHEN 'r' THEN 'RESTRICT'
455
+ WHEN 'c' THEN 'CASCADE'
456
+ WHEN 'n' THEN 'SET NULL'
457
+ WHEN 'd' THEN 'SET DEFAULT'
458
+ END as "onUpdate"
459
+ FROM pg_constraint c
460
+ JOIN pg_class t ON t.oid = c.conrelid
461
+ CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS x(attnum, fkattnum, n)
462
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = x.attnum
463
+ JOIN pg_attribute af ON af.attrelid = c.confrelid AND af.attnum = x.fkattnum
464
+ WHERE t.relname = $1 AND c.contype = 'f'
465
+ GROUP BY conname, confrelid, confdeltype, confupdtype
466
+ `, [table]);
467
+ for (const fk of fkResult.rows) {
468
+ foreignKeys.push({
469
+ name: fk.name,
470
+ columns: fk.columns,
471
+ referencedTable: fk.referencedTable,
472
+ referencedColumns: fk.referencedColumns,
473
+ onDelete: fk.onDelete,
474
+ onUpdate: fk.onUpdate,
475
+ });
476
+ }
477
+ }
478
+ else if (connection.engine === 'oracle') {
479
+ // In Oracle, database parameter is treated as schema/owner
480
+ const pool = await getOraclePool(connection);
481
+ const conn = await pool.getConnection();
482
+ const execOptions = { outFormat: oracledb.OUT_FORMAT_OBJECT };
483
+ try {
484
+ // Get columns (compatible with Oracle 11g and later)
485
+ const columnResult = await conn.execute(`
486
+ SELECT
487
+ c.COLUMN_NAME as NAME,
488
+ c.DATA_TYPE || CASE
489
+ WHEN c.DATA_TYPE IN ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR') THEN '(' || c.DATA_LENGTH || ')'
490
+ WHEN c.DATA_TYPE = 'NUMBER' AND c.DATA_PRECISION IS NOT NULL THEN '(' || c.DATA_PRECISION || ',' || NVL(c.DATA_SCALE, 0) || ')'
491
+ ELSE ''
492
+ END as TYPE,
493
+ c.NULLABLE,
494
+ c.DATA_DEFAULT as DEFAULT_VALUE,
495
+ CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END as PRIMARY_KEY,
496
+ cc.COMMENTS
497
+ FROM ALL_TAB_COLUMNS c
498
+ LEFT JOIN (
499
+ SELECT cols.COLUMN_NAME, cols.TABLE_NAME, cols.OWNER
500
+ FROM ALL_CONSTRAINTS cons
501
+ JOIN ALL_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME AND cons.OWNER = cols.OWNER
502
+ WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.TABLE_NAME = :table AND cons.OWNER = :owner
503
+ ) pk ON c.COLUMN_NAME = pk.COLUMN_NAME AND c.TABLE_NAME = pk.TABLE_NAME AND c.OWNER = pk.OWNER
504
+ LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER
505
+ WHERE c.TABLE_NAME = :table AND c.OWNER = :owner
506
+ ORDER BY c.COLUMN_ID
507
+ `, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
508
+ for (const col of columnResult.rows || []) {
509
+ columns.push({
510
+ name: col.NAME,
511
+ type: col.TYPE,
512
+ nullable: col.NULLABLE === 'Y',
513
+ defaultValue: col.DEFAULT_VALUE?.trim() || undefined,
514
+ primaryKey: col.PRIMARY_KEY === 1,
515
+ autoIncrement: false, // Oracle doesn't have auto_increment in the same way
516
+ comment: col.COMMENTS || undefined,
517
+ });
518
+ }
519
+ // Get indexes
520
+ const indexResult = await conn.execute(`
521
+ SELECT
522
+ i.INDEX_NAME as NAME,
523
+ LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) as COLUMNS,
524
+ i.UNIQUENESS as IS_UNIQUE,
525
+ i.INDEX_TYPE as TYPE
526
+ FROM ALL_INDEXES i
527
+ JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER
528
+ WHERE i.TABLE_NAME = :table AND i.OWNER = :owner
529
+ GROUP BY i.INDEX_NAME, i.UNIQUENESS, i.INDEX_TYPE
530
+ `, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
531
+ for (const idx of indexResult.rows || []) {
532
+ indexes.push({
533
+ name: idx.NAME,
534
+ columns: idx.COLUMNS.split(','),
535
+ unique: idx.IS_UNIQUE === 'UNIQUE',
536
+ type: idx.TYPE,
537
+ });
538
+ }
539
+ // Get foreign keys
540
+ const fkResult = await conn.execute(`
541
+ SELECT
542
+ c.CONSTRAINT_NAME as NAME,
543
+ LISTAGG(cols.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY cols.POSITION) as COLUMNS,
544
+ r_cons.TABLE_NAME as REFERENCED_TABLE,
545
+ LISTAGG(r_cols.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY r_cols.POSITION) as REFERENCED_COLUMNS,
546
+ c.DELETE_RULE
547
+ FROM ALL_CONSTRAINTS c
548
+ JOIN ALL_CONS_COLUMNS cols ON c.CONSTRAINT_NAME = cols.CONSTRAINT_NAME AND c.OWNER = cols.OWNER
549
+ JOIN ALL_CONSTRAINTS r_cons ON c.R_CONSTRAINT_NAME = r_cons.CONSTRAINT_NAME AND c.R_OWNER = r_cons.OWNER
550
+ JOIN ALL_CONS_COLUMNS r_cols ON r_cons.CONSTRAINT_NAME = r_cols.CONSTRAINT_NAME AND r_cons.OWNER = r_cols.OWNER
551
+ WHERE c.CONSTRAINT_TYPE = 'R' AND c.TABLE_NAME = :table AND c.OWNER = :owner
552
+ GROUP BY c.CONSTRAINT_NAME, r_cons.TABLE_NAME, c.DELETE_RULE
553
+ `, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
554
+ for (const fk of fkResult.rows || []) {
555
+ foreignKeys.push({
556
+ name: fk.NAME,
557
+ columns: fk.COLUMNS.split(','),
558
+ referencedTable: fk.REFERENCED_TABLE,
559
+ referencedColumns: fk.REFERENCED_COLUMNS.split(','),
560
+ onDelete: fk.DELETE_RULE || undefined,
561
+ onUpdate: undefined, // Oracle doesn't support ON UPDATE in the same way
562
+ });
563
+ }
564
+ }
565
+ finally {
566
+ await conn.close();
567
+ }
568
+ }
569
+ }
570
+ catch (error) {
571
+ console.error('Error getting table schema:', error);
572
+ throw error;
573
+ }
574
+ return { columns, indexes, foreignKeys };
575
+ }
576
+ /**
577
+ * Execute a query and return results
578
+ */
579
+ export async function executeQuery(connection, database, query, limit = 1000) {
580
+ const queryId = `q_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
581
+ const startTime = Date.now();
582
+ try {
583
+ let rows = [];
584
+ let fields = [];
585
+ let affectedRows;
586
+ // Determine if this is a SELECT-like query
587
+ const trimmedQuery = query.trim().toLowerCase();
588
+ const isSelect = trimmedQuery.startsWith('select') ||
589
+ trimmedQuery.startsWith('show') ||
590
+ trimmedQuery.startsWith('describe') ||
591
+ trimmedQuery.startsWith('explain');
592
+ if (connection.engine === 'mysql') {
593
+ const pool = await getMySQLPool(connection, database);
594
+ if (isSelect) {
595
+ // Add LIMIT if not present
596
+ let limitedQuery = query;
597
+ if (!trimmedQuery.includes(' limit ')) {
598
+ limitedQuery = `${query.trim().replace(/;$/, '')} LIMIT ${limit}`;
599
+ }
600
+ const [result, fieldInfo] = await pool.query(limitedQuery);
601
+ rows = result;
602
+ if (fieldInfo && Array.isArray(fieldInfo)) {
603
+ fields = fieldInfo.map((f) => ({
604
+ name: f.name,
605
+ type: getFieldTypeName(f.type, 'mysql'),
606
+ table: f.table || undefined,
607
+ }));
608
+ }
609
+ }
610
+ else {
611
+ const [result] = await pool.query(query);
612
+ affectedRows = result.affectedRows;
613
+ }
614
+ }
615
+ else if (connection.engine === 'postgresql') {
616
+ const pool = await getPgPool(connection, database);
617
+ if (isSelect) {
618
+ // Add LIMIT if not present
619
+ let limitedQuery = query;
620
+ if (!trimmedQuery.includes(' limit ')) {
621
+ limitedQuery = `${query.trim().replace(/;$/, '')} LIMIT ${limit}`;
622
+ }
623
+ const result = await pool.query(limitedQuery);
624
+ rows = result.rows;
625
+ if (result.fields) {
626
+ fields = result.fields.map(f => ({
627
+ name: f.name,
628
+ type: getFieldTypeName(f.dataTypeID, 'postgresql'),
629
+ table: undefined,
630
+ }));
631
+ }
632
+ }
633
+ else {
634
+ const result = await pool.query(query);
635
+ affectedRows = result.rowCount ?? undefined;
636
+ }
637
+ }
638
+ else if (connection.engine === 'oracle') {
639
+ const pool = await getOraclePool(connection);
640
+ const conn = await pool.getConnection();
641
+ try {
642
+ if (isSelect) {
643
+ // Add FETCH FIRST for Oracle 12c+ if no ROWNUM/FETCH present
644
+ let limitedQuery = query;
645
+ if (!trimmedQuery.includes('rownum') && !trimmedQuery.includes('fetch ')) {
646
+ limitedQuery = `${query.trim().replace(/;$/, '')} FETCH FIRST ${limit} ROWS ONLY`;
647
+ }
648
+ const result = await conn.execute(limitedQuery, [], {
649
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
650
+ fetchArraySize: limit,
651
+ });
652
+ rows = result.rows || [];
653
+ if (result.metaData) {
654
+ fields = result.metaData.map(m => ({
655
+ name: m.name,
656
+ type: getFieldTypeName(m.dbType, 'oracle'),
657
+ table: undefined,
658
+ }));
659
+ }
660
+ }
661
+ else {
662
+ const result = await conn.execute(query, [], { autoCommit: true });
663
+ affectedRows = result.rowsAffected;
664
+ }
665
+ }
666
+ finally {
667
+ await conn.close();
668
+ }
669
+ }
670
+ const duration = Date.now() - startTime;
671
+ return {
672
+ id: queryId,
673
+ connectionId: connection.id,
674
+ database,
675
+ query,
676
+ status: 'success',
677
+ executedAt: startTime,
678
+ duration,
679
+ rows,
680
+ fields,
681
+ rowCount: rows.length,
682
+ affectedRows,
683
+ };
684
+ }
685
+ catch (error) {
686
+ const duration = Date.now() - startTime;
687
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
688
+ const errorCode = error?.code;
689
+ return {
690
+ id: queryId,
691
+ connectionId: connection.id,
692
+ database,
693
+ query,
694
+ status: 'error',
695
+ executedAt: startTime,
696
+ duration,
697
+ error: errorMessage,
698
+ errorCode,
699
+ };
700
+ }
701
+ }
702
+ /**
703
+ * Add a query to history
704
+ */
705
+ export function addToHistory(buildingId, result) {
706
+ const entry = {
707
+ id: result.id,
708
+ buildingId,
709
+ connectionId: result.connectionId,
710
+ database: result.database,
711
+ query: result.query,
712
+ executedAt: result.executedAt,
713
+ duration: result.duration,
714
+ status: result.status,
715
+ rowCount: result.rowCount,
716
+ error: result.error,
717
+ favorite: false,
718
+ };
719
+ let history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
720
+ // Add new entry at the beginning
721
+ history = [entry, ...history];
722
+ // Keep only last 50 entries
723
+ if (history.length > 50) {
724
+ history = history.slice(0, 50);
725
+ }
726
+ queryHistoryCache.set(buildingId, history);
727
+ saveQueryHistory(buildingId, history);
728
+ }
729
+ /**
730
+ * Get query history for a building
731
+ */
732
+ export function getHistory(buildingId, limit = 100) {
733
+ let history = queryHistoryCache.get(buildingId);
734
+ if (!history) {
735
+ history = loadQueryHistory(buildingId);
736
+ queryHistoryCache.set(buildingId, history);
737
+ }
738
+ return history.slice(0, limit);
739
+ }
740
+ /**
741
+ * Toggle favorite status for a query
742
+ */
743
+ export function toggleFavorite(buildingId, queryId) {
744
+ const history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
745
+ const entry = history.find(h => h.id === queryId);
746
+ if (entry) {
747
+ entry.favorite = !entry.favorite;
748
+ queryHistoryCache.set(buildingId, history);
749
+ saveQueryHistory(buildingId, history);
750
+ return entry.favorite;
751
+ }
752
+ return false;
753
+ }
754
+ /**
755
+ * Delete a query from history
756
+ */
757
+ export function deleteFromHistory(buildingId, queryId) {
758
+ let history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
759
+ history = history.filter(h => h.id !== queryId);
760
+ queryHistoryCache.set(buildingId, history);
761
+ saveQueryHistory(buildingId, history);
762
+ }
763
+ /**
764
+ * Clear all query history for a building
765
+ */
766
+ export function clearHistory(buildingId) {
767
+ queryHistoryCache.set(buildingId, []);
768
+ saveQueryHistory(buildingId, []);
769
+ }
770
+ /**
771
+ * Close all connection pools for a building/connection
772
+ */
773
+ export async function closeConnection(connectionId) {
774
+ // Close all pools that match this connection ID
775
+ for (const [key, pool] of mysqlPools.entries()) {
776
+ if (key.startsWith(connectionId + ':')) {
777
+ pool.end();
778
+ mysqlPools.delete(key);
779
+ }
780
+ }
781
+ for (const [key, pool] of pgPools.entries()) {
782
+ if (key.startsWith(connectionId + ':')) {
783
+ pool.end();
784
+ pgPools.delete(key);
785
+ }
786
+ }
787
+ for (const [key, pool] of oraclePools.entries()) {
788
+ if (key.startsWith(connectionId + ':')) {
789
+ await pool.close(0);
790
+ oraclePools.delete(key);
791
+ }
792
+ }
793
+ }
794
+ /**
795
+ * Close all connection pools
796
+ */
797
+ export async function closeAllConnections() {
798
+ for (const pool of mysqlPools.values()) {
799
+ await pool.end();
800
+ }
801
+ mysqlPools.clear();
802
+ for (const pool of pgPools.values()) {
803
+ await pool.end();
804
+ }
805
+ pgPools.clear();
806
+ for (const pool of oraclePools.values()) {
807
+ await pool.close(0);
808
+ }
809
+ oraclePools.clear();
810
+ }
811
+ /**
812
+ * Get human-readable field type name
813
+ */
814
+ function getFieldTypeName(typeId, engine) {
815
+ if (typeId === undefined)
816
+ return 'unknown';
817
+ if (engine === 'mysql') {
818
+ // MySQL field types
819
+ const mysqlTypes = {
820
+ 0: 'DECIMAL',
821
+ 1: 'TINYINT',
822
+ 2: 'SMALLINT',
823
+ 3: 'INT',
824
+ 4: 'FLOAT',
825
+ 5: 'DOUBLE',
826
+ 6: 'NULL',
827
+ 7: 'TIMESTAMP',
828
+ 8: 'BIGINT',
829
+ 9: 'MEDIUMINT',
830
+ 10: 'DATE',
831
+ 11: 'TIME',
832
+ 12: 'DATETIME',
833
+ 13: 'YEAR',
834
+ 14: 'NEWDATE',
835
+ 15: 'VARCHAR',
836
+ 16: 'BIT',
837
+ 245: 'JSON',
838
+ 246: 'NEWDECIMAL',
839
+ 247: 'ENUM',
840
+ 248: 'SET',
841
+ 249: 'TINY_BLOB',
842
+ 250: 'MEDIUM_BLOB',
843
+ 251: 'LONG_BLOB',
844
+ 252: 'BLOB',
845
+ 253: 'VAR_STRING',
846
+ 254: 'STRING',
847
+ 255: 'GEOMETRY',
848
+ };
849
+ return mysqlTypes[typeId] || `TYPE_${typeId}`;
850
+ }
851
+ else if (engine === 'postgresql') {
852
+ // PostgreSQL OID types
853
+ const pgTypes = {
854
+ 16: 'boolean',
855
+ 17: 'bytea',
856
+ 18: 'char',
857
+ 19: 'name',
858
+ 20: 'bigint',
859
+ 21: 'smallint',
860
+ 23: 'integer',
861
+ 25: 'text',
862
+ 26: 'oid',
863
+ 114: 'json',
864
+ 142: 'xml',
865
+ 700: 'real',
866
+ 701: 'double',
867
+ 790: 'money',
868
+ 1042: 'char',
869
+ 1043: 'varchar',
870
+ 1082: 'date',
871
+ 1083: 'time',
872
+ 1114: 'timestamp',
873
+ 1184: 'timestamptz',
874
+ 1186: 'interval',
875
+ 1266: 'timetz',
876
+ 1700: 'numeric',
877
+ 2950: 'uuid',
878
+ 3802: 'jsonb',
879
+ };
880
+ return pgTypes[typeId] || `OID_${typeId}`;
881
+ }
882
+ else if (engine === 'oracle') {
883
+ // Oracle DB_TYPE_* constants from oracledb
884
+ const oracleTypes = {
885
+ 2001: 'VARCHAR2',
886
+ 2002: 'NUMBER',
887
+ 2003: 'LONG',
888
+ 2004: 'DATE',
889
+ 2005: 'RAW',
890
+ 2006: 'LONG RAW',
891
+ 2007: 'ROWID',
892
+ 2010: 'BINARY_FLOAT',
893
+ 2011: 'BINARY_DOUBLE',
894
+ 2012: 'CHAR',
895
+ 2013: 'NCHAR',
896
+ 2014: 'NVARCHAR2',
897
+ 2015: 'CLOB',
898
+ 2016: 'NCLOB',
899
+ 2017: 'BLOB',
900
+ 2018: 'BFILE',
901
+ 2019: 'TIMESTAMP',
902
+ 2020: 'TIMESTAMP WITH TIME ZONE',
903
+ 2021: 'INTERVAL YEAR TO MONTH',
904
+ 2022: 'INTERVAL DAY TO SECOND',
905
+ 2023: 'TIMESTAMP WITH LOCAL TIME ZONE',
906
+ 2024: 'OBJECT',
907
+ 2025: 'CURSOR',
908
+ 2100: 'BOOLEAN',
909
+ 2101: 'JSON',
910
+ };
911
+ return oracleTypes[typeId] || `DBTYPE_${typeId}`;
912
+ }
913
+ return 'unknown';
914
+ }