From ea94f09702525f5cd4aba34730cb1282c5f1c275 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Tue, 9 Jul 2024 19:50:41 -0400 Subject: [PATCH] Added SQL Console View. --- .../com/andrewlalis/perfin/PerfinApp.java | 1 + .../perfin/control/MainViewController.java | 5 + .../control/SqlConsoleViewController.java | 110 ++++++++++++++++++ src/main/resources/main-view.fxml | 1 + src/main/resources/sql-console-view.fxml | 29 +++++ src/main/resources/sql/schema.sql | 11 +- 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java create mode 100644 src/main/resources/sql-console-view.fxml diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index 02aec48..30cf1c0 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -97,6 +97,7 @@ public class PerfinApp extends Application { router.map("categories", PerfinApp.class.getResource("/categories-view.fxml")); router.map("edit-category", PerfinApp.class.getResource("/edit-category.fxml")); router.map("tags", PerfinApp.class.getResource("/tags-view.fxml")); + router.map("sql-console", PerfinApp.class.getResource("/sql-console-view.fxml")); // Help pages. helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); diff --git a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java index 995d70f..797f998 100644 --- a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java @@ -4,6 +4,7 @@ import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.ProfilesStage; import com.andrewlalis.perfin.view.component.ScrollPaneRouterView; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -102,4 +103,8 @@ public class MainViewController { @FXML public void goToDashboard() { router.replace("dashboard"); } + + @FXML public void goToSqlConsole() { + router.replace("sql-console"); + } } diff --git a/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java new file mode 100644 index 0000000..312b6a4 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java @@ -0,0 +1,110 @@ +package com.andrewlalis.perfin.control; + +import com.andrewlalis.javafx_scene_router.RouteSelectionListener; +import com.andrewlalis.perfin.data.impl.JdbcDataSource; +import com.andrewlalis.perfin.model.Profile; +import javafx.fxml.FXML; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.TextArea; +import javafx.stage.Modality; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +/** + * Controller for the SQL Console View, in which the user can write and execute + * arbitrary SQL queries on the database. This allows power users to create + * custom analytics queries and get exactly the data they want, without fiddling + * with user-friendly search fields. + */ +public class SqlConsoleViewController implements RouteSelectionListener { + + @FXML public TextArea sqlEditorTextArea; + @FXML public TextArea outputTextArea; + + @Override + public void onRouteSelected(Object context) { + sqlEditorTextArea.clear(); + outputTextArea.clear(); + } + + public void executeQuery() { + String queryText = sqlEditorTextArea.getText().strip(); + String[] rawQueries = queryText.split("\\s*;\\s*"); + List queries = Arrays.stream(rawQueries) + .filter(s -> !s.isBlank()) + .filter(s -> !s.startsWith("#") && !s.startsWith("//")) + .toList(); + outputTextArea.clear(); + JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource(); + try ( + var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + StringBuilder sb = new StringBuilder(); + for (int queryIdx = 0; queryIdx < queries.size(); queryIdx++) { + sb.append("Query ").append(queryIdx + 1).append(" of ").append(queries.size()).append(":\n"); + String query = queries.get(queryIdx); + ResultSet rs = stmt.executeQuery(query); + int columnCount = rs.getMetaData().getColumnCount(); + + for (int i = 1; i <= columnCount; i++) { + sb.append(rs.getMetaData().getColumnLabel(i)); + if (i < columnCount) sb.append(", "); + } + sb.append('\n'); + while (rs.next()) { + for (int i = 1; i <= columnCount; i++) { + sb.append(rs.getString(i)); + if (i < columnCount) sb.append(", "); + } + sb.append('\n'); + } + if (queryIdx < queries.size() - 1) { + sb.append("-----\n\n"); + } + } + outputTextArea.setText(sb.toString()); + } catch (SQLException e) { + outputTextArea.setText("Error: " + e.getMessage()); + } + } + + @FXML public void showSchema() { + SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow()); + dialog.show(); + } + + private static class SchemaDialog extends Dialog { + public SchemaDialog(Window owner) { + DialogPane pane = new DialogPane(); + TextArea schemaTextArea = new TextArea(); + schemaTextArea.setEditable(false); + schemaTextArea.getStyleClass().addAll("mono-font", "small-font"); + try (var in = SqlConsoleViewController.class.getResourceAsStream("/sql/schema.sql")) { + if (in == null) throw new IOException("Could not load database schema from resource location."); + String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8); + schemaTextArea.setText(schemaStr); + } catch (IOException e) { + schemaTextArea.setText("Failed to load schema file!"); + } + pane.setContent(schemaTextArea); + pane.getButtonTypes().add(ButtonType.OK); + + initOwner(owner); + initModality(Modality.NONE); + initStyle(StageStyle.DECORATED); + setResizable(true); + setTitle("Perfin Database Schema"); + setDialogPane(pane); + } + } +} diff --git a/src/main/resources/main-view.fxml b/src/main/resources/main-view.fxml index 4e5de13..f0a9b7c 100644 --- a/src/main/resources/main-view.fxml +++ b/src/main/resources/main-view.fxml @@ -17,6 +17,7 @@