From 6ca1207333bb8a13bc1c421f5792cec273203170 Mon Sep 17 00:00:00 2001 From: stfrigerio Date: Mon, 7 Apr 2025 10:30:29 +0200 Subject: [PATCH] adapted sqlchart block to remote db --- src/DBService.ts | 2 +- .../helpers/buildSqlQuery.ts | 86 ++++++----- .../helpers/processChartData.ts | 60 +++++--- .../helpers/validateColumns.ts | 75 ++++++---- src/codeblocks/processSqlChartBlock/index.ts | 137 +++++++++++------- 5 files changed, 222 insertions(+), 138 deletions(-) diff --git a/src/DBService.ts b/src/DBService.ts index 23ef304..7eb5395 100644 --- a/src/DBService.ts +++ b/src/DBService.ts @@ -5,7 +5,7 @@ import { writeFile } from "fs/promises"; import { SQLiteDBSettings } from "./types"; export class DBService { - private mode: "local" | "remote" = "local"; + public mode: "local" | "remote" = "local"; private db: Database | null = null; private SQL: SqlJsStatic | null = null; // We still need to store the initialized library instance private app: App; diff --git a/src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts b/src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts index 884c2b8..70d6383 100644 --- a/src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts +++ b/src/codeblocks/processSqlChartBlock/helpers/buildSqlQuery.ts @@ -1,56 +1,72 @@ -import { ChartConfig } from "./parseChartParams"; +import { ChartConfig, PieChartConfig } from "./parseChartParams"; + +function isPieConfig(config: ChartConfig): config is PieChartConfig { + return config.chartType === "pie"; +} export function buildSqlQuery(config: ChartConfig) { - let query, params = []; - - switch (config.chartType) { - case 'pie': - query = ` + let query, params = []; + + // Check for special case: Time table and duration field + const isTimeDuration = + config.table.toLowerCase() === "time" && + config.chartType === "pie" && + config.valueColumn === "duration"; + + const valueExpr = isTimeDuration + ? `SUM(strftime('%s', '1970-01-01T' || ${config.valueColumn}) - strftime('%s', '1970-01-01T00:00:00'))` + : isPieConfig(config) + ? `SUM(${config.valueColumn})` + : ""; + + switch (config.chartType) { + case 'pie': + query = ` SELECT ${config.categoryColumn}, - SUM(${config.valueColumn}) as value + ${valueExpr} as value FROM "${config.table}" `; - - if (config.startDate && config.endDate) { - query += ` WHERE ${config.dateColumn} BETWEEN ? AND ?`; - params.push(config.startDate, config.endDate); - } - - query += ` GROUP BY ${config.categoryColumn}`; - query += ` ORDER BY value DESC`; - break; - case 'line': - case 'bar': - if (config.categoryColumn) { - query = ` + if (config.startDate && config.endDate) { + query += ` WHERE ${config.dateColumn} BETWEEN ? AND ?`; + params.push(config.startDate, config.endDate); + } + + query += ` GROUP BY ${config.categoryColumn}`; + query += ` ORDER BY value DESC`; + break; + + case 'line': + case 'bar': + if (config.categoryColumn) { + query = ` SELECT ${config.xColumn}, ${config.categoryColumn}, ${config.yColumns.join(', ')} FROM "${config.table}" `; - } else { - query = ` + } else { + query = ` SELECT ${config.xColumn}, ${config.yColumns.join(', ')} FROM "${config.table}" `; - } + } - if (config.startDate && config.endDate) { - query += ` WHERE ${config.dateColumn} BETWEEN ? AND ?`; - params.push(config.startDate, config.endDate); - } + if (config.startDate && config.endDate) { + query += ` WHERE ${config.dateColumn} BETWEEN ? AND ?`; + params.push(config.startDate, config.endDate); + } - query += ` ORDER BY ${config.xColumn}`; - if (config.categoryColumn) { - query += `, ${config.categoryColumn}`; - } - break; - } + query += ` ORDER BY ${config.xColumn}`; + if (config.categoryColumn) { + query += `, ${config.categoryColumn}`; + } + break; + } - return { query, params }; -} \ No newline at end of file + return { query, params }; +} diff --git a/src/codeblocks/processSqlChartBlock/helpers/processChartData.ts b/src/codeblocks/processSqlChartBlock/helpers/processChartData.ts index 47eeae1..3473b83 100644 --- a/src/codeblocks/processSqlChartBlock/helpers/processChartData.ts +++ b/src/codeblocks/processSqlChartBlock/helpers/processChartData.ts @@ -9,31 +9,47 @@ function isPieConfig(config: ChartConfig): config is PieChartConfig { return config.chartType === 'pie'; } -export function processChartData(rowObj: any, config: ChartConfig) { - if (isPieConfig(config)) { - const labels = rowObj.values.map((row: any[]) => row[0]); - const datasets = [{ - data: rowObj.values.map((row: any[]) => row[1]), - label: config.valueColumn - }]; - return { labels, datasets }; - } +function formatSeconds(totalSeconds: number): string { + const hrs = Math.floor(totalSeconds / 3600); + const mins = Math.floor((totalSeconds % 3600) / 60); + const secs = Math.floor(totalSeconds % 60); - let labels: any[]; - let datasets: any[]; - - if (config.categoryColumn) { - const { labels: groupLabels, datasets: groupDatasets } = processGroupedData(rowObj, config); - labels = groupLabels; - datasets = groupDatasets; - } else { - labels = rowObj.values.map((row: any[]) => row[0]); - datasets = processUngroupedData(rowObj, config); - } - - return { labels, datasets }; + const pad = (n: number) => String(n).padStart(2, '0'); + return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`; } +export function processChartData(rowObj: any, config: ChartConfig) { + if (isPieConfig(config)) { + const isDuration = config.table === 'Time' && config.valueColumn === 'duration'; + + const labels = rowObj.values.map((row: any[]) => { + const label = row[0]; + const value = row[1]; + return isDuration ? `${label} | ${formatSeconds(value)}` : label; + }); + + const datasets = [{ + data: rowObj.values.map((row: any[]) => row[1]), + label: config.valueColumn + }]; + + return { labels, datasets }; + } + + let labels: any[]; + let datasets: any[]; + + if (config.categoryColumn) { + const { labels: groupLabels, datasets: groupDatasets } = processGroupedData(rowObj, config); + labels = groupLabels; + datasets = groupDatasets; + } else { + labels = rowObj.values.map((row: any[]) => row[0]); + datasets = processUngroupedData(rowObj, config); + } + + return { labels, datasets }; +} function processGroupedData(rowObj: any, config: TimeSeriesChartConfig) { const groupedData = new Map(); diff --git a/src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts b/src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts index b0f4d1e..183a401 100644 --- a/src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts +++ b/src/codeblocks/processSqlChartBlock/helpers/validateColumns.ts @@ -1,33 +1,50 @@ import { ChartConfig } from "./parseChartParams"; +import { DBService } from "../../../DBService"; -export async function validateColumns(db: any, config: ChartConfig) { - const tableInfo = db.exec(`PRAGMA table_info("${config.table}");`); - const availableColumns = tableInfo[0].values.map((row: any[]) => row[1]); +export async function validateColumns(dbService: DBService, config: ChartConfig) { + const tableName = config.table; + + let columns: string[] = []; - let requestedColumns = []; - - if (config.chartType === 'pie') { - requestedColumns = [config.categoryColumn, config.valueColumn]; - if (config.dateColumn && (config.startDate || config.endDate)) { - requestedColumns.push(config.dateColumn); - } - } else { - requestedColumns = [config.xColumn, ...config.yColumns]; - if (config.categoryColumn) { - requestedColumns.push(config.categoryColumn); - } - if (config.dateColumn && (config.startDate || config.endDate)) { - requestedColumns.push(config.dateColumn); - } - } - - const invalidColumns = requestedColumns.filter(col => !availableColumns.includes(col)); - if (invalidColumns.length > 0) { - return { - message: `The following columns do not exist: ${invalidColumns.join(", ")}`, - availableColumns - }; - } + try { + const result = await dbService.getQuery<{ name: string }>( + `PRAGMA table_info("${tableName}")` + ); - return null; -} \ No newline at end of file + columns = result.map((row: { name: string }) => row.name); + } catch (error) { + console.error(`[validateColumns] Failed to fetch columns for table "${tableName}"`, error); + return { + message: `Unable to fetch column info for table "${tableName}".`, + availableColumns: [] + }; + } + + let requestedColumns: string[] = []; + + if (config.chartType === "pie") { + requestedColumns = [config.categoryColumn, config.valueColumn]; + if (config.dateColumn && (config.startDate || config.endDate)) { + requestedColumns.push(config.dateColumn); + } + } else { + requestedColumns = [config.xColumn, ...config.yColumns]; + if (config.categoryColumn) { + requestedColumns.push(config.categoryColumn); + } + if (config.dateColumn && (config.startDate || config.endDate)) { + requestedColumns.push(config.dateColumn); + } + } + + const invalidColumns = requestedColumns.filter(col => !columns.includes(col)); + + if (invalidColumns.length > 0) { + return { + message: `The following columns do not exist: ${invalidColumns.join(", ")}`, + availableColumns: columns + }; + } + + return null; +} diff --git a/src/codeblocks/processSqlChartBlock/index.ts b/src/codeblocks/processSqlChartBlock/index.ts index a2ceea8..b6d7651 100644 --- a/src/codeblocks/processSqlChartBlock/index.ts +++ b/src/codeblocks/processSqlChartBlock/index.ts @@ -1,63 +1,98 @@ -import { Notice } from "obsidian"; import { DBService } from "../../DBService"; import { parseChartParams, validateColumns, buildSqlQuery, processChartData } from "./helpers"; import { ChartType, createChart } from "./charts"; -import { pluginState } from "../../pluginState"; +import { pluginState, DATE_CHANGED_EVENT_NAME } from "../../pluginState"; +import { Notice } from "obsidian"; export async function processSqlChartBlock(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; - } + await renderChartBlock(); - // replace @date with the selected date - const injectedSource = source.replace(/@date/g, pluginState.selectedDate); - const config = parseChartParams(injectedSource); + const onDateChange = async (e: CustomEvent) => { + el.empty(); // Clear existing chart or messages + await renderChartBlock(); + }; - if (!config) { - // @ts-ignore - const message = config?.chartType === 'pie' - ? "Required parameters for pie chart are: table, categoryColumn, valueColumn" - : "Required parameters are: table, xColumn, yColumns"; - el.createEl("p", { text: message }); - return; - } + document.addEventListener(DATE_CHANGED_EVENT_NAME, onDateChange as EventListener); - try { - const validationError = await validateColumns(db, config); - if (validationError) { - el.createEl("p", { text: validationError.message }); - if (validationError.availableColumns) { - el.createEl("p", { text: `Available columns are: ${validationError.availableColumns.join(", ")}` }); + async function renderChartBlock() { + // replace @date with the selected date + const injectedSource = source.replace(/@date/g, pluginState.selectedDate); + const config = parseChartParams(injectedSource); + + if (!config) { + // @ts-ignore + const message = config?.chartType === 'pie' + ? "Required parameters for pie chart are: table, categoryColumn, valueColumn" + : "Required parameters are: table, xColumn, yColumns"; + el.createEl("p", { text: message }); + return; + } + + try { + const validationError = await validateColumns(dbService, config); + if (validationError) { + el.createEl("p", { text: validationError.message }); + if (validationError.availableColumns) { + el.createEl("p", { text: `Available columns are: ${validationError.availableColumns.join(", ")}` }); + } + return; } - return; + + const { query, params } = buildSqlQuery(config); + + let rows: any[]; + let labels, datasets; + + if (dbService.mode === "remote") { + // Use remote query + rows = await dbService.getQuery(query, params); + if (!rows || rows.length === 0) { + el.createEl("p", { text: "No data found for the specified parameters." }); + return; + } + + const columns = Object.keys(rows[0]); + const values = rows.map(obj => columns.map(col => obj[col])); + const execStyleResult = { columns, values }; + + ({ labels, datasets } = processChartData(execStyleResult, config)); + } else { + // Local mode: use exec + 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 result = db.exec(query, params); + if (!result || result.length === 0 || result[0].values.length === 0) { + el.createEl("p", { text: "No data found for the specified parameters." }); + return; + } + + const columns = result[0].columns; + const values = result[0].values; + rows = values.map(rowArr => + Object.fromEntries(rowArr.map((val, idx) => [columns[idx], val])) + ); + + ({ labels, datasets } = processChartData(rows, config)); + } + + const chartData = createChart( + config.chartType as ChartType, + labels, + datasets, + config.chartOptions + ); + + const wrapper = el.createDiv({ cls: 'obsidian-sql-chart' }); + // @ts-ignore + window.renderChart(chartData, wrapper); + } catch (error) { + el.createEl("p", { text: "An error occurred while processing the chart." }); + el.createEl("p", { text: String(error) }); + console.error(error); } - - const { query, params } = buildSqlQuery(config); - const result = db.exec(query, params); - - if (!result || result.length === 0 || result[0].values.length === 0) { - el.createEl("p", { text: "No data found for the specified parameters." }); - return; - } - - const { labels, datasets } = processChartData(result[0], config); - - const chartData = createChart( - config.chartType as ChartType, - labels, - datasets, - config.chartOptions - ); - - // @ts-ignore - window.renderChart(chartData, el); - - } catch (error) { - el.createEl("p", { text: "An error occurred while processing the chart." }); - el.createEl("p", { text: String(error) }); - console.error(error); } } \ No newline at end of file