@elyracode/db-tools 0.4.9 → 0.5.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.
- package/README.md +11 -1
- package/extensions/index.ts +158 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,17 @@ export ELYRA_CLICKHOUSE_TIMEOUT_QUERY=30000
|
|
|
61
61
|
|------|-------------|
|
|
62
62
|
| `query_mysql` | Execute SQL against MySQL. Results returned as JSON. |
|
|
63
63
|
| `query_clickhouse` | Execute SQL against ClickHouse. Optimized for analytical queries on large datasets. |
|
|
64
|
-
| `
|
|
64
|
+
| `query_sqlite` | Execute SQL against SQLite. Auto-detects Laravel SQLite databases from .env. |
|
|
65
|
+
| `get_database_schema` | Discover tables, columns, types, and indexes. Supports MySQL, ClickHouse, and SQLite. |
|
|
66
|
+
|
|
67
|
+
### SQLite
|
|
68
|
+
|
|
69
|
+
SQLite is auto-detected from your project:
|
|
70
|
+
1. If `.env` has `DB_CONNECTION=sqlite`, uses `DB_DATABASE` path
|
|
71
|
+
2. Falls back to `database/database.sqlite` (Laravel default)
|
|
72
|
+
3. Or pass an explicit path: the agent uses `db_path` parameter
|
|
73
|
+
|
|
74
|
+
No configuration needed for most Laravel projects.
|
|
65
75
|
|
|
66
76
|
## Security
|
|
67
77
|
|
package/extensions/index.ts
CHANGED
|
@@ -175,6 +175,76 @@ export default function (elyra: ExtensionAPI) {
|
|
|
175
175
|
},
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
// ── Tool: query_sqlite ──
|
|
179
|
+
elyra.registerTool({
|
|
180
|
+
name: "query_sqlite",
|
|
181
|
+
label: "Query SQLite",
|
|
182
|
+
description:
|
|
183
|
+
"Execute a SQL query against an SQLite database file. Returns results as JSON. " +
|
|
184
|
+
"Use get_database_schema first to understand the database structure. " +
|
|
185
|
+
"By default, only SELECT queries are allowed. " +
|
|
186
|
+
"Auto-detects Laravel SQLite databases from .env (DB_CONNECTION=sqlite).",
|
|
187
|
+
parameters: Type.Object({
|
|
188
|
+
sql: Type.String({ description: "The SQL query to execute" }),
|
|
189
|
+
db_path: Type.Optional(
|
|
190
|
+
Type.String({ description: "Path to the SQLite database file (default: auto-detect from .env or database/database.sqlite)" }),
|
|
191
|
+
),
|
|
192
|
+
}),
|
|
193
|
+
execute: async (_toolCallId, params) => {
|
|
194
|
+
const dbPath = resolveSqlitePath(params.db_path);
|
|
195
|
+
if (!dbPath) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: "SQLite database not found. Provide db_path or set DB_CONNECTION=sqlite in .env.",
|
|
200
|
+
}],
|
|
201
|
+
details: {},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!isReadOnly(params.sql) && !isWriteEnabled()) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `Blocked: Only SELECT queries are allowed in read-only mode. Set ELYRA_DB_ALLOW_WRITES=true to enable writes.\n\nQuery: ${params.sql}`,
|
|
210
|
+
}],
|
|
211
|
+
details: {},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const { execSync } = await import("node:child_process");
|
|
217
|
+
const result = execSync(
|
|
218
|
+
`sqlite3 -json "${dbPath}" "${params.sql.replace(/"/g, '\\"')}"`,
|
|
219
|
+
{ timeout: 30000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const trimmed = result.trim();
|
|
223
|
+
if (!trimmed) {
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text: "Query executed successfully (no results)." }],
|
|
226
|
+
details: { rowCount: 0 },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const truncated = trimmed.length > 50000
|
|
231
|
+
? `${trimmed.slice(0, 50000)}\n\n... (truncated, ${trimmed.length} chars total)`
|
|
232
|
+
: trimmed;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: truncated }],
|
|
236
|
+
details: {},
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: "text", text: `SQLite error: ${msg}` }],
|
|
242
|
+
details: {},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
178
248
|
// ── Tool: get_database_schema ──
|
|
179
249
|
elyra.registerTool({
|
|
180
250
|
name: "get_database_schema",
|
|
@@ -182,9 +252,9 @@ export default function (elyra: ExtensionAPI) {
|
|
|
182
252
|
description:
|
|
183
253
|
"Discover the database schema (tables, columns, types, indexes). " +
|
|
184
254
|
"Call this before writing queries to understand the database structure. " +
|
|
185
|
-
"Supports
|
|
255
|
+
"Supports MySQL, ClickHouse, and SQLite.",
|
|
186
256
|
parameters: Type.Object({
|
|
187
|
-
engine: Type.Union([Type.Literal("mysql"), Type.Literal("clickhouse")], {
|
|
257
|
+
engine: Type.Union([Type.Literal("mysql"), Type.Literal("clickhouse"), Type.Literal("sqlite")], {
|
|
188
258
|
description: "Which database engine to query",
|
|
189
259
|
}),
|
|
190
260
|
database: Type.Optional(
|
|
@@ -197,10 +267,13 @@ export default function (elyra: ExtensionAPI) {
|
|
|
197
267
|
}),
|
|
198
268
|
),
|
|
199
269
|
}),
|
|
200
|
-
execute: async (
|
|
270
|
+
execute: async (_toolCallId, params) => {
|
|
201
271
|
if (params.engine === "mysql") {
|
|
202
272
|
return getMysqlSchema(params.database, params.table);
|
|
203
273
|
}
|
|
274
|
+
if (params.engine === "sqlite") {
|
|
275
|
+
return getSqliteSchema(params.database, params.table);
|
|
276
|
+
}
|
|
204
277
|
return getClickHouseSchema(params.database, params.table);
|
|
205
278
|
},
|
|
206
279
|
});
|
|
@@ -480,3 +553,85 @@ async function getClickHouseSchema(
|
|
|
480
553
|
};
|
|
481
554
|
}
|
|
482
555
|
}
|
|
556
|
+
|
|
557
|
+
// ── SQLite Helpers ──
|
|
558
|
+
|
|
559
|
+
function resolveSqlitePath(explicitPath?: string): string | undefined {
|
|
560
|
+
if (explicitPath) {
|
|
561
|
+
const resolved = explicitPath.startsWith("/") ? explicitPath : join(process.cwd(), explicitPath);
|
|
562
|
+
return existsSync(resolved) ? resolved : undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Auto-detect from .env
|
|
566
|
+
const cwd = process.cwd();
|
|
567
|
+
const envPath = join(cwd, ".env");
|
|
568
|
+
if (existsSync(envPath)) {
|
|
569
|
+
const parsed = parseEnvFile(readFileSync(envPath, "utf-8"));
|
|
570
|
+
if (parsed.DB_CONNECTION === "sqlite") {
|
|
571
|
+
const dbDatabase = parsed.DB_DATABASE;
|
|
572
|
+
if (dbDatabase) {
|
|
573
|
+
const resolved = dbDatabase.startsWith("/") ? dbDatabase : join(cwd, dbDatabase);
|
|
574
|
+
if (existsSync(resolved)) return resolved;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Laravel default
|
|
580
|
+
const laravelDefault = join(cwd, "database", "database.sqlite");
|
|
581
|
+
if (existsSync(laravelDefault)) return laravelDefault;
|
|
582
|
+
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function getSqliteSchema(
|
|
587
|
+
dbPathOverride?: string,
|
|
588
|
+
table?: string,
|
|
589
|
+
): { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> } {
|
|
590
|
+
const dbPath = resolveSqlitePath(dbPathOverride);
|
|
591
|
+
if (!dbPath) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{ type: "text", text: "SQLite database not found." }],
|
|
594
|
+
details: {},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const { execSync } = require("node:child_process");
|
|
600
|
+
|
|
601
|
+
if (table) {
|
|
602
|
+
const columns = execSync(
|
|
603
|
+
`sqlite3 -json "${dbPath}" "PRAGMA table_info('${table}')"`,
|
|
604
|
+
{ timeout: 10000, encoding: "utf-8" },
|
|
605
|
+
);
|
|
606
|
+
const indexes = execSync(
|
|
607
|
+
`sqlite3 -json "${dbPath}" "PRAGMA index_list('${table}')"`,
|
|
608
|
+
{ timeout: 10000, encoding: "utf-8" },
|
|
609
|
+
);
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: `Table: ${table}\n\nColumns:\n${columns.trim()}\n\nIndexes:\n${indexes.trim()}`,
|
|
614
|
+
}],
|
|
615
|
+
details: {},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const tables = execSync(
|
|
620
|
+
`sqlite3 -json "${dbPath}" "SELECT name, type FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"`,
|
|
621
|
+
{ timeout: 10000, encoding: "utf-8" },
|
|
622
|
+
);
|
|
623
|
+
return {
|
|
624
|
+
content: [{
|
|
625
|
+
type: "text",
|
|
626
|
+
text: `Database: ${dbPath}\n\nTables:\n${tables.trim()}`,
|
|
627
|
+
}],
|
|
628
|
+
details: {},
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: "text", text: `SQLite schema error: ${msg}` }],
|
|
634
|
+
details: {},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|