improving the sqlqueryblock

This commit is contained in:
stfrigerio 2025-02-04 17:08:15 +01:00
parent 904cacde9c
commit c8539fadaf
22 changed files with 316 additions and 141 deletions

View file

@ -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 {

4
src/codeblocks/index.ts Normal file
View file

@ -0,0 +1,4 @@
import { processSqlBlock } from "./processSqlBlock";
import { processSqlChartBlock } from "./processSqlChartBlock";
export { processSqlBlock, processSqlChartBlock };

View file

@ -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
};
}

View file

@ -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 };

View file

@ -0,0 +1,50 @@
import { SqlParams } from "../types";
export function parseSqlParams(source: string): SqlParams | null {
const lines = source.split("\n");
const params: Partial<SqlParams> = {};
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;
}

View file

@ -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");
});
}

View file

@ -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;
}

View file

@ -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)}` });
}
}
}

View file

@ -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[];
}

View file

@ -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
}

View file

@ -1,5 +1,3 @@
import { pickTableName } from "./pickTableName";
import { processSqlBlock } from "./processSqlBlock";
import { processSqlChartBlock } from "./processSqlChartBlock/index";
export { pickTableName, processSqlBlock, processSqlChartBlock };
export { pickTableName };

View file

@ -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: <string>
* keyColumn: <string>
* value: <string>
* columns: <string> (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;
}