diff --git a/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java index 20558f2..39cf77a 100644 --- a/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java @@ -3,6 +3,7 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.data.SavedQueryRepository; import com.andrewlalis.perfin.data.impl.JdbcDataSource; +import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.model.Profile; import javafx.fxml.FXML; import javafx.scene.Node; @@ -10,12 +11,17 @@ import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.StageStyle; import javafx.stage.Window; +import java.io.File; import java.io.IOException; +import java.io.PrintWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; @@ -42,12 +48,7 @@ public class SqlConsoleViewController implements RouteSelectionListener { } @FXML 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(); + List queries = getCurrentQueries(); outputTextArea.clear(); JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource(); try ( @@ -107,6 +108,71 @@ public class SqlConsoleViewController implements RouteSelectionListener { } } + @FXML public void exportToFile() { + if (sqlEditorTextArea.getText().isBlank()) { + Popups.message(sqlEditorTextArea, "Cannot export the results of an empty query."); + return; + } + if (getCurrentQueries().size() > 1) { + Popups.message(sqlEditorTextArea, "Note: Export to file will only export the results of your first query."); + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Export to File"); + fileChooser.setInitialFileName("export.csv"); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( + "CSV Files", ".csv" + )); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( + "JSON Files", ".json" + )); + fileChooser.setInitialDirectory(Profile.getCurrent().dataSource().getContentDir().toFile()); + File chosenFile = fileChooser.showSaveDialog(sqlEditorTextArea.getScene().getWindow()); + if (chosenFile == null) return; + + String name = chosenFile.getName().strip().toLowerCase(); + if (!name.endsWith(".csv") && !name.endsWith(".json")) { + Popups.error(sqlEditorTextArea, "Invalid file format. Only CSV and JSON are permitted."); + return; + } + String query = getCurrentQueries().getFirst(); + JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource(); + try ( + var conn = dataSource.getConnection(); + var stmt = conn.createStatement() + ) { + ResultSet rs = stmt.executeQuery(query); + if (name.endsWith(".csv")) { + writeQueryResultsToCsv(rs, chosenFile.toPath()); + } else if (name.endsWith(".json")) { +// writeQueryResultsToJson(rs, chosenFile.toPath()); + Popups.message(sqlEditorTextArea, "JSON not yet supported."); + } + } catch (Exception e) { + Popups.error(sqlEditorTextArea, e); + } + } + + private void writeQueryResultsToCsv(ResultSet rs, Path file) throws SQLException, IOException { + try (var out = Files.newOutputStream(file); var writer = new PrintWriter(out)) { + final int columnCount = rs.getMetaData().getColumnCount(); + // First write the header. + for (int i = 1; i <= columnCount; i++) { + writer.append(FileUtil.escapeCSVText(rs.getMetaData().getColumnLabel(i))); + if (i < columnCount) writer.append(','); + } + writer.println(); + // Then write the body rows. + while (rs.next()) { + for (int i = 1; i <= columnCount; i++) { + writer.append(FileUtil.escapeCSVText(rs.getString(i))); + if (i < columnCount) writer.append(','); + } + writer.println(); + } + } + } + private void refreshSavedQueries() { savedQueriesVBox.getChildren().clear(); List savedQueries = Profile.getCurrent().dataSource() @@ -147,6 +213,15 @@ public class SqlConsoleViewController implements RouteSelectionListener { return pane; } + private List getCurrentQueries() { + String queryText = sqlEditorTextArea.getText().strip(); + String[] rawQueries = queryText.split("\\s*;\\s*"); + return Arrays.stream(rawQueries) + .filter(s -> !s.isBlank()) + .filter(s -> !s.startsWith("#") && !s.startsWith("//")) + .toList(); + } + @FXML public void showSchema() { SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow()); dialog.show(); diff --git a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java index 09bb389..eb18598 100644 --- a/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java +++ b/src/main/java/com/andrewlalis/perfin/data/util/FileUtil.java @@ -114,4 +114,10 @@ public class FileUtil { in.transferTo(out); } } + + public static String escapeCSVText(String raw) { + if (raw == null) return "NULL"; + if (!raw.contains("\"") && !raw.contains(",") && !raw.contains(";")) return raw; + return '"' + raw.replaceAll("\"", "\"\"") + '"'; + } } diff --git a/src/main/resources/sql-console-view.fxml b/src/main/resources/sql-console-view.fxml index 6ecaf9c..3e75f02 100644 --- a/src/main/resources/sql-console-view.fxml +++ b/src/main/resources/sql-console-view.fxml @@ -21,6 +21,7 @@