Added export to file button with CSV support.
This commit is contained in:
parent
3908515ca4
commit
408d5e415d
|
@ -3,6 +3,7 @@ package com.andrewlalis.perfin.control;
|
||||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
import com.andrewlalis.perfin.data.SavedQueryRepository;
|
import com.andrewlalis.perfin.data.SavedQueryRepository;
|
||||||
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
|
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
|
||||||
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -10,12 +11,17 @@ import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.StageStyle;
|
import javafx.stage.StageStyle;
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -42,12 +48,7 @@ public class SqlConsoleViewController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void executeQuery() {
|
@FXML public void executeQuery() {
|
||||||
String queryText = sqlEditorTextArea.getText().strip();
|
List<String> queries = getCurrentQueries();
|
||||||
String[] rawQueries = queryText.split("\\s*;\\s*");
|
|
||||||
List<String> queries = Arrays.stream(rawQueries)
|
|
||||||
.filter(s -> !s.isBlank())
|
|
||||||
.filter(s -> !s.startsWith("#") && !s.startsWith("//"))
|
|
||||||
.toList();
|
|
||||||
outputTextArea.clear();
|
outputTextArea.clear();
|
||||||
JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
|
JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
|
||||||
try (
|
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() {
|
private void refreshSavedQueries() {
|
||||||
savedQueriesVBox.getChildren().clear();
|
savedQueriesVBox.getChildren().clear();
|
||||||
List<String> savedQueries = Profile.getCurrent().dataSource()
|
List<String> savedQueries = Profile.getCurrent().dataSource()
|
||||||
|
@ -147,6 +213,15 @@ public class SqlConsoleViewController implements RouteSelectionListener {
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> 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() {
|
@FXML public void showSchema() {
|
||||||
SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
|
SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
|
@ -114,4 +114,10 @@ public class FileUtil {
|
||||||
in.transferTo(out);
|
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("\"", "\"\"") + '"';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
<Button text="Execute Query" onAction="#executeQuery"/>
|
<Button text="Execute Query" onAction="#executeQuery"/>
|
||||||
<Button text="View Schema" onAction="#showSchema"/>
|
<Button text="View Schema" onAction="#showSchema"/>
|
||||||
<Button text="Save Query" onAction="#saveQuery"/>
|
<Button text="Save Query" onAction="#saveQuery"/>
|
||||||
|
<Button text="Export to File" onAction="#exportToFile"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
<ScrollPane fitToHeight="true" fitToWidth="true">
|
<ScrollPane fitToHeight="true" fitToWidth="true">
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.andrewlalis.perfin.data.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class FileUtilTest {
|
||||||
|
@Test
|
||||||
|
public void testEscapeCSVText() {
|
||||||
|
assertEquals("regular_value", FileUtil.escapeCSVText("regular_value"));
|
||||||
|
assertEquals("\"\"\"\"", FileUtil.escapeCSVText("\""));
|
||||||
|
assertEquals("\"\"\",\"\"\"", FileUtil.escapeCSVText("\",\""));
|
||||||
|
assertEquals("\"and, this\"", FileUtil.escapeCSVText("and, this"));
|
||||||
|
assertEquals("\"1,345.43\"", FileUtil.escapeCSVText("1,345.43"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue