Punching screen controller created - migrated initview and addcase endpoints

This commit is contained in:
2026-08-01 19:16:24 +05:30
parent 0e5de99f55
commit 1adcc04efc
109 changed files with 2918 additions and 661 deletions

View File

@@ -0,0 +1,39 @@
# Cygnus On-Premises Database Core
This module executes cloud-supplied, parameterized query definitions against the
on-premises JDBC `DataSource` without creating delimiter strings or `String[][]`
result buffers.
## Query format
The query catalog value retains the operation prefix during incremental migration:
```text
select!C0L!select id, name from operation where operation_type = ? and operated_by = ?
```
Legacy `val0`, `val1`, ... placeholders are rejected by the modern executor. They
remain available through the existing `DBFunctions.FetchRunQuery` path until each
query is migrated.
## Usage
```java
List<Operation> rows = dbFunctions.query(
600,
new Object[] { operationType, operatedBy },
Operation.class);
```
For maximum throughput, use an explicit mapper:
```java
List<Operation> rows = dbFunctions.query(
600,
parameters,
result -> new Operation(result.getLong("id"), result.getString("name")));
```
Large reports can use `stream(...)` so rows are consumed without retaining the
whole result in memory. PostgreSQL cursor fetching is enabled by running reads with
auto-commit disabled and the configured fetch size.

38
cygnus-onprem-db/pom.xml Normal file
View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cygnus</groupId>
<artifactId>cygnus-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cygnus-onprem-db</artifactId>
<packaging>jar</packaging>
<name>Cygnus On-Premises Database Core</name>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,21 @@
package com.cygnus.db;
import java.util.List;
import java.util.Optional;
public interface CygnusDbExecutor {
<T> DbResult<T> execute(int queryId, Object[] parameters, Class<T> responseType);
DbResult<RowView> execute(int queryId, Object[] parameters);
<T> List<T> query(int queryId, Object[] parameters, Class<T> responseType);
<T> List<T> query(int queryId, Object[] parameters, RowMapper<T> mapper);
List<RowView> query(int queryId, Object[] parameters);
<T> Optional<T> queryOne(int queryId, Object[] parameters, Class<T> responseType);
int insert(int queryId, Object[] parameters);
<K> K insert(int queryId, Object[] parameters, Class<K> generatedKeyType);
int update(int queryId, Object[] parameters);
int delete(int queryId, Object[] parameters);
<T> T procedure(int queryId, Object[] parameters, Class<T> responseType);
<T> List<T> procedureRows(int queryId, Object[] parameters, Class<T> responseType);
<T> void stream(int queryId, Object[] parameters, RowMapper<T> mapper, RowConsumer<T> consumer);
<T> T transaction(TransactionCallback<T> callback);
}

View File

@@ -0,0 +1,12 @@
package com.cygnus.db;
public final class DatabaseExecutionException extends RuntimeException {
private final int queryId;
public DatabaseExecutionException(int queryId, String message, Throwable cause) {
super(message, cause);
this.queryId = queryId;
}
public int queryId() { return queryId; }
}

View File

@@ -0,0 +1,20 @@
package com.cygnus.db;
import java.util.List;
import java.util.Optional;
public record DbResult<T>(QueryType type, List<T> rows, int affectedRows, T value) {
public DbResult {
rows = rows == null ? List.of() : List.copyOf(rows);
}
public Optional<T> optionalValue() { return Optional.ofNullable(value); }
public static <T> DbResult<T> rows(QueryType type, List<T> rows) {
return new DbResult<>(type, rows, 0, rows.isEmpty() ? null : rows.getFirst());
}
public static <T> DbResult<T> affected(QueryType type, int count) {
return new DbResult<>(type, List.of(), count, null);
}
}

View File

@@ -0,0 +1,10 @@
package com.cygnus.db;
public record ExecutorOptions(int fetchSize, int queryTimeoutSeconds) {
public static final ExecutorOptions DEFAULT = new ExecutorOptions(250, 60);
public ExecutorOptions {
if (fetchSize < 0) throw new IllegalArgumentException("fetchSize cannot be negative");
if (queryTimeoutSeconds < 0) throw new IllegalArgumentException("queryTimeoutSeconds cannot be negative");
}
}

View File

@@ -0,0 +1,33 @@
package com.cygnus.db;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
final class ImmutableRowView implements RowView {
private final Map<String, Object> values;
private final Map<String, Object> normalized;
ImmutableRowView(Map<String, Object> values) {
this.values = Collections.unmodifiableMap(new LinkedHashMap<>(values));
Map<String, Object> index = new LinkedHashMap<>();
values.forEach((name, value) -> index.put(name.toLowerCase(Locale.ROOT), value));
this.normalized = Map.copyOf(index);
}
@Override
public Object get(String column) {
Object value = values.get(column);
return value != null || values.containsKey(column)
? value : normalized.get(column.toLowerCase(Locale.ROOT));
}
@Override
public <T> T get(String column, Class<T> type) {
return ValueConverter.convert(get(column), type);
}
@Override
public Map<String, Object> asMap() { return values; }
}

View File

@@ -0,0 +1,316 @@
package com.cygnus.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.sql.DataSource;
public final class JdbcCygnusDbExecutor implements CygnusDbExecutor {
private final DataSource dataSource;
private final QueryDefinitionProvider queryProvider;
private final ExecutorOptions options;
private final ParameterBinder binder = new ParameterBinder();
private final ObjectRowMapperFactory mapperFactory = new ObjectRowMapperFactory();
public JdbcCygnusDbExecutor(DataSource dataSource, QueryDefinitionProvider queryProvider) {
this(dataSource, queryProvider, ExecutorOptions.DEFAULT);
}
public JdbcCygnusDbExecutor(
DataSource dataSource, QueryDefinitionProvider queryProvider, ExecutorOptions options) {
this.dataSource = java.util.Objects.requireNonNull(dataSource, "dataSource");
this.queryProvider = java.util.Objects.requireNonNull(queryProvider, "queryProvider");
this.options = java.util.Objects.requireNonNull(options, "options");
}
@Override
public <T> DbResult<T> execute(int queryId, Object[] parameters, Class<T> responseType) {
QueryDefinition definition = queryProvider.get(queryId);
if (definition.type() == QueryType.SELECT) {
return withReadConnection(queryId, connection ->
executeRowsOrCount(connection, definition, parameters, responseType));
}
if (definition.type() == QueryType.PROCEDURE) {
return executeRowsOrCount(definition, parameters, responseType);
}
return DbResult.affected(definition.type(), executeUpdate(definition, parameters));
}
@Override
public DbResult<RowView> execute(int queryId, Object[] parameters) {
return execute(queryId, parameters, RowView.class);
}
@Override
public <T> List<T> query(int queryId, Object[] parameters, Class<T> responseType) {
return withReadConnection(queryId, connection -> query(
connection, require(queryId, QueryType.SELECT), parameters, responseType));
}
@Override
public <T> List<T> query(int queryId, Object[] parameters, RowMapper<T> mapper) {
return withReadConnection(queryId, connection -> query(
connection, require(queryId, QueryType.SELECT), parameters, mapper));
}
@Override
public List<RowView> query(int queryId, Object[] parameters) {
return query(queryId, parameters, RowView.class);
}
@Override
public <T> Optional<T> queryOne(int queryId, Object[] parameters, Class<T> responseType) {
List<T> rows = query(queryId, parameters, responseType);
if (rows.size() > 1) throw new DatabaseExecutionException(
queryId, "Expected one row but received " + rows.size(), null);
return rows.stream().findFirst();
}
@Override
public int insert(int queryId, Object[] parameters) {
return executeUpdate(require(queryId, QueryType.INSERT), parameters);
}
@Override
public <K> K insert(int queryId, Object[] parameters, Class<K> generatedKeyType) {
QueryDefinition definition = require(queryId, QueryType.INSERT);
return withConnection(queryId, connection -> {
try (PreparedStatement statement = prepare(
connection, definition, Statement.RETURN_GENERATED_KEYS)) {
binder.bind(statement, parameters);
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (!keys.next()) throw new DatabaseExecutionException(
queryId, "Insert did not return a generated key", null);
return ValueConverter.convert(keys.getObject(1), generatedKeyType);
}
}
});
}
@Override
public int update(int queryId, Object[] parameters) {
return executeUpdate(require(queryId, QueryType.UPDATE), parameters);
}
@Override
public int delete(int queryId, Object[] parameters) {
return executeUpdate(require(queryId, QueryType.DELETE), parameters);
}
@Override
public <T> T procedure(int queryId, Object[] parameters, Class<T> responseType) {
List<T> rows = procedureRows(queryId, parameters, responseType);
return rows.isEmpty() ? null : rows.getFirst();
}
@Override
public <T> List<T> procedureRows(int queryId, Object[] parameters, Class<T> responseType) {
DbResult<T> result = executeRowsOrCount(
require(queryId, QueryType.PROCEDURE), parameters, responseType);
return result.rows();
}
@Override
public <T> void stream(
int queryId, Object[] parameters, RowMapper<T> mapper, RowConsumer<T> consumer) {
QueryDefinition definition = require(queryId, QueryType.SELECT);
withReadConnection(queryId, connection -> {
try (PreparedStatement statement = prepare(connection, definition);
ResultSet result = executeQuery(statement, parameters)) {
while (result.next()) consumer.accept(mapper.map(result));
return null;
}
});
}
@Override
public <T> T transaction(TransactionCallback<T> callback) {
try (Connection connection = dataSource.getConnection()) {
boolean autoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
T result = callback.execute(new TransactionOperationsImpl(connection));
connection.commit();
return result;
} catch (Exception exception) {
try { connection.rollback(); } catch (SQLException rollback) { exception.addSuppressed(rollback); }
if (exception instanceof RuntimeException runtime) throw runtime;
throw new DatabaseExecutionException(0, "Transaction failed", exception);
} finally {
connection.setAutoCommit(autoCommit);
}
} catch (SQLException exception) {
throw new DatabaseExecutionException(0, "Unable to manage transaction", exception);
}
}
private <T> DbResult<T> executeRowsOrCount(
QueryDefinition definition, Object[] parameters, Class<T> responseType) {
return withConnection(definition.queryId(), connection ->
executeRowsOrCount(connection, definition, parameters, responseType));
}
private <T> DbResult<T> executeRowsOrCount(
Connection connection,
QueryDefinition definition,
Object[] parameters,
Class<T> responseType) throws SQLException {
try (PreparedStatement statement = prepare(connection, definition)) {
binder.bind(statement, parameters);
boolean rows = statement.execute();
if (!rows) return DbResult.affected(definition.type(), statement.getUpdateCount());
try (ResultSet result = statement.getResultSet()) {
return DbResult.rows(definition.type(), map(result, responseType));
}
}
}
private int executeUpdate(QueryDefinition definition, Object[] parameters) {
return withConnection(definition.queryId(), connection ->
executeUpdate(connection, definition, parameters));
}
private int executeUpdate(
Connection connection, QueryDefinition definition, Object[] parameters) throws SQLException {
try (PreparedStatement statement = prepare(connection, definition)) {
binder.bind(statement, parameters);
return statement.executeUpdate();
}
}
private <T> List<T> query(
Connection connection,
QueryDefinition definition,
Object[] parameters,
Class<T> responseType) throws SQLException {
try (PreparedStatement statement = prepare(connection, definition);
ResultSet result = executeQuery(statement, parameters)) {
return map(result, responseType);
}
}
private <T> List<T> query(
Connection connection,
QueryDefinition definition,
Object[] parameters,
RowMapper<T> mapper) throws SQLException {
try (PreparedStatement statement = prepare(connection, definition);
ResultSet result = executeQuery(statement, parameters)) {
List<T> rows = new ArrayList<>();
while (result.next()) rows.add(mapper.map(result));
return List.copyOf(rows);
}
}
private <T> List<T> map(ResultSet result, Class<T> responseType) throws SQLException {
RowMapper<T> mapper = mapperFactory.create(responseType, result.getMetaData());
List<T> rows = new ArrayList<>();
while (result.next()) rows.add(mapper.map(result));
return List.copyOf(rows);
}
private ResultSet executeQuery(PreparedStatement statement, Object[] parameters) throws SQLException {
binder.bind(statement, parameters);
return statement.executeQuery();
}
private PreparedStatement prepare(Connection connection, QueryDefinition definition) throws SQLException {
PreparedStatement statement = connection.prepareStatement(
definition.sql(), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
configure(statement);
return statement;
}
private PreparedStatement prepare(
Connection connection, QueryDefinition definition, int generatedKeys) throws SQLException {
PreparedStatement statement = connection.prepareStatement(definition.sql(), generatedKeys);
configure(statement);
return statement;
}
private void configure(PreparedStatement statement) throws SQLException {
statement.setFetchSize(options.fetchSize());
statement.setQueryTimeout(options.queryTimeoutSeconds());
}
private QueryDefinition require(int queryId, QueryType expected) {
QueryDefinition definition = queryProvider.get(queryId);
if (definition.type() != expected) throw new QueryDefinitionException(
"Query " + queryId + " is " + definition.type() + ", not " + expected);
return definition;
}
private <T> T withConnection(int queryId, SqlWork<T> work) {
try (Connection connection = dataSource.getConnection()) {
return work.execute(connection);
} catch (DatabaseExecutionException exception) {
throw exception;
} catch (Exception exception) {
throw new DatabaseExecutionException(queryId, "Database operation failed for query " + queryId, exception);
}
}
private <T> T withReadConnection(int queryId, SqlWork<T> work) {
try (Connection connection = dataSource.getConnection()) {
boolean autoCommit = connection.getAutoCommit();
connection.setAutoCommit(false); // PostgreSQL requires this for cursor-based fetchSize.
try {
T value = work.execute(connection);
connection.commit();
return value;
} catch (Exception exception) {
try { connection.rollback(); } catch (SQLException rollback) { exception.addSuppressed(rollback); }
if (exception instanceof RuntimeException runtime) throw runtime;
throw new DatabaseExecutionException(queryId,
"Database read failed for query " + queryId, exception);
} finally {
connection.setAutoCommit(autoCommit);
}
} catch (SQLException exception) {
throw new DatabaseExecutionException(queryId,
"Unable to manage database read for query " + queryId, exception);
}
}
@FunctionalInterface
private interface SqlWork<T> { T execute(Connection connection) throws Exception; }
private final class TransactionOperationsImpl implements TransactionOperations {
private final Connection connection;
private TransactionOperationsImpl(Connection connection) { this.connection = connection; }
@Override
public <T> List<T> query(int queryId, Object[] parameters, Class<T> type) {
try { return JdbcCygnusDbExecutor.this.query(
connection, require(queryId, QueryType.SELECT), parameters, type); }
catch (SQLException exception) { throw failure(queryId, exception); }
}
@Override
public <T> Optional<T> queryOne(int queryId, Object[] parameters, Class<T> type) {
List<T> rows = query(queryId, parameters, type);
if (rows.size() > 1) throw new DatabaseExecutionException(
queryId, "Expected one row but received " + rows.size(), null);
return rows.stream().findFirst();
}
@Override public int insert(int id, Object[] values) { return updateType(id, values, QueryType.INSERT); }
@Override public int update(int id, Object[] values) { return updateType(id, values, QueryType.UPDATE); }
@Override public int delete(int id, Object[] values) { return updateType(id, values, QueryType.DELETE); }
private int updateType(int id, Object[] values, QueryType type) {
try { return executeUpdate(connection, require(id, type), values); }
catch (SQLException exception) { throw failure(id, exception); }
}
private DatabaseExecutionException failure(int id, SQLException exception) {
return new DatabaseExecutionException(id, "Transaction operation failed for query " + id, exception);
}
}
}

View File

@@ -0,0 +1,141 @@
package com.cygnus.db;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
final class ObjectRowMapperFactory {
private final ConcurrentMap<Class<?>, TypeMetadata> metadataCache = new ConcurrentHashMap<>();
<T> RowMapper<T> create(Class<T> type, ResultSetMetaData resultMetadata) throws SQLException {
if (type == RowView.class) return result -> type.cast(rowView(result, resultMetadata));
if (isScalar(type)) return result -> ValueConverter.convert(result.getObject(1), type);
TypeMetadata typeMetadata = metadataCache.computeIfAbsent(type, this::inspect);
Map<String, Integer> columns = columns(resultMetadata);
return typeMetadata.mapper(type, columns);
}
private RowView rowView(java.sql.ResultSet result, ResultSetMetaData metadata) throws SQLException {
Map<String, Object> values = new LinkedHashMap<>();
for (int column = 1; column <= metadata.getColumnCount(); column++) {
values.put(metadata.getColumnLabel(column), result.getObject(column));
}
return new ImmutableRowView(values);
}
private TypeMetadata inspect(Class<?> type) {
try {
if (type.isRecord()) {
RecordComponent[] components = type.getRecordComponents();
Class<?>[] parameterTypes = new Class<?>[components.length];
String[] names = new String[components.length];
for (int index = 0; index < components.length; index++) {
parameterTypes[index] = components[index].getType();
names[index] = normalize(components[index].getName());
}
Constructor<?> constructor = type.getDeclaredConstructor(parameterTypes);
constructor.setAccessible(true);
return new RecordMetadata(constructor, parameterTypes, names);
}
Constructor<?> constructor = type.getDeclaredConstructor();
constructor.setAccessible(true);
List<Field> fields = new ArrayList<>();
for (Class<?> current = type; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
fields.add(field);
}
}
}
return new BeanMetadata(constructor, List.copyOf(fields));
} catch (ReflectiveOperationException exception) {
throw new IllegalArgumentException(
"Response type must be a record or have a no-argument constructor: " + type.getName(), exception);
}
}
private Map<String, Integer> columns(ResultSetMetaData metadata) throws SQLException {
Map<String, Integer> result = new LinkedHashMap<>();
for (int column = 1; column <= metadata.getColumnCount(); column++) {
result.putIfAbsent(normalize(metadata.getColumnLabel(column)), column);
}
return result;
}
private static String normalize(String value) {
return value.replace("_", "").toLowerCase(Locale.ROOT);
}
private static boolean isScalar(Class<?> type) {
return type.isPrimitive() || type.isEnum()
|| Number.class.isAssignableFrom(type)
|| type == String.class || type == Boolean.class || type == Character.class
|| type == java.util.UUID.class
|| type.getPackageName().equals("java.time");
}
private sealed interface TypeMetadata permits RecordMetadata, BeanMetadata {
<T> RowMapper<T> mapper(Class<T> type, Map<String, Integer> columns);
}
private record RecordMetadata(
Constructor<?> constructor, Class<?>[] parameterTypes, String[] names) implements TypeMetadata {
@Override
public <T> RowMapper<T> mapper(Class<T> type, Map<String, Integer> columns) {
int[] indexes = new int[names.length];
for (int index = 0; index < names.length; index++) {
Integer column = columns.get(names[index]);
if (column == null) throw new IllegalArgumentException(
"Result does not contain record component: " + names[index]);
indexes[index] = column;
}
return result -> {
try {
Object[] values = new Object[indexes.length];
for (int index = 0; index < indexes.length; index++) {
values[index] = ValueConverter.convert(
result.getObject(indexes[index]), parameterTypes[index]);
}
return type.cast(constructor.newInstance(values));
} catch (ReflectiveOperationException exception) {
throw new DatabaseExecutionException(0, "Unable to map record " + type.getName(), exception);
}
};
}
}
private record BeanMetadata(Constructor<?> constructor, List<Field> fields) implements TypeMetadata {
@Override
public <T> RowMapper<T> mapper(Class<T> type, Map<String, Integer> columns) {
List<FieldBinding> bindings = fields.stream()
.map(field -> new FieldBinding(field, columns.get(normalize(field.getName()))))
.filter(binding -> binding.column() != null)
.toList();
return result -> {
try {
T instance = type.cast(constructor.newInstance());
for (FieldBinding binding : bindings) {
binding.field().set(instance, ValueConverter.convert(
result.getObject(binding.column()), binding.field().getType()));
}
return instance;
} catch (ReflectiveOperationException exception) {
throw new DatabaseExecutionException(0, "Unable to map bean " + type.getName(), exception);
}
};
}
}
private record FieldBinding(Field field, Integer column) {}
}

View File

@@ -0,0 +1,42 @@
package com.cygnus.db;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.UUID;
final class ParameterBinder {
void bind(PreparedStatement statement, Object[] parameters) throws SQLException {
Object[] values = parameters == null ? new Object[0] : parameters;
for (int index = 0; index < values.length; index++) {
bind(statement, index + 1, values[index]);
}
}
private void bind(PreparedStatement statement, int index, Object value) throws SQLException {
if (value instanceof SqlParameter parameter) {
if (parameter.value() == null) statement.setNull(index, parameter.sqlType());
else statement.setObject(index, parameter.value(), parameter.sqlType());
} else if (value == null) {
statement.setObject(index, null);
} else if (value instanceof Instant instant) {
statement.setTimestamp(index, Timestamp.from(instant));
} else if (value instanceof LocalDate date) {
statement.setObject(index, date);
} else if (value instanceof LocalDateTime dateTime) {
statement.setObject(index, dateTime);
} else if (value instanceof OffsetDateTime dateTime) {
statement.setObject(index, dateTime);
} else if (value instanceof UUID uuid) {
statement.setObject(index, uuid);
} else if (value instanceof Enum<?> enumValue) {
statement.setString(index, enumValue.name());
} else {
statement.setObject(index, value);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.cygnus.db;
import java.util.Objects;
import java.util.regex.Pattern;
public record QueryDefinition(int queryId, QueryType type, String sql) {
private static final String DELIMITER = "!C0L!";
private static final Pattern LEGACY_PARAMETER = Pattern.compile("\\bval\\d+\\b");
public QueryDefinition {
if (queryId <= 0) throw new QueryDefinitionException("Query ID must be positive");
Objects.requireNonNull(type, "type");
if (sql == null || sql.isBlank()) throw new QueryDefinitionException("Query SQL is empty: " + queryId);
if (LEGACY_PARAMETER.matcher(sql).find()) {
throw new QueryDefinitionException(
"Query " + queryId + " has not been migrated to parameterized SQL");
}
}
public static QueryDefinition parse(int queryId, String storedValue) {
if (storedValue == null) throw new QueryDefinitionException("Query was not found: " + queryId);
int delimiter = storedValue.indexOf(DELIMITER);
if (delimiter <= 0) throw new QueryDefinitionException("Invalid query definition: " + queryId);
return new QueryDefinition(
queryId,
QueryType.parse(storedValue.substring(0, delimiter)),
storedValue.substring(delimiter + DELIMITER.length()).trim());
}
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.db;
public final class QueryDefinitionException extends RuntimeException {
public QueryDefinitionException(String message) { super(message); }
public QueryDefinitionException(String message, Throwable cause) { super(message, cause); }
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.db;
@FunctionalInterface
public interface QueryDefinitionProvider {
QueryDefinition get(int queryId);
}

View File

@@ -0,0 +1,15 @@
package com.cygnus.db;
import java.util.Locale;
public enum QueryType {
SELECT, INSERT, UPDATE, DELETE, PROCEDURE;
public static QueryType parse(String value) {
try {
return valueOf(value.trim().toUpperCase(Locale.ROOT));
} catch (RuntimeException exception) {
throw new QueryDefinitionException("Unsupported query type: " + value, exception);
}
}
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.db;
@FunctionalInterface
public interface RowConsumer<T> {
void accept(T row) throws Exception;
}

View File

@@ -0,0 +1,9 @@
package com.cygnus.db;
import java.sql.ResultSet;
import java.sql.SQLException;
@FunctionalInterface
public interface RowMapper<T> {
T map(ResultSet resultSet) throws SQLException;
}

View File

@@ -0,0 +1,9 @@
package com.cygnus.db;
import java.util.Map;
public interface RowView {
Object get(String column);
<T> T get(String column, Class<T> type);
Map<String, Object> asMap();
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.db;
public record SqlParameter(int sqlType, Object value) {
public static SqlParameter of(int sqlType, Object value) { return new SqlParameter(sqlType, value); }
public static SqlParameter nullValue(int sqlType) { return new SqlParameter(sqlType, null); }
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.db;
@FunctionalInterface
public interface TransactionCallback<T> {
T execute(TransactionOperations operations) throws Exception;
}

View File

@@ -0,0 +1,12 @@
package com.cygnus.db;
import java.util.List;
import java.util.Optional;
public interface TransactionOperations {
<T> List<T> query(int queryId, Object[] parameters, Class<T> responseType);
<T> Optional<T> queryOne(int queryId, Object[] parameters, Class<T> responseType);
int insert(int queryId, Object[] parameters);
int update(int queryId, Object[] parameters);
int delete(int queryId, Object[] parameters);
}

View File

@@ -0,0 +1,79 @@
package com.cygnus.db;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.UUID;
final class ValueConverter {
private ValueConverter() {}
@SuppressWarnings({"unchecked", "rawtypes"})
static <T> T convert(Object value, Class<T> target) {
if (value == null) return target.isPrimitive() ? (T) primitiveDefault(target) : null;
Class<?> boxed = box(target);
if (boxed.isInstance(value)) return (T) value;
if (boxed == String.class) return (T) value.toString();
if (Number.class.isAssignableFrom(boxed) && value instanceof Number number) {
return (T) number(number, boxed);
}
if (boxed == Boolean.class) {
if (value instanceof Number number) return (T) Boolean.valueOf(number.intValue() != 0);
return (T) Boolean.valueOf(value.toString());
}
if (boxed == Character.class) {
String text = value.toString();
if (text.isEmpty()) throw new IllegalArgumentException("Cannot convert an empty value to char");
return (T) Character.valueOf(text.charAt(0));
}
if (boxed == UUID.class) return (T) UUID.fromString(value.toString());
if (boxed == LocalDate.class && value instanceof Date date) return (T) date.toLocalDate();
if (boxed == LocalDateTime.class && value instanceof Timestamp timestamp) return (T) timestamp.toLocalDateTime();
if (boxed == Instant.class && value instanceof Timestamp timestamp) return (T) timestamp.toInstant();
if (boxed == OffsetDateTime.class && value instanceof OffsetDateTime dateTime) return (T) dateTime;
if (boxed.isEnum()) return (T) Enum.valueOf((Class<Enum>) boxed, value.toString());
throw new DatabaseExecutionException(0,
"Cannot convert " + value.getClass().getName() + " to " + target.getName(), null);
}
private static Object number(Number value, Class<?> target) {
if (target == Integer.class) return value.intValue();
if (target == Long.class) return value.longValue();
if (target == Double.class) return value.doubleValue();
if (target == Float.class) return value.floatValue();
if (target == Short.class) return value.shortValue();
if (target == Byte.class) return value.byteValue();
if (target == BigDecimal.class) return value instanceof BigDecimal decimal
? decimal : new BigDecimal(value.toString());
return value;
}
private static Class<?> box(Class<?> type) {
if (!type.isPrimitive()) return type;
if (type == int.class) return Integer.class;
if (type == long.class) return Long.class;
if (type == double.class) return Double.class;
if (type == float.class) return Float.class;
if (type == short.class) return Short.class;
if (type == byte.class) return Byte.class;
if (type == boolean.class) return Boolean.class;
if (type == char.class) return Character.class;
return type;
}
private static Object primitiveDefault(Class<?> type) {
if (type == boolean.class) return false;
if (type == char.class) return '\0';
if (type == byte.class) return (byte) 0;
if (type == short.class) return (short) 0;
if (type == int.class) return 0;
if (type == long.class) return 0L;
if (type == float.class) return 0F;
if (type == double.class) return 0D;
return null;
}
}

View File

@@ -0,0 +1,86 @@
package com.cygnus.db;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class JdbcCygnusDbExecutorTest {
private JdbcCygnusDbExecutor executor;
@BeforeEach
void setUp() throws Exception {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:cygnus;DB_CLOSE_DELAY=-1");
try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS person");
statement.execute("CREATE TABLE person (id bigint generated by default as identity primary key, name varchar(100), active boolean)");
statement.execute("INSERT INTO person(name, active) VALUES ('Asha', true), ('Ravi', false)");
}
Map<Integer, QueryDefinition> queries = Map.of(
1, new QueryDefinition(1, QueryType.SELECT,
"select id, name from person where active = ? order by id"),
2, new QueryDefinition(2, QueryType.INSERT,
"insert into person(name, active) values (?, ?)"),
3, new QueryDefinition(3, QueryType.UPDATE,
"update person set active = ? where name = ?"),
4, new QueryDefinition(4, QueryType.DELETE,
"delete from person where name = ?"),
5, new QueryDefinition(5, QueryType.PROCEDURE, "call abs(?)"));
executor = new JdbcCygnusDbExecutor(dataSource, queries::get, new ExecutorOptions(10, 5));
}
@Test
void mapsSelectDirectlyToRecordsAndDynamicRows() {
var people = executor.query(1, new Object[]{true}, Person.class);
assertEquals(java.util.List.of(new Person(1L, "Asha")), people);
RowView row = executor.query(1, new Object[]{true}).getFirst();
assertEquals("Asha", row.get("NAME", String.class));
assertEquals(1L, row.get("id", Long.class));
}
@Test
void executesInsertUpdateDeleteAndGeneratedKeys() {
Long id = executor.insert(2, new Object[]{"Mira", true}, Long.class);
assertTrue(id > 0);
assertEquals(1, executor.update(3, new Object[]{false, "Mira"}));
assertEquals(1, executor.delete(4, new Object[]{"Mira"}));
}
@Test
void executesProcedureAndStreamsWithoutMaterializingRows() {
assertEquals(7, executor.procedure(5, new Object[]{-7}, Integer.class));
AtomicInteger count = new AtomicInteger();
executor.stream(1, new Object[]{true},
result -> new Person(result.getLong("id"), result.getString("name")),
row -> count.incrementAndGet());
assertEquals(1, count.get());
}
@Test
void rollsBackFailedTransactions() {
assertThrows(IllegalStateException.class, () -> executor.transaction(tx -> {
tx.insert(2, new Object[]{"Rollback", true});
throw new IllegalStateException("stop");
}));
assertFalse(executor.query(1, new Object[]{true}, Person.class).stream()
.anyMatch(person -> person.name().equals("Rollback")));
}
@Test
void rejectsLegacyStringReplacementQueries() {
assertThrows(QueryDefinitionException.class, () ->
QueryDefinition.parse(513, "select!C0L!select * from person where id=val0"));
}
record Person(long id, String name) {}
}

View File

@@ -0,0 +1,3 @@
artifactId=cygnus-onprem-db
groupId=com.cygnus
version=1.0.0-SNAPSHOT

View File

@@ -0,0 +1,25 @@
com/cygnus/db/RowConsumer.class
com/cygnus/db/RowView.class
com/cygnus/db/QueryDefinitionException.class
com/cygnus/db/QueryDefinitionProvider.class
com/cygnus/db/CygnusDbExecutor.class
com/cygnus/db/SqlParameter.class
com/cygnus/db/ObjectRowMapperFactory$RecordMetadata.class
com/cygnus/db/JdbcCygnusDbExecutor.class
com/cygnus/db/JdbcCygnusDbExecutor$SqlWork.class
com/cygnus/db/QueryDefinition.class
com/cygnus/db/QueryType.class
com/cygnus/db/ObjectRowMapperFactory$TypeMetadata.class
com/cygnus/db/TransactionCallback.class
com/cygnus/db/RowMapper.class
com/cygnus/db/ObjectRowMapperFactory.class
com/cygnus/db/DatabaseExecutionException.class
com/cygnus/db/ObjectRowMapperFactory$FieldBinding.class
com/cygnus/db/TransactionOperations.class
com/cygnus/db/ValueConverter.class
com/cygnus/db/ExecutorOptions.class
com/cygnus/db/DbResult.class
com/cygnus/db/ParameterBinder.class
com/cygnus/db/ObjectRowMapperFactory$BeanMetadata.class
com/cygnus/db/JdbcCygnusDbExecutor$TransactionOperationsImpl.class
com/cygnus/db/ImmutableRowView.class

View File

@@ -0,0 +1,19 @@
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/CygnusDbExecutor.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/DatabaseExecutionException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/DbResult.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ExecutorOptions.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ImmutableRowView.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/JdbcCygnusDbExecutor.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ObjectRowMapperFactory.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ParameterBinder.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinition.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryDefinitionProvider.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/QueryType.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowConsumer.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowMapper.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/RowView.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/SqlParameter.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionCallback.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/TransactionOperations.java
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/main/java/com/cygnus/db/ValueConverter.java

View File

@@ -0,0 +1,2 @@
com/cygnus/db/JdbcCygnusDbExecutorTest.class
com/cygnus/db/JdbcCygnusDbExecutorTest$Person.class

View File

@@ -0,0 +1 @@
/Users/maddy/Projects/cygnus/matrix/cygnus-onprem-db/src/test/java/com/cygnus/db/JdbcCygnusDbExecutorTest.java