@elyracode/db-tools 0.3.1
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 +62 -0
- package/extensions/index.ts +412 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @elyracode/db-tools
|
|
2
|
+
|
|
3
|
+
Database tools for Elyra -- query MySQL and ClickHouse with schema awareness and safety guardrails.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
elyra install npm:@elyracode/db-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Set environment variables for your database connections:
|
|
14
|
+
|
|
15
|
+
### MySQL
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
export ELYRA_MYSQL_HOST=localhost
|
|
19
|
+
export ELYRA_MYSQL_PORT=3306
|
|
20
|
+
export ELYRA_MYSQL_USER=readonly_user
|
|
21
|
+
export ELYRA_MYSQL_PASSWORD=secret
|
|
22
|
+
export ELYRA_MYSQL_DATABASE=myapp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### ClickHouse
|
|
26
|
+
```
|
|
27
|
+
export ELYRA_CLICKHOUSE_HOST=localhost
|
|
28
|
+
export ELYRA_CLICKHOUSE_PORT=8123
|
|
29
|
+
export ELYRA_CLICKHOUSE_DATABASE=mydb
|
|
30
|
+
export ELYRA_CLICKHOUSE_USERNAME=default
|
|
31
|
+
export ELYRA_CLICKHOUSE_PASSWORD=secret
|
|
32
|
+
export ELYRA_CLICKHOUSE_HTTPS=false
|
|
33
|
+
export ELYRA_CLICKHOUSE_TIMEOUT_CONNECT=10000
|
|
34
|
+
export ELYRA_CLICKHOUSE_TIMEOUT_QUERY=30000
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|------|-------------|
|
|
41
|
+
| `query_mysql` | Execute SQL against MySQL. Results returned as JSON. |
|
|
42
|
+
| `query_clickhouse` | Execute SQL against ClickHouse. Optimized for analytical queries on large datasets. |
|
|
43
|
+
| `get_database_schema` | Discover tables, columns, types, and indexes. The agent calls this before writing queries. |
|
|
44
|
+
|
|
45
|
+
## Security
|
|
46
|
+
|
|
47
|
+
- **Read-only by default**: Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries are allowed.
|
|
48
|
+
- **Write access**: Set `ELYRA_DB_ALLOW_WRITES=true` to enable INSERT, UPDATE, DELETE.
|
|
49
|
+
- **Recommendation**: Use a database user with minimal privileges (SELECT only).
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Once installed, just ask Elyra about your data:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
> How many users registered this week?
|
|
57
|
+
> Show me the top 10 products by revenue
|
|
58
|
+
> What's the average response time from the logs table?
|
|
59
|
+
> Describe the schema of the orders table
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The agent will automatically use `get_database_schema` to understand your database structure, then write and execute the appropriate SQL.
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@elyracode/coding-agent";
|
|
2
|
+
import { createClient } from "@clickhouse/client";
|
|
3
|
+
import mysql2 from "mysql2/promise";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
|
|
6
|
+
export default function (elyra: ExtensionAPI) {
|
|
7
|
+
// ── Configuration ──
|
|
8
|
+
// Connection details come from environment variables.
|
|
9
|
+
// MySQL: ELYRA_MYSQL_HOST, ELYRA_MYSQL_PORT, ELYRA_MYSQL_USER, ELYRA_MYSQL_PASSWORD, ELYRA_MYSQL_DATABASE
|
|
10
|
+
// ClickHouse: ELYRA_CLICKHOUSE_HOST, ELYRA_CLICKHOUSE_PORT, ELYRA_CLICKHOUSE_DATABASE,
|
|
11
|
+
// ELYRA_CLICKHOUSE_USERNAME, ELYRA_CLICKHOUSE_PASSWORD, ELYRA_CLICKHOUSE_HTTPS,
|
|
12
|
+
// ELYRA_CLICKHOUSE_TIMEOUT_CONNECT, ELYRA_CLICKHOUSE_TIMEOUT_QUERY
|
|
13
|
+
|
|
14
|
+
// ── Tool: query_mysql ──
|
|
15
|
+
elyra.registerTool({
|
|
16
|
+
name: "query_mysql",
|
|
17
|
+
label: "Query MySQL",
|
|
18
|
+
description:
|
|
19
|
+
"Execute a SQL query against the configured MySQL database. Returns results as JSON. " +
|
|
20
|
+
"Use get_database_schema first to understand the database structure. " +
|
|
21
|
+
"By default, only SELECT queries are allowed. The user must approve each query before execution.",
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
sql: Type.String({ description: "The SQL query to execute" }),
|
|
24
|
+
database: Type.Optional(
|
|
25
|
+
Type.String({ description: "Override the default database (optional)" }),
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
execute: async (toolCallId, params, ctx) => {
|
|
29
|
+
const config = getMysqlConfig(params.database);
|
|
30
|
+
if (!config) {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: "MySQL not configured. Set environment variables: ELYRA_MYSQL_HOST, ELYRA_MYSQL_USER, ELYRA_MYSQL_PASSWORD, ELYRA_MYSQL_DATABASE",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
details: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Security: block non-SELECT queries unless ELYRA_DB_ALLOW_WRITES=true
|
|
43
|
+
if (!isReadOnly(params.sql) && !isWriteEnabled()) {
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `Blocked: Only SELECT queries are allowed in read-only mode. Set ELYRA_DB_ALLOW_WRITES=true to enable writes.\n\nQuery: ${params.sql}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
details: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const connection = await mysql2.createConnection({
|
|
57
|
+
host: config.host,
|
|
58
|
+
port: config.port,
|
|
59
|
+
user: config.user,
|
|
60
|
+
password: config.password,
|
|
61
|
+
database: config.database,
|
|
62
|
+
connectTimeout: 10000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const [rows] = await connection.execute(params.sql);
|
|
67
|
+
const resultText = JSON.stringify(rows, null, 2);
|
|
68
|
+
const truncated =
|
|
69
|
+
resultText.length > 50000
|
|
70
|
+
? `${resultText.slice(0, 50000)}\n\n... (truncated, ${resultText.length} chars total)`
|
|
71
|
+
: resultText;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: truncated }],
|
|
75
|
+
details: { rowCount: Array.isArray(rows) ? rows.length : 0 },
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
await connection.end();
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: `MySQL error: ${msg}` }],
|
|
84
|
+
details: {},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── Tool: query_clickhouse ──
|
|
91
|
+
elyra.registerTool({
|
|
92
|
+
name: "query_clickhouse",
|
|
93
|
+
label: "Query ClickHouse",
|
|
94
|
+
description:
|
|
95
|
+
"Execute a SQL query against the configured ClickHouse database. Returns results as JSON. " +
|
|
96
|
+
"ClickHouse is optimized for analytical queries on large datasets. " +
|
|
97
|
+
"Use get_database_schema first to understand the database structure. The user must approve each query before execution.",
|
|
98
|
+
parameters: Type.Object({
|
|
99
|
+
sql: Type.String({ description: "The SQL query to execute" }),
|
|
100
|
+
database: Type.Optional(
|
|
101
|
+
Type.String({ description: "Override the default database (optional)" }),
|
|
102
|
+
),
|
|
103
|
+
}),
|
|
104
|
+
execute: async (toolCallId, params, ctx) => {
|
|
105
|
+
const config = getClickHouseConfig(params.database);
|
|
106
|
+
if (!config) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: "ClickHouse not configured. Set environment variables: ELYRA_CLICKHOUSE_HOST, ELYRA_CLICKHOUSE_DATABASE",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
details: {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Security: block non-SELECT queries unless writes enabled
|
|
119
|
+
if (!isReadOnly(params.sql) && !isWriteEnabled()) {
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: `Blocked: Only SELECT queries are allowed in read-only mode. Set ELYRA_DB_ALLOW_WRITES=true to enable writes.\n\nQuery: ${params.sql}`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
details: {},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const protocol = config.https ? "https" : "http";
|
|
133
|
+
const client = createClient({
|
|
134
|
+
url: `${protocol}://${config.host}:${config.port}`,
|
|
135
|
+
username: config.user,
|
|
136
|
+
password: config.password,
|
|
137
|
+
database: config.database,
|
|
138
|
+
connect_timeout: config.timeoutConnect,
|
|
139
|
+
request_timeout: config.timeoutQuery,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await client.query({
|
|
144
|
+
query: params.sql,
|
|
145
|
+
format: "JSONEachRow",
|
|
146
|
+
});
|
|
147
|
+
const rows = await result.json();
|
|
148
|
+
const resultText = JSON.stringify(rows, null, 2);
|
|
149
|
+
const truncated =
|
|
150
|
+
resultText.length > 50000
|
|
151
|
+
? `${resultText.slice(0, 50000)}\n\n... (truncated, ${resultText.length} chars total)`
|
|
152
|
+
: resultText;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: truncated }],
|
|
156
|
+
details: { rowCount: Array.isArray(rows) ? rows.length : 0 },
|
|
157
|
+
};
|
|
158
|
+
} finally {
|
|
159
|
+
await client.close();
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `ClickHouse error: ${msg}` }],
|
|
165
|
+
details: {},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Tool: get_database_schema ──
|
|
172
|
+
elyra.registerTool({
|
|
173
|
+
name: "get_database_schema",
|
|
174
|
+
label: "Get Database Schema",
|
|
175
|
+
description:
|
|
176
|
+
"Discover the database schema (tables, columns, types, indexes). " +
|
|
177
|
+
"Call this before writing queries to understand the database structure. " +
|
|
178
|
+
"Supports both MySQL and ClickHouse.",
|
|
179
|
+
parameters: Type.Object({
|
|
180
|
+
engine: Type.Union([Type.Literal("mysql"), Type.Literal("clickhouse")], {
|
|
181
|
+
description: "Which database engine to query",
|
|
182
|
+
}),
|
|
183
|
+
database: Type.Optional(
|
|
184
|
+
Type.String({ description: "Override the default database (optional)" }),
|
|
185
|
+
),
|
|
186
|
+
table: Type.Optional(
|
|
187
|
+
Type.String({
|
|
188
|
+
description:
|
|
189
|
+
"Get detailed schema for a specific table (optional, omit for overview of all tables)",
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
}),
|
|
193
|
+
execute: async (toolCallId, params, ctx) => {
|
|
194
|
+
if (params.engine === "mysql") {
|
|
195
|
+
return getMysqlSchema(params.database, params.table);
|
|
196
|
+
}
|
|
197
|
+
return getClickHouseSchema(params.database, params.table);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Config Helpers ──
|
|
203
|
+
|
|
204
|
+
interface MysqlConfig {
|
|
205
|
+
host: string;
|
|
206
|
+
port: number;
|
|
207
|
+
user: string;
|
|
208
|
+
password: string;
|
|
209
|
+
database: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface ClickHouseConfig {
|
|
213
|
+
host: string;
|
|
214
|
+
port: number;
|
|
215
|
+
https: boolean;
|
|
216
|
+
user: string;
|
|
217
|
+
password: string;
|
|
218
|
+
database: string;
|
|
219
|
+
timeoutConnect: number;
|
|
220
|
+
timeoutQuery: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getMysqlConfig(databaseOverride?: string): MysqlConfig | undefined {
|
|
224
|
+
const host = process.env.ELYRA_MYSQL_HOST;
|
|
225
|
+
const user = process.env.ELYRA_MYSQL_USER;
|
|
226
|
+
const password = process.env.ELYRA_MYSQL_PASSWORD;
|
|
227
|
+
const database = databaseOverride || process.env.ELYRA_MYSQL_DATABASE;
|
|
228
|
+
if (!host || !user || !database) return undefined;
|
|
229
|
+
return {
|
|
230
|
+
host,
|
|
231
|
+
port: Number.parseInt(process.env.ELYRA_MYSQL_PORT || "3306", 10),
|
|
232
|
+
user,
|
|
233
|
+
password: password || "",
|
|
234
|
+
database,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getClickHouseConfig(databaseOverride?: string): ClickHouseConfig | undefined {
|
|
239
|
+
const host = process.env.ELYRA_CLICKHOUSE_HOST;
|
|
240
|
+
const database = databaseOverride || process.env.ELYRA_CLICKHOUSE_DATABASE;
|
|
241
|
+
if (!host || !database) return undefined;
|
|
242
|
+
const port = Number.parseInt(process.env.ELYRA_CLICKHOUSE_PORT || "8123", 10);
|
|
243
|
+
const https = process.env.ELYRA_CLICKHOUSE_HTTPS === "true";
|
|
244
|
+
const timeoutConnect = Number.parseInt(process.env.ELYRA_CLICKHOUSE_TIMEOUT_CONNECT || "10000", 10);
|
|
245
|
+
const timeoutQuery = Number.parseInt(process.env.ELYRA_CLICKHOUSE_TIMEOUT_QUERY || "30000", 10);
|
|
246
|
+
return {
|
|
247
|
+
host,
|
|
248
|
+
port,
|
|
249
|
+
https,
|
|
250
|
+
user: process.env.ELYRA_CLICKHOUSE_USERNAME || "default",
|
|
251
|
+
password: process.env.ELYRA_CLICKHOUSE_PASSWORD || "",
|
|
252
|
+
database,
|
|
253
|
+
timeoutConnect,
|
|
254
|
+
timeoutQuery,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isWriteEnabled(): boolean {
|
|
259
|
+
const v = process.env.ELYRA_DB_ALLOW_WRITES;
|
|
260
|
+
return v === "true" || v === "1" || v === "yes";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isReadOnly(sql: string): boolean {
|
|
264
|
+
const trimmed = sql.trim().toUpperCase();
|
|
265
|
+
// Allow SELECT, SHOW, DESCRIBE, EXPLAIN, WITH (CTEs start with WITH...SELECT)
|
|
266
|
+
return /^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN|WITH)\b/.test(trimmed);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Schema Discovery ──
|
|
270
|
+
|
|
271
|
+
async function getMysqlSchema(
|
|
272
|
+
databaseOverride?: string,
|
|
273
|
+
table?: string,
|
|
274
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
275
|
+
const config = getMysqlConfig(databaseOverride);
|
|
276
|
+
if (!config) {
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: "text", text: "MySQL not configured." }],
|
|
279
|
+
details: {},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const connection = await mysql2.createConnection({
|
|
285
|
+
host: config.host,
|
|
286
|
+
port: config.port,
|
|
287
|
+
user: config.user,
|
|
288
|
+
password: config.password,
|
|
289
|
+
database: config.database,
|
|
290
|
+
connectTimeout: 10000,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
if (table) {
|
|
295
|
+
// Detailed schema for a specific table
|
|
296
|
+
const [columns] = await connection.execute(
|
|
297
|
+
`SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA
|
|
298
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
299
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
300
|
+
ORDER BY ORDINAL_POSITION`,
|
|
301
|
+
[config.database, table],
|
|
302
|
+
);
|
|
303
|
+
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: `Table: ${table}\n\nColumns:\n${JSON.stringify(columns, null, 2)}\n\nIndexes:\n${JSON.stringify(indexes, null, 2)}`,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
details: {},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Overview: all tables with row counts
|
|
316
|
+
const [tables] = await connection.execute(
|
|
317
|
+
`SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, TABLE_COMMENT
|
|
318
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
319
|
+
WHERE TABLE_SCHEMA = ?
|
|
320
|
+
ORDER BY TABLE_NAME`,
|
|
321
|
+
[config.database],
|
|
322
|
+
);
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: "text",
|
|
327
|
+
text: `Database: ${config.database}\n\nTables:\n${JSON.stringify(tables, null, 2)}`,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
details: { tableCount: Array.isArray(tables) ? tables.length : 0 },
|
|
331
|
+
};
|
|
332
|
+
} finally {
|
|
333
|
+
await connection.end();
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text", text: `MySQL schema error: ${msg}` }],
|
|
339
|
+
details: {},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function getClickHouseSchema(
|
|
345
|
+
databaseOverride?: string,
|
|
346
|
+
table?: string,
|
|
347
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
|
|
348
|
+
const config = getClickHouseConfig(databaseOverride);
|
|
349
|
+
if (!config) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: "ClickHouse not configured." }],
|
|
352
|
+
details: {},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const protocol = config.https ? "https" : "http";
|
|
358
|
+
const client = createClient({
|
|
359
|
+
url: `${protocol}://${config.host}:${config.port}`,
|
|
360
|
+
username: config.user,
|
|
361
|
+
password: config.password,
|
|
362
|
+
database: config.database,
|
|
363
|
+
connect_timeout: config.timeoutConnect,
|
|
364
|
+
request_timeout: config.timeoutQuery,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
if (table) {
|
|
369
|
+
const result = await client.query({
|
|
370
|
+
query: `DESCRIBE TABLE ${table}`,
|
|
371
|
+
format: "JSONEachRow",
|
|
372
|
+
});
|
|
373
|
+
const columns = await result.json();
|
|
374
|
+
return {
|
|
375
|
+
content: [
|
|
376
|
+
{
|
|
377
|
+
type: "text",
|
|
378
|
+
text: `Table: ${table}\n\nColumns:\n${JSON.stringify(columns, null, 2)}`,
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
details: {},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = await client.query({
|
|
386
|
+
query: `SELECT name, engine, total_rows, total_bytes
|
|
387
|
+
FROM system.tables
|
|
388
|
+
WHERE database = '${config.database}'
|
|
389
|
+
ORDER BY name`,
|
|
390
|
+
format: "JSONEachRow",
|
|
391
|
+
});
|
|
392
|
+
const tables = await result.json();
|
|
393
|
+
return {
|
|
394
|
+
content: [
|
|
395
|
+
{
|
|
396
|
+
type: "text",
|
|
397
|
+
text: `Database: ${config.database}\n\nTables:\n${JSON.stringify(tables, null, 2)}`,
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
details: { tableCount: Array.isArray(tables) ? tables.length : 0 },
|
|
401
|
+
};
|
|
402
|
+
} finally {
|
|
403
|
+
await client.close();
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: "text", text: `ClickHouse schema error: ${msg}` }],
|
|
409
|
+
details: {},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyracode/db-tools",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Database tools for Elyra - query MySQL and ClickHouse with schema awareness and safety guardrails",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["elyra-package", "database", "mysql", "clickhouse", "sql"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Knut W. Horne",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/kwhorne/elyra.git",
|
|
12
|
+
"directory": "packages/db-tools"
|
|
13
|
+
},
|
|
14
|
+
"elyra": {
|
|
15
|
+
"extensions": ["./extensions/index.ts"]
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"mysql2": "^3.14.0",
|
|
19
|
+
"@clickhouse/client": "^1.8.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@elyracode/coding-agent": "*",
|
|
23
|
+
"typebox": "*"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "echo 'nothing to clean'",
|
|
27
|
+
"build": "echo 'nothing to build'",
|
|
28
|
+
"check": "echo 'nothing to check'"
|
|
29
|
+
}
|
|
30
|
+
}
|