Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 171 additions & 7 deletions src/main/java/dev/sqlcj/analysis/QueryAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.sqlcj.analysis;

import dev.sqlcj.parser.Query;
import dev.sqlcj.parser.QueryType;
import dev.sqlcj.schema.Schema;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcParameter;
Expand All @@ -18,7 +19,9 @@
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.Values;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;

import java.util.ArrayList;
import java.util.Comparator;
Expand All @@ -34,16 +37,16 @@ public QueryModel analyze(Query query, Statement statement, Schema schema) {
return analyzeSelect(query, select, schema);
}

if (statement instanceof Insert) {
throw new UnsupportedOperationException("INSERT is not supported yet");
if (statement instanceof Insert insert) {
return analyzeInsert(query, insert, schema);
}

if (statement instanceof Update) {
throw new UnsupportedOperationException("UPDATE is not supported yet");
if (statement instanceof Update update) {
return analyzeUpdate(query, update, schema);
}

if (statement instanceof Delete) {
throw new UnsupportedOperationException("DELETE is not supported yet");
if (statement instanceof Delete delete) {
return analyzeDelete(query, delete, schema);
}

throw new UnsupportedOperationException(
Expand All @@ -63,6 +66,167 @@ private QueryModel analyzeSelect(Query query, Select select, Schema schema) {

List<QueryParameter> bindingParameters = resolveBindingParameters(plainSelect, schema, table);

return toQueryModel(
query,
table.getUnquotedName(),
columns,
bindingParameters
);
}

private QueryModel analyzeInsert(Query query, Insert insert, Schema schema) {
requireExecQueryType(query);

Table table = insert.getTable();
dev.sqlcj.schema.Table schemaTable = findTable(schema, table.getUnquotedName());

return toQueryModel(
query,
table.getUnquotedName(),
List.of(),
resolveInsertParameters(insert, schemaTable)
);
}

private QueryModel analyzeUpdate(Query query, Update update, Schema schema) {
requireExecQueryType(query);

Table table = update.getTable();
dev.sqlcj.schema.Table schemaTable = findTable(schema, table.getUnquotedName());

List<QueryParameter> bindingParameters = resolveUpdateSetParameters(update, schemaTable);

if (update.getWhere() != null) {
resolveParameters(update.getWhere(), schemaTable, bindingParameters);
}

return toQueryModel(
query,
table.getUnquotedName(),
List.of(),
bindingParameters
);
}

private QueryModel analyzeDelete(Query query, Delete delete, Schema schema) {
requireExecQueryType(query);

Table table = delete.getTable();
dev.sqlcj.schema.Table schemaTable = findTable(schema, table.getUnquotedName());

List<QueryParameter> bindingParameters = new ArrayList<>();

if (delete.getWhere() != null) {
resolveParameters(delete.getWhere(), schemaTable, bindingParameters);
}

return toQueryModel(
query,
table.getUnquotedName(),
List.of(),
bindingParameters
);
}

private void requireExecQueryType(Query query) {
if (query.type() != QueryType.EXEC) {
throw new UnsupportedOperationException(
"Write queries must be declared as :exec"
);
}
}

/**
* Resolves the {@code INSERT} parameters by pairing the explicit column
* list with the single values row, which is also their textual order.
*/
private List<QueryParameter> resolveInsertParameters(Insert insert, dev.sqlcj.schema.Table table) {
ExpressionList<net.sf.jsqlparser.schema.Column> columns = insert.getColumns();

if (columns == null || columns.isEmpty()) {
throw new UnsupportedOperationException("INSERT requires an explicit column list.");
}

ParenthesedExpressionList<?> values = resolveInsertValues(insert);

if (values.size() != columns.size()) {
throw new UnsupportedOperationException(
"INSERT column and value counts must match."
);
}

List<QueryParameter> parameters = new ArrayList<>();

for (int index = 0; index < columns.size(); index++) {
if (!(values.get(index) instanceof JdbcParameter parameter)) {
throw new UnsupportedOperationException(
"INSERT values must be indexed placeholders."
);
}

addParameter(
parameter,
columns.get(index).getUnquotedColumnName(),
table,
parameters
);
}

return parameters;
}

private ParenthesedExpressionList<?> resolveInsertValues(Insert insert) {
Values values = insert.getValues();

if (values == null || !(values.getExpressions() instanceof ParenthesedExpressionList<?> row)) {
throw new UnsupportedOperationException(
"INSERT requires a single VALUES row."
);
}

return row;
}

/**
* Resolves the {@code UPDATE} assignment parameters in source order, which
* precedes any parameter in the {@code WHERE} expression.
*/
private List<QueryParameter> resolveUpdateSetParameters(Update update, dev.sqlcj.schema.Table table) {
List<QueryParameter> parameters = new ArrayList<>();

for (UpdateSet updateSet : update.getUpdateSets()) {
if (
updateSet.getColumns().size() != 1
|| updateSet.getValues().size() != 1
|| !(updateSet.getValue(0) instanceof JdbcParameter parameter)
) {
throw new UnsupportedOperationException(
"UPDATE assignments must set one column to an indexed placeholder."
);
}

addParameter(
parameter,
updateSet.getColumn(0).getUnquotedColumnName(),
table,
parameters
);
}

return parameters;
}

/**
* Builds the analyzed model from parameters collected in textual order,
* keeping that order for JDBC binding and exposing the parameters in
* logical placeholder-index order.
*/
private QueryModel toQueryModel(
Query query,
String tableName,
List<QueryColumn> columns,
List<QueryParameter> bindingParameters
) {
List<Integer> bindingParameterIndexes = bindingParameters.stream()
.map(QueryParameter::index)
.toList();
Expand All @@ -74,7 +238,7 @@ private QueryModel analyzeSelect(Query query, Select select, Schema schema) {
return new QueryModel(
query.name(),
query.type(),
table.getUnquotedName(),
tableName,
toExecutableSql(query.sql()),
bindingParameterIndexes,
columns,
Expand Down
63 changes: 45 additions & 18 deletions src/main/java/dev/sqlcj/generator/JavaCodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public final class JavaCodeGenerator implements CodeGenerator {

Expand Down Expand Up @@ -85,6 +86,9 @@ private String generateImports(QueryModel query) {

if (hasResult(query)) {
imports.add("dev.sqlcj.runtime.RowMapper");
}

if (hasResult(query) || isExec(query)) {
imports.add("java.util.List");
}

Expand All @@ -110,6 +114,15 @@ private boolean hasResult(QueryModel query) {
|| query.type() == QueryType.MANY;
}

private boolean isExec(QueryModel query) {
return query.type() == QueryType.EXEC;
}

/** A write query has no generated result record. */
private boolean hasResultType(QueryModel query) {
return !isExec(query);
}

private String resolveImport(ColumnType type) {
return switch (type) {
case DATE -> "java.time.LocalDate";
Expand Down Expand Up @@ -137,31 +150,26 @@ private String generateJavaDoc(QueryModel query) {
}

private String generateClass(QueryModel query, JavaNames names) {
String rowMapper = hasResult(query)
? indent(generateRowMapper(query, names))
: "";
String members = Stream.of(
generateExecutorField(),
generateConstructor(names),
hasResultType(query) ? generateResultType(query, names) : "",
hasResult(query) ? generateRowMapper(query, names) : "",
generateMethod(query, names)
)
.filter(member -> !member.isBlank())
.map(this::indent)
.collect(Collectors.joining("\n\n"));

return """
public final class %s {

%s

%s

%s

%s

%s
}
"""
.formatted(
names.className(),
indent(generateExecutorField()),
indent(generateConstructor(names)),
indent(generateResultType(query, names)),
rowMapper,
indent(generateMethod(query, names))
members
);
}

Expand Down Expand Up @@ -218,7 +226,8 @@ private String generateMethodParameters(QueryModel query, JavaNames names) {
private String generateMethod(QueryModel query, JavaNames names) {
return switch (query.type()) {
case ONE, MANY -> generateQueryMethod(query, names);
case EXEC, EXEC_RESULT, BATCH_EXEC, BATCH_MANY, BATCH_ONE -> generateUnsupportedMethod(query, names);
case EXEC -> generateExecMethod(query, names);
case EXEC_RESULT, BATCH_EXEC, BATCH_MANY, BATCH_ONE -> generateUnsupportedMethod(query, names);
};
}

Expand All @@ -242,6 +251,24 @@ private String generateQueryMethod(QueryModel query, JavaNames names) {
);
}

private String generateExecMethod(QueryModel query, JavaNames names) {
return """
public %s %s(%s) {
return executor.execute(
%s,
%s
);
}
"""
.formatted(
generateReturnType(query, names),
names.methodName(),
generateMethodParameters(query, names),
generateSql(query),
generateParameterList(query, names)
);
}

private String generateUnsupportedMethod(QueryModel query, JavaNames names) {
return """
public %s %s(%s) {
Expand Down Expand Up @@ -351,7 +378,7 @@ private String generateReturnType(QueryModel query, JavaNames names) {
return switch (query.type()) {
case ONE -> names.resultTypeName();
case MANY -> "List<" + names.resultTypeName() + ">";
case EXEC -> "void";
case EXEC -> "int";
case EXEC_RESULT -> "void";
case BATCH_EXEC -> "void";
case BATCH_MANY -> "void";
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/dev/sqlcj/runtime/JdbcQueryExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ public <T> List<T> queryMany(String sql, List<?> parameters, RowMapper<T> mapper
}
}

@Override
public int execute(String sql, List<?> parameters) {
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)
) {
bindParameters(statement, parameters);

return statement.executeUpdate();
} catch (SQLException e) {
throw new QueryExecutionException(
"Failed to execute query",
e
);
}
}

private void bindParameters(PreparedStatement statement, List<?> parameters) throws SQLException {
for (int i = 0; i < parameters.size(); i++) {
statement.setObject(i + 1, parameters.get(i));
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/dev/sqlcj/runtime/QueryExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ public interface QueryExecutor {
<T> T query(String sql, List<?> parameters, RowMapper<T> mapper);

<T> List<T> queryMany(String sql, List<?> parameters, RowMapper<T> mapper);

/**
* Executes a write statement and returns its affected-row count.
*/
int execute(String sql, List<?> parameters);
}
Loading