@elyracode/db-tools 0.4.9 → 0.5.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 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
- | `get_database_schema` | Discover tables, columns, types, and indexes. The agent calls this before writing queries. |
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
 
@@ -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 both MySQL and ClickHouse.",
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 (toolCallId, params, ctx) => {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyracode/db-tools",
3
- "version": "0.4.9",
3
+ "version": "0.5.1",
4
4
  "description": "Database tools for Elyra - query MySQL and ClickHouse with schema awareness and safety guardrails",
5
5
  "type": "module",
6
6
  "keywords": [