diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java
index b357d48..5573e61 100644
--- a/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java
+++ b/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java
@@ -41,13 +41,17 @@ import tech.ailef.dbadmin.external.dbmapping.DbField;
import tech.ailef.dbadmin.external.dbmapping.DbFieldValue;
import tech.ailef.dbadmin.external.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
+import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResult;
+import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResultRow;
import tech.ailef.dbadmin.external.dto.DataExportFormat;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils;
+import tech.ailef.dbadmin.internal.model.ConsoleQuery;
+import tech.ailef.dbadmin.internal.repository.ConsoleQueryRepository;
@Controller
-@RequestMapping(value = { "/${dbadmin.baseUrl}/export", "/${dbadmin.baseUrl}/export/" })
+@RequestMapping(value = { "/${dbadmin.baseUrl}/", "/${dbadmin.baseUrl}" })
public class DataExportController {
private static final Logger logger = LoggerFactory.getLogger(DataExportFormat.class);
@@ -57,10 +61,50 @@ public class DataExportController {
@Autowired
private DbAdminRepository repository;
+ @Autowired
+ private ConsoleQueryRepository queryRepository;
+
@Autowired
private ObjectMapper mapper;
- @GetMapping("/{className}")
+ @GetMapping("/console/export/{queryId}")
+ public ResponseEntity export(@PathVariable String queryId, @RequestParam String format,
+ @RequestParam MultiValueMap otherParams) {
+ ConsoleQuery query = queryRepository.findById(queryId).orElseThrow(() -> new DbAdminException("Query not found: " + queryId));
+
+ DataExportFormat exportFormat = null;
+ try {
+ exportFormat = DataExportFormat.valueOf(format.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new DbAdminException("Unsupported export format: " + format);
+ }
+
+ List fieldsToInclude = otherParams.getOrDefault("fields[]", new ArrayList<>());
+ DbQueryResult results = repository.executeQuery(query.getSql());
+
+ switch (exportFormat) {
+ case CSV:
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"export_" + query.getTitle().replaceAll("[^a-zA-Z0-9.-]", "_") + ".csv\"")
+ .body(toCsvQuery(results, fieldsToInclude).getBytes());
+ case XLSX:
+ String sheetName = query.getTitle();
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"export_" + query.getTitle().replaceAll("[^a-zA-Z0-9.-]", "_") + ".xlsx\"")
+ .body(toXlsxQuery(sheetName, results, fieldsToInclude));
+ case JSONL:
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"export_" + query.getTitle().replaceAll("[^a-zA-Z0-9.-]", "_") + ".jsonl\"")
+ .body(toJsonlQuery(results, fieldsToInclude).getBytes());
+ default:
+ throw new DbAdminException("Invalid DataExportFormat");
+ }
+ }
+
+ @GetMapping("/export/{className}")
@ResponseBody
public ResponseEntity export(@PathVariable String className, @RequestParam(required = false) String query,
@RequestParam String format, @RequestParam(required=false) Boolean raw,
@@ -149,6 +193,49 @@ public class DataExportController {
}
+ return fos.toByteArray();
+ }
+
+ private byte[] toXlsxQuery(String sheetName, DbQueryResult result, List fields) {
+ Workbook workbook = new XSSFWorkbook();
+
+ Sheet sheet = workbook.createSheet(sheetName);
+
+ CellStyle headerStyle = workbook.createCellStyle();
+ Font headerFont = workbook.createFont();
+ headerFont.setBold(true);
+ headerStyle.setFont(headerFont);
+
+ int rowIndex = 0;
+ Row headerRow = sheet.createRow(rowIndex++);
+ for (int i = 0; i < fields.size(); i++) {
+ Cell headerCell = headerRow.createCell(i);
+ headerCell.setCellValue(fields.get(i));
+ headerCell.setCellStyle(headerStyle);
+ }
+
+ for (DbQueryResultRow item : result.getRows()) {
+ Row row = sheet.createRow(rowIndex++);
+ int cellIndex = 0;
+
+ List record = getRecord(item, fields);
+
+ for (String value : record) {
+ Cell cell = row.createCell(cellIndex++);
+ cell.setCellValue(value);
+ }
+ }
+
+ ByteArrayOutputStream fos = new ByteArrayOutputStream();
+ try {
+ workbook.write(fos);
+ fos.close();
+ workbook.close();
+ } catch (IOException e) {
+ throw new DbAdminException("Error during serialization for XLSX workbook", e);
+ }
+
+
return fos.toByteArray();
}
@@ -181,6 +268,28 @@ public class DataExportController {
return sb.toString();
}
+ private String toJsonlQuery(DbQueryResult result, List fields) {
+ if (result.isEmpty())
+ return "";
+
+ StringBuilder sb = new StringBuilder();
+
+ for (DbQueryResultRow item : result.getRows()) {
+ Map map = item.toMap(fields);
+ try {
+ String json = mapper.writeValueAsString(map);
+ sb.append(json);
+ } catch (JsonProcessingException e) {
+ throw new DbAdminException(e);
+ }
+
+ sb.append("\n");
+ }
+
+ return sb.toString();
+
+ }
+
private String toCsv(List items, List fields, boolean raw) {
if (items.isEmpty())
return "";
@@ -203,6 +312,40 @@ public class DataExportController {
}
}
+ private String toCsvQuery(DbQueryResult result, List fields) {
+ if (result.isEmpty())
+ return "";
+
+ StringWriter sw = new StringWriter();
+
+ CSVFormat csvFormat =
+ CSVFormat.DEFAULT.builder()
+ .setHeader(fields.toArray(String[]::new))
+ .build();
+
+ try (final CSVPrinter printer = new CSVPrinter(sw, csvFormat)) {
+ for (DbQueryResultRow item : result.getRows()) {
+ printer.printRecord(getRecord(item, fields));
+ }
+
+ return sw.toString();
+ } catch (IOException e) {
+ throw new DbAdminException("Error during creation of CSV file", e);
+ }
+
+ }
+
+ private List getRecord(DbQueryResultRow row, List fields) {
+ List record = new ArrayList<>();
+
+ for (String field : fields) {
+ Object value = row.getFieldByName(field);
+ record.add(value == null ? null : value.toString());
+ }
+
+ return record;
+ }
+
/**
* Builds and returns a record (i.e a row) for a spreadsheet file (CSV or XLSX) as a list of Strings.
* Each column contains the value of a database column, potentially with some processing applied if
diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java
index 7d0fe15..66f60f1 100644
--- a/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java
+++ b/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java
@@ -19,7 +19,6 @@
package tech.ailef.dbadmin.external.controller;
-import java.sql.ResultSetMetaData;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -32,7 +31,6 @@ import java.util.stream.Collectors;
import org.hibernate.id.IdentifierGenerationException;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.UncategorizedSQLException;
@@ -59,9 +57,7 @@ import tech.ailef.dbadmin.external.DbAdminProperties;
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.external.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
-import tech.ailef.dbadmin.external.dbmapping.query.DbQueryOutputField;
import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResult;
-import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResultRow;
import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.dto.FacetedSearchRequest;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
@@ -101,9 +97,6 @@ public class DefaultDbAdminController {
@Autowired
private ConsoleQueryRepository consoleQueryRepository;
- @Autowired
- private JdbcTemplate jdbcTemplate;
-
@Autowired
private UserSettingsRepository userSettingsRepo;
@@ -589,8 +582,6 @@ public class DefaultDbAdminController {
return "redirect:/" + properties.getBaseUrl() + "/console";
}
-
-
@GetMapping("/console/run/{queryId}")
public String consoleRun(Model model, @RequestParam(required = false) String query,
@RequestParam(required = false) String queryTitle,
@@ -626,36 +617,7 @@ public class DefaultDbAdminController {
List tabs = consoleQueryRepository.findAll();
model.addAttribute("tabs", tabs);
- List results = new ArrayList<>();
- if (activeQuery.getSql() != null && !activeQuery.getSql().isBlank()) {
- try {
- results = jdbcTemplate.query(activeQuery.getSql(), (rs, rowNum) -> {
- Map result = new HashMap<>();
-
- ResultSetMetaData metaData = rs.getMetaData();
- int cols = metaData.getColumnCount();
-
- for (int i = 0; i < cols; i++) {
- Object o = rs.getObject(i + 1);
- String columnName = metaData.getColumnName(i + 1);
- String tableName = metaData.getTableName(i + 1);
- DbQueryOutputField field = new DbQueryOutputField(columnName, tableName, dbAdmin);
-
- result.put(field, o);
- }
-
- DbQueryResultRow row = new DbQueryResultRow(result, query);
-
- result.keySet().forEach(f -> {
- f.setResult(row);
- });
-
- return row;
- });
- } catch (DataAccessException e) {
- model.addAttribute("error", e.getMessage());
- }
- }
+ DbQueryResult results = repository.executeQuery(activeQuery.getSql());
if (!results.isEmpty()) {
int maxPage = (int)(Math.ceil ((double)results.size() / pageSize));
@@ -665,9 +627,9 @@ public class DefaultDbAdminController {
endOffset = Math.min(results.size(), endOffset);
- results = results.subList(startOffset, endOffset);
+ results.crop(startOffset, endOffset);
model.addAttribute("pagination", pagination);
- model.addAttribute("results", new DbQueryResult(results));
+ model.addAttribute("results", results);
}
model.addAttribute("title", "SQL Console | " + activeQuery.getTitle());
@@ -675,6 +637,7 @@ public class DefaultDbAdminController {
model.addAttribute("elapsedTime", new DecimalFormat("0.0#").format(elapsedTime));
return "console";
}
+
@GetMapping("/settings/appearance")
public String settingsAppearance(Model model) {
diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java
index c6948c0..0fe9b85 100644
--- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java
+++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java
@@ -19,7 +19,9 @@
package tech.ailef.dbadmin.external.dbmapping;
+import java.sql.ResultSetMetaData;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -27,10 +29,12 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
+import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -39,7 +43,11 @@ import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
+import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.annotations.ReadOnly;
+import tech.ailef.dbadmin.external.dbmapping.query.DbQueryOutputField;
+import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResult;
+import tech.ailef.dbadmin.external.dbmapping.query.DbQueryResultRow;
import tech.ailef.dbadmin.external.dto.FacetedSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo;
@@ -52,6 +60,12 @@ import tech.ailef.dbadmin.external.exceptions.InvalidPageException;
*/
@Component
public class DbAdminRepository {
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ private DbAdmin dbAdmin;
+
public DbAdminRepository() {
}
@@ -272,6 +286,39 @@ public class DbAdminRepository {
.toList();
}
+ /**
+ * Execute custom SQL query using jdbcTemplate
+ */
+ public DbQueryResult executeQuery(String sql) {
+ List results = new ArrayList<>();
+ if (sql != null && !sql.isBlank()) {
+ results = jdbcTemplate.query(sql, (rs, rowNum) -> {
+ Map result = new HashMap<>();
+
+ ResultSetMetaData metaData = rs.getMetaData();
+ int cols = metaData.getColumnCount();
+
+ for (int i = 0; i < cols; i++) {
+ Object o = rs.getObject(i + 1);
+ String columnName = metaData.getColumnName(i + 1);
+ String tableName = metaData.getTableName(i + 1);
+ DbQueryOutputField field = new DbQueryOutputField(columnName, tableName, dbAdmin);
+
+ result.put(field, o);
+ }
+
+ DbQueryResultRow row = new DbQueryResultRow(result, sql);
+
+ result.keySet().forEach(f -> {
+ f.setResult(row);
+ });
+
+ return row;
+ });
+ }
+ return new DbQueryResult(results);
+ }
+
/**
* Delete a specific object
* @param schema
diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryOutputField.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryOutputField.java
index cc4de2b..c38bab9 100644
--- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryOutputField.java
+++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryOutputField.java
@@ -64,6 +64,11 @@ public class DbQueryOutputField {
public boolean isForeignKey() {
return dbField != null && dbField.isForeignKey();
}
+
+ public boolean isExportable() {
+ if (dbField == null) return true;
+ return dbField.isExportable();
+ }
public Class> getConnectedType() {
if (dbField == null) return null;
diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResult.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResult.java
index b0982fa..c60797b 100644
--- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResult.java
+++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResult.java
@@ -29,4 +29,8 @@ public class DbQueryResult {
public int size() {
return rows.size();
}
+
+ public void crop(int startOffset, int endOffset) {
+ rows = rows.subList(startOffset, endOffset);
+ }
}
diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResultRow.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResultRow.java
index 1490b81..a1630cd 100644
--- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResultRow.java
+++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/query/DbQueryResultRow.java
@@ -1,8 +1,11 @@
package tech.ailef.dbadmin.external.dbmapping.query;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import tech.ailef.dbadmin.external.exceptions.DbAdminException;
+
public class DbQueryResultRow {
private Map values;
@@ -32,8 +35,24 @@ public class DbQueryResultRow {
public Object get(DbQueryOutputField field) {
return values.get(field);
}
+
+ public Object getFieldByName(String field) {
+ DbQueryOutputField key =
+ values.keySet().stream().filter(f -> f.getName().equals(field)).findFirst().orElse(null);
+ if (key == null) {
+ throw new DbAdminException("Field " + field + " not found");
+ }
+ return get(key);
+ }
-
+ public Map toMap(List fields) {
+ Map result = new HashMap<>();
+ for (String field : fields) {
+ result.put(field, getFieldByName(field));
+ }
+ return result;
+
+ }
}
diff --git a/src/main/resources/templates/console.html b/src/main/resources/templates/console.html
index beb009b..c5e62e0 100644
--- a/src/main/resources/templates/console.html
+++ b/src/main/resources/templates/console.html
@@ -3,6 +3,60 @@
+
+
+
+
@@ -92,6 +146,10 @@
Showing [[ ${results.size()} ]] of [[ ${pagination.getMaxElement()} ]]
results in [[ ${elapsedTime} ]] seconds
+
@@ -100,13 +158,17 @@
Showing [[ ${results.size()} ]] of [[ ${pagination.getMaxElement()} ]]
results in [[ ${elapsedTime} ]] seconds
+
-
-
+
+
@@ -163,9 +225,9 @@
+
-
diff --git a/src/main/resources/templates/fragments/generic_data_row.html b/src/main/resources/templates/fragments/generic_data_row.html
index 31ea17f..7f66b28 100644
--- a/src/main/resources/templates/fragments/generic_data_row.html
+++ b/src/main/resources/templates/fragments/generic_data_row.html
@@ -24,8 +24,6 @@
-