diff --git a/main.ts b/main.ts index d96b246..cc3f918 100644 --- a/main.ts +++ b/main.ts @@ -13,7 +13,8 @@ import { import { DBService } from "./src/dbService"; import { inspectTableStructure, convertEntriesInNotes } from "./src/commands"; -import { pickTableName, processSqlBlock, processSqlChartBlock } from "./src/helpers"; +import { processSqlBlock, processSqlChartBlock } from "./src/codeblocks"; +import { pickTableName } from "./src/helpers"; import { SqliteDBSettings, DEFAULT_SETTINGS } from "./src/types"; export default class SqliteDBPlugin extends Plugin { diff --git a/src/codeblocks/index.ts b/src/codeblocks/index.ts new file mode 100644 index 0000000..38c67b6 --- /dev/null +++ b/src/codeblocks/index.ts @@ -0,0 +1,4 @@ +import { processSqlBlock } from "./processSqlBlock"; +import { processSqlChartBlock } from "./processSqlChartBlock"; + +export { processSqlBlock, processSqlChartBlock }; \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/helpers/buildSqlQuery.ts b/src/codeblocks/processSqlBlock/helpers/buildSqlQuery.ts new file mode 100644 index 0000000..d9f6b55 --- /dev/null +++ b/src/codeblocks/processSqlBlock/helpers/buildSqlQuery.ts @@ -0,0 +1,73 @@ +import { SqlParams } from "../types"; + +export function buildSqlQuery(params: SqlParams): { query: string; queryParams: any[] } { + const { + table, + columns, + keyColumn, + value, + dateColumn, + startDate, + endDate, + filterColumn, + filterValue, + orderBy, + orderDirection, + limit + } = params; + + const selectCols = columns + ? columns.split(",").map(c => c.trim()).join(", ") + : "*"; + + const queryParams: any[] = []; + const conditions: string[] = []; + + // Add key-value condition if provided + if (keyColumn && value !== undefined) { + conditions.push(`"${keyColumn}" = ?`); + queryParams.push(value); + } + + // Add date range conditions if provided + if (dateColumn) { + if (startDate) { + conditions.push(`"${dateColumn}" >= ?`); + queryParams.push(startDate); + } + if (endDate) { + conditions.push(`"${dateColumn}" <= ?`); + queryParams.push(endDate); + } + } + + // Add filter condition if provided + if (filterColumn && filterValue !== undefined) { + conditions.push(`"${filterColumn}" = ?`); + queryParams.push(filterValue); + } + + // Build the query + let query = `SELECT ${selectCols} FROM "${table}"`; + + // Add WHERE clause if there are conditions + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + // Add ORDER BY if specified + if (orderBy) { + query += ` ORDER BY "${orderBy}" ${orderDirection || 'ASC'}`; + } + + // Add LIMIT if specified + if (limit) { + query += ` LIMIT ?`; + queryParams.push(limit); + } + + return { + query: query + ";", + queryParams + }; +} \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/helpers/index.ts b/src/codeblocks/processSqlBlock/helpers/index.ts new file mode 100644 index 0000000..56335ac --- /dev/null +++ b/src/codeblocks/processSqlBlock/helpers/index.ts @@ -0,0 +1,6 @@ +import { parseSqlParams } from "./parseSqlParams"; +import { validateTable } from "./validateTable"; +import { buildSqlQuery } from "./buildSqlQuery"; +import { renderResults } from "./renderResults"; + +export { parseSqlParams, validateTable, buildSqlQuery, renderResults }; \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/helpers/parseSqlParams.ts b/src/codeblocks/processSqlBlock/helpers/parseSqlParams.ts new file mode 100644 index 0000000..f5acb88 --- /dev/null +++ b/src/codeblocks/processSqlBlock/helpers/parseSqlParams.ts @@ -0,0 +1,50 @@ +import { SqlParams } from "../types"; + +export function parseSqlParams(source: string): SqlParams | null { + const lines = source.split("\n"); + const params: Partial = {}; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + const parts = trimmed.split(":").map((p) => p.trim()); + if (parts.length >= 2) { + const key = parts[0]; + const val = parts.slice(1).join(":").trim(); // re-join in case there's a colon + + switch (key) { + case 'columns': + params.columns = val.split(',').map(c => c.trim()).join(', '); + break; + case 'limit': + const limitNum = parseInt(val); + if (!isNaN(limitNum)) params.limit = limitNum; + break; + case 'orderDirection': + if (val.toLowerCase() === 'asc' || val.toLowerCase() === 'desc') { + params.orderDirection = val.toLowerCase() as 'asc' | 'desc'; + } + break; + case 'table': + case 'keyColumn': + case 'value': + case 'dateColumn': + case 'startDate': + case 'endDate': + case 'filterColumn': + case 'filterValue': + case 'orderBy': + params[key] = val; + break; + } + } + } + + // Validate required parameters + if (!params.table) { + return null; + } + + return params as SqlParams; +} \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/helpers/renderResults.ts b/src/codeblocks/processSqlBlock/helpers/renderResults.ts new file mode 100644 index 0000000..56335c6 --- /dev/null +++ b/src/codeblocks/processSqlBlock/helpers/renderResults.ts @@ -0,0 +1,28 @@ +export function renderResults(result: { columns: string[], values: any[][] }, el: HTMLElement) { + const { columns, values } = result; + + // Create a container element for all rows. + const container = el.createEl("div"); + + values.forEach(rowValues => { + // Create a container for each row. + const rowContainer = container.createEl("div"); + + columns.forEach((col, index) => { + // Create a span for the column/value pair. + const pairSpan = rowContainer.createEl("span"); + + // Append the column name in bold. + pairSpan.createEl("strong", { text: col }); + + // Append the colon and the corresponding value. + pairSpan.createEl("span", { text: ": " + String(rowValues[index] ?? "") }); + + // Add a line break after each column/value pair. + rowContainer.createEl("br"); + }); + + // Optionally add an extra break between rows. + container.createEl("br"); + }); +} diff --git a/src/codeblocks/processSqlBlock/helpers/validateTable.ts b/src/codeblocks/processSqlBlock/helpers/validateTable.ts new file mode 100644 index 0000000..5af7941 --- /dev/null +++ b/src/codeblocks/processSqlBlock/helpers/validateTable.ts @@ -0,0 +1,52 @@ +import { SqlParams, ValidationError } from "../types"; + +export async function validateTable(db: any, params: SqlParams) { + // Get table info + const tableInfo = db.exec(`PRAGMA table_info("${params.table}")`); + if (!tableInfo || tableInfo.length === 0) { + return { + message: `Table "${params.table}" does not exist.` + }; + } + + const availableColumns = tableInfo[0].values.map((row: any) => row[1] as string); + + // Validate columns if specified + if (params.columns) { + const requestedColumns = params.columns.split(',').map(c => c.trim()); + for (const col of requestedColumns) { + if (!availableColumns.includes(col)) { + return { + message: `Column "${col}" does not exist in table "${params.table}".`, + availableColumns + }; + } + } + } + + // Validate dateColumn if specified + if (params.dateColumn && !availableColumns.includes(params.dateColumn)) { + return { + message: `Date column "${params.dateColumn}" does not exist in table "${params.table}".`, + availableColumns + }; + } + + // Validate filterColumn if specified + if (params.filterColumn && !availableColumns.includes(params.filterColumn)) { + return { + message: `Filter column "${params.filterColumn}" does not exist in table "${params.table}".`, + availableColumns + }; + } + + // Validate orderBy if specified + if (params.orderBy && !availableColumns.includes(params.orderBy)) { + return { + message: `Order by column "${params.orderBy}" does not exist in table "${params.table}".`, + availableColumns + }; + } + + return null; +} \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/index.ts b/src/codeblocks/processSqlBlock/index.ts new file mode 100644 index 0000000..31d46af --- /dev/null +++ b/src/codeblocks/processSqlBlock/index.ts @@ -0,0 +1,80 @@ +import { Notice } from "obsidian"; +import { DBService } from "../../dbService"; +import { parseSqlParams, validateTable, buildSqlQuery, renderResults } from "./helpers"; + +export async function processSqlBlock(dbService: DBService, source: string, el: HTMLElement) { + const db = dbService.getDB(); + if (!db) { + new Notice("No DB loaded. Please open the DB first."); + el.createEl("p", { text: "Database not loaded." }); + return; + } + + const params = parseSqlParams(source); + if (!params) { + el.createEl("p", { text: "Missing required parameter: table" }); + el.createEl("p", { text: "Example usage:" }); + el.createEl("pre", { + text: `table: tasks +columns: title, status, dueDate +dateColumn: dueDate +startDate: 2024-01-01 +endDate: 2024-12-31 +filterColumn: status +filterValue: active +limit: 10 +orderBy: dueDate +orderDirection: asc` + }); + return; + } + + try { + const validationError = await validateTable(db, params); + if (validationError) { + el.createEl("p", { text: validationError.message }); + if (validationError.availableColumns) { + el.createEl("p", { text: `Available columns are: ${validationError.availableColumns.join(", ")}` }); + } + return; + } + + const { query, queryParams } = buildSqlQuery(params); + const result = db.exec(query, queryParams); + + if (!result || result.length === 0 || result[0].values.length === 0) { + el.createEl("p", { + text: `No rows found in table "${params.table}"`, + }); + + // Show the applied filters to help debug + if (params.keyColumn && params.value) { + el.createEl("p", { text: `Filter: ${params.keyColumn} = ${params.value}` }); + } + if (params.dateColumn && (params.startDate || params.endDate)) { + el.createEl("p", { + text: `Date range: ${params.dateColumn} from ${params.startDate || 'start'} to ${params.endDate || 'end'}` + }); + } + if (params.filterColumn && params.filterValue) { + el.createEl("p", { text: `Additional filter: ${params.filterColumn} = ${params.filterValue}` }); + } + + el.createEl("p", { + text: "Try adjusting your filters or date range.", + }); + return; + } + + renderResults(result[0], el); + + } catch (error) { + el.createEl("p", { text: "An error occurred while processing the SQL block." }); + el.createEl("p", { text: String(error) }); + // Optionally show the query that caused the error in development + if (process.env.NODE_ENV === 'development') { + const { query, queryParams } = buildSqlQuery(params); + el.createEl("pre", { text: `Query: ${query}\nParams: ${JSON.stringify(queryParams)}` }); + } + } +} \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/types.ts b/src/codeblocks/processSqlBlock/types.ts new file mode 100644 index 0000000..be532c0 --- /dev/null +++ b/src/codeblocks/processSqlBlock/types.ts @@ -0,0 +1,20 @@ +export interface SqlParams { + table?: string; + keyColumn?: string; + value?: string; + columns?: string; + dateColumn?: string; + startDate?: string; + endDate?: string; + filterColumn?: string; + filterValue?: string; + orderBy?: string; + orderDirection?: string; + limit?: number; +} + +export interface ValidationError { + message: string; + availableColumns?: string[]; +} + diff --git a/src/helpers/processSqlChartBlock/charts/BarChart.ts b/src/codeblocks/processSqlChartBlock/charts/BarChart.ts similarity index 100% rename from src/helpers/processSqlChartBlock/charts/BarChart.ts rename to src/codeblocks/processSqlChartBlock/charts/BarChart.ts diff --git a/src/helpers/processSqlChartBlock/charts/LineChart.ts b/src/codeblocks/processSqlChartBlock/charts/LineChart.ts similarity index 100% rename from src/helpers/processSqlChartBlock/charts/LineChart.ts rename to src/codeblocks/processSqlChartBlock/charts/LineChart.ts diff --git a/src/helpers/processSqlChartBlock/charts/PieChart.ts b/src/codeblocks/processSqlChartBlock/charts/PieChart.ts similarity index 100% rename from src/helpers/processSqlChartBlock/charts/PieChart.ts rename to src/codeblocks/processSqlChartBlock/charts/PieChart.ts diff --git a/src/helpers/processSqlChartBlock/charts/index.ts b/src/codeblocks/processSqlChartBlock/charts/index.ts similarity index 95% rename from src/helpers/processSqlChartBlock/charts/index.ts rename to src/codeblocks/processSqlChartBlock/charts/index.ts index c21e693..06f17e0 100644 --- a/src/helpers/processSqlChartBlock/charts/index.ts +++ b/src/codeblocks/processSqlChartBlock/charts/index.ts @@ -12,7 +12,6 @@ export function createChart(type: ChartType, labels: string[], datasets: any[], return createBarChart(labels, datasets, options); case 'pie': return createPieChart(labels, datasets, options); - //todo add pie and others default: return createLineChart(labels, datasets); // fallback to line chart } diff --git a/src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts b/src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts rename to src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts diff --git a/src/helpers/processSqlChartBlock/helpers/getColorForIndex.ts b/src/codeblocks/processSqlChartBlock/helpers/getColorForIndex.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/getColorForIndex.ts rename to src/codeblocks/processSqlChartBlock/helpers/getColorForIndex.ts diff --git a/src/helpers/processSqlChartBlock/helpers/index.ts b/src/codeblocks/processSqlChartBlock/helpers/index.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/index.ts rename to src/codeblocks/processSqlChartBlock/helpers/index.ts diff --git a/src/helpers/processSqlChartBlock/helpers/parseChartParams.ts b/src/codeblocks/processSqlChartBlock/helpers/parseChartParams.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/parseChartParams.ts rename to src/codeblocks/processSqlChartBlock/helpers/parseChartParams.ts diff --git a/src/helpers/processSqlChartBlock/helpers/processChartData.ts b/src/codeblocks/processSqlChartBlock/helpers/processChartData.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/processChartData.ts rename to src/codeblocks/processSqlChartBlock/helpers/processChartData.ts diff --git a/src/helpers/processSqlChartBlock/helpers/validateColumns.ts b/src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts similarity index 100% rename from src/helpers/processSqlChartBlock/helpers/validateColumns.ts rename to src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts diff --git a/src/helpers/processSqlChartBlock/index.ts b/src/codeblocks/processSqlChartBlock/index.ts similarity index 100% rename from src/helpers/processSqlChartBlock/index.ts rename to src/codeblocks/processSqlChartBlock/index.ts diff --git a/src/helpers/index.ts b/src/helpers/index.ts index a27cbd4..a3074a1 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,5 +1,3 @@ import { pickTableName } from "./pickTableName"; -import { processSqlBlock } from "./processSqlBlock"; -import { processSqlChartBlock } from "./processSqlChartBlock/index"; -export { pickTableName, processSqlBlock, processSqlChartBlock }; +export { pickTableName }; diff --git a/src/helpers/processSqlBlock.ts b/src/helpers/processSqlBlock.ts deleted file mode 100644 index f250273..0000000 --- a/src/helpers/processSqlBlock.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Notice } from "obsidian"; -import { DBService } from "../dbService"; - -/** - * Processes a code block of the form: - * - * ```sqlite - * table: MyTable - * keyColumn: id - * value: 5 - * columns: col1, col2, col3 - * ``` - * - * - If `columns` is specified, we only select those columns. - * - Otherwise, we select all columns (*). - * - If multiple rows match, we create one table per row. - */ -export async function processSqlBlock(dbService: DBService, source: string, el: HTMLElement) { - const db = dbService.getDB(); - if (!db) { - new Notice("No DB loaded. Please open the DB first."); - el.createEl("p", { text: "Database not loaded." }); - return; - } - - // Parse code block params - const params = parseSqlParams(source); - const { table, keyColumn, value, columns } = params; - - if (!table || !keyColumn || !value) { - el.createEl("p", { text: "Missing required parameters (table, keyColumn, value)." }); - return; - } - - // Verify table exists - try { - const tableCheck = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name=?;`, [table]); - if (!tableCheck || tableCheck.length === 0 || tableCheck[0].values.length === 0) { - el.createEl("p", { text: `Table "${table}" does not exist in the database.` }); - return; - } - - // Get available columns for the table - const tableInfo = db.exec(`PRAGMA table_info("${table}");`); - const availableColumns = tableInfo[0].values.map((row: any[]) => row[1]); - - // Verify keyColumn exists - if (!availableColumns.includes(keyColumn)) { - el.createEl("p", { text: `Column "${keyColumn}" does not exist in table "${table}". Available columns are: ${availableColumns.join(", ")}` }); - return; - } - - // Build query based on columns - // If columns was provided, we split on commas, else use "*". - let selectCols = "*"; - if (columns) { - const requestedColumns = columns.split(",").map(c => c.trim()); - const invalidColumns = requestedColumns.filter(col => !availableColumns.includes(col)); - - if (invalidColumns.length > 0) { - el.createEl("p", { text: `The following columns do not exist in table "${table}": ${invalidColumns.join(", ")}` }); - el.createEl("p", { text: `Available columns are: ${availableColumns.join(", ")}` }); - return; - } - selectCols = requestedColumns.join(", "); - } - - const query = `SELECT ${selectCols} FROM "${table}" WHERE "${keyColumn}" = ?;`; - const result = db.exec(query, [value]); - - if (!result || result.length === 0 || result[0].values.length === 0) { - el.createEl("p", { - text: `No rows found in table "${table}" where ${keyColumn} = ${value}`, - }); - el.createEl("p", { - text: "Try checking the value or using a different key column.", - }); - return; - } - - // result[0] => { columns: string[], values: any[][] } - const rowObj = result[0]; - const columnsReturned = rowObj.columns; - const rows = rowObj.values; - - // For each row, create a separate table - rows.forEach((rowValues, rowIndex) => { - // Optional heading - el.createEl("h4", { text: `Row #${rowIndex + 1}` }); - - // Build the table for this row - const tableEl = el.createEl("table"); - columnsReturned.forEach((colName, colIndex) => { - const tr = tableEl.createEl("tr"); - tr.createEl("th", { text: colName }); - tr.createEl("td", { text: String(rowValues[colIndex] ?? "") }); - }); - - el.createEl("hr"); - }); - } catch (error) { - el.createEl("p", { text: "An error occurred while processing the SQL block." }); - el.createEl("p", { text: String(error) }); - } -} - -/** - * Parses the code block parameters line by line for: - * table: - * keyColumn: - * value: - * columns: (e.g. "col1, col2, col3") - */ -function parseSqlParams(source: string): { - table?: string; - keyColumn?: string; - value?: string; - columns?: string; -} { - const lines = source.split("\n"); - const params: any = {}; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - const parts = trimmed.split(":").map((p) => p.trim()); - if (parts.length >= 2) { - const key = parts[0]; - const val = parts.slice(1).join(":"); // re-join in case there's a colon - if (["table", "keyColumn", "value", "columns"].includes(key)) { - params[key] = val; - } - } - } - return params; -}