adapted sqlchart block to remote db

This commit is contained in:
stfrigerio 2025-04-07 10:30:29 +02:00
parent 466b239153
commit 6ca1207333
5 changed files with 222 additions and 138 deletions

View file

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

View file

@ -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 };
}
return { query, params };
}

View file

@ -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();

View file

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

View file

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