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
11 changes: 4 additions & 7 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,8 @@ jobs:
with:
name: jacoco-report
path: target/site/jacoco/
- name: Comment JaCoCo coverage summary in PR
if: github.event_name == 'pull_request'
uses: madrapps/jacoco-report@v1.7.2
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
paths: target/site/jacoco/jacoco.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 0
min-coverage-changed-files: 0
files: target/site/jacoco/jacoco.xml
token: ${{ secrets.CODECOV_TOKEN }}
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Add the dependency to your `pom.xml`:
<dependency>
<groupId>com.github.EzFramework</groupId>
<artifactId>JavaQueryBuilder</artifactId>
<version>1.0.3</version>
<version>1.0.4</version>
</dependency>
```

Expand Down Expand Up @@ -321,14 +321,39 @@ SqlResult result = new QueryBuilder()
// → SELECT * FROM "users" WHERE "status" = ?
```

| Dialect | Identifier quoting | Boolean values |
|---|---|---|
| `SqlDialect.STANDARD` | none | `true` / `false` |
| `SqlDialect.MYSQL` | back-ticks `` `col` `` | `true` / `false` |
| `SqlDialect.SQLITE` | double quotes `"col"` | `1` / `0` |
| Dialect | Identifier quoting | Boolean values | DELETE LIMIT |
|---|---|---|---|
| `SqlDialect.STANDARD` | none | `true` / `false` | no |
| `SqlDialect.MYSQL` | back-ticks `` `col` `` | `true` / `false` | yes |
| `SqlDialect.SQLITE` | double quotes `"col"` | `1` / `0` | yes |

MySQL back-tick quoting safely handles reserved words and case-sensitive identifiers. SQLite double-quote quoting serves the same purpose, and Java booleans are converted to `1`/`0` to match SQLite's integer-backed boolean storage.

### Dialect-aware DELETE rendering

Every `SqlDialect` exposes a `renderDelete(Query query)` method that generates a fully dialect-aware `DELETE FROM ... WHERE ...` statement, identifier quoting and all. MySQL and SQLite also append a `LIMIT` clause when one is set on the query; the standard dialect silently ignores it.

```java
// Obtain a Query from any QueryBuilder call
Query query = new QueryBuilder()
.from("users")
.whereEquals("id", 42)
.limit(1)
.build();

// Standard — no quoting, no LIMIT
SqlDialect.STANDARD.renderDelete(query);
// → DELETE FROM users WHERE id = ? params=[42]

// MySQL — back-tick quoting + LIMIT
SqlDialect.MYSQL.renderDelete(query);
// → DELETE FROM `users` WHERE `id` = ? LIMIT 1 params=[42]

// SQLite — double-quote quoting + LIMIT
SqlDialect.SQLITE.renderDelete(query);
// → DELETE FROM "users" WHERE "id" = ? LIMIT 1 params=[42]
```

## How SQL Generation Works

`buildSql(table)` (or `query.toSql(table)`) translates the `Query` into a single-line parameterized SQL string. All values are returned separately as a `List<Object>` — the SQL string itself only contains `?` placeholders, so **user-supplied values are never interpolated into the string**. This makes it inherently safe against SQL injection when used with a `PreparedStatement`.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.github.EzFramework</groupId>
<artifactId>java-query-builder</artifactId>
<version>1.0.3</version>
<version>1.0.4</version>
<packaging>jar</packaging>

<name>JavaQueryBuilder</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
import com.github.ezframework.javaquerybuilder.query.condition.Operator;

/**
* Base SQL rendering logic for SELECT queries.
* Base SQL rendering logic for SELECT and DELETE queries.
*
* <p>Implements the standard (ANSI) SQL dialect. Subclasses may override
* {@link #quoteIdentifier(String)} to apply dialect-specific identifier quoting.
* <p>Implements the standard (ANSI) SQL dialect and provides shared helpers for
* rendering {@code WHERE} clauses. Subclasses may override
* {@link #quoteIdentifier(String)} to apply dialect-specific identifier quoting
* and {@link #supportsDeleteLimit()} to enable dialect-specific DELETE
* {@code LIMIT} behaviour.
*
* @author EzFramework
* @version 1.0.0
Expand Down Expand Up @@ -44,6 +47,44 @@ protected String quoteIdentifier(String name) {
return name;
}

@Override
public SqlResult renderDelete(Query query) {
final StringBuilder sql = new StringBuilder();
final List<Object> params = new ArrayList<>();
sql.append("DELETE FROM ").append(quoteIdentifier(query.getTable()));
appendWhereClause(sql, params, query);

if (supportsDeleteLimit() && query.getLimit() != null && query.getLimit() >= 0) {
sql.append(" LIMIT ").append(query.getLimit());
}

final String sqlStr = sql.toString();
final List<Object> paramsCopy = Collections.unmodifiableList(new ArrayList<>(params));
return new SqlResult() {
@Override
public String getSql() {
return sqlStr;
}

@Override
public List<Object> getParameters() {
return paramsCopy;
}
};
}

/**
* Hook for dialects that support a `LIMIT` clause on DELETE statements
* (for example, MySQL). The default implementation returns {@code false},
* meaning the base renderer will ignore `limit` on `Query` for DELETE.
* Subclasses that want to enable `LIMIT` should override this method.
*
* @return {@code true} if the dialect appends a `LIMIT` to DELETE statements
*/
protected boolean supportsDeleteLimit() {
return false;
}

@Override
public SqlResult render(Query query) {
final StringBuilder sql = new StringBuilder();
Expand Down Expand Up @@ -94,7 +135,7 @@ private void appendSelectColumns(StringBuilder sql, Query query) {
}
}

private void appendWhereClause(StringBuilder sql, List<Object> params, Query query) {
protected void appendWhereClause(StringBuilder sql, List<Object> params, Query query) {
final List<ConditionEntry> conditions = query.getConditions();
if (conditions.isEmpty()) {
return;
Expand All @@ -111,7 +152,7 @@ private void appendWhereClause(StringBuilder sql, List<Object> params, Query que
}

@SuppressWarnings("unchecked")
private void appendConditionFragment(StringBuilder sql, List<Object> params, ConditionEntry entry) {
protected void appendConditionFragment(StringBuilder sql, List<Object> params, ConditionEntry entry) {
final Operator op = entry.getCondition().getOperator();
if (COMPARISON_OPERATORS.containsKey(op)) {
sql.append(COMPARISON_OPERATORS.get(op));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,17 @@ public interface SqlDialect {
* @return the SQL result containing the SQL string and bound parameters
*/
SqlResult render(Query query);

/**
* Renders a DELETE statement for the given query (DELETE FROM ... WHERE ...).
*
* @param query the query describing the delete target and conditions
* @return the SQL result containing the SQL string and bound parameters
*
* <p>Default behaviour is implemented by {@link AbstractSqlDialect#renderDelete(Query)},
* which renders {@code DELETE FROM <table> WHERE ...} and preserves parameter
* ordering to match {@link #render(Query)}. Dialects that support a DELETE
* {@code LIMIT} may enable it by overriding {@link AbstractSqlDialect#supportsDeleteLimit()}.
*/
SqlResult renderDelete(Query query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public class MySqlDialect extends AbstractSqlDialect {
protected String quoteIdentifier(String name) {
return "`" + name + "`";
}

@Override
protected boolean supportsDeleteLimit() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public class SqliteDialect extends AbstractSqlDialect {
protected String quoteIdentifier(String name) {
return "\"" + name + "\"";
}

@Override
protected boolean supportsDeleteLimit() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.github.ezframework.javaquerybuilder.query.sql;

import java.util.List;

import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder;
import com.github.ezframework.javaquerybuilder.query.Query;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class AbstractSqlDialectDeleteTest {

static class DummyDialect extends AbstractSqlDialect {}

@Test
void deleteParametersMatchSelectParameters() {
final DummyDialect dialect = new DummyDialect();
final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry ae =
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("a",
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
com.github.ezframework.javaquerybuilder.query.condition.Operator.EQ, 1),
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);

final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry ce =
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("c",
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
com.github.ezframework.javaquerybuilder.query.condition.Operator.GT, 2),
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);

final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry inE =
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("l",
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
com.github.ezframework.javaquerybuilder.query.condition.Operator.IN, List.of(6, 7)),
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);

final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry betweenE =
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("n",
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
com.github.ezframework.javaquerybuilder.query.condition.Operator.BETWEEN, List.of(10, 11)),
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);

final Query q = new Query();
q.setTable("t");
q.setConditions(List.of(ae, ce, inE, betweenE));

final SqlResult select = dialect.render(q);
final SqlResult del = dialect.renderDelete(q);

assertTrue(del.getSql().startsWith("DELETE FROM t"));
assertTrue(del.getSql().contains("WHERE"));
assertTrue(del.getSql().contains("?"));
assertEquals(select.getParameters(), del.getParameters());
}

@Test
void sqliteAndMysqlApplyQuotingAndLimitBehavior() {
final QueryBuilder qb = new QueryBuilder().from("users").whereEquals("id", 42).limit(5);
final Query q = qb.build();
q.setTable("users");
final com.github.ezframework.javaquerybuilder.query.sql.mysql.MySqlDialect my =
new com.github.ezframework.javaquerybuilder.query.sql.mysql.MySqlDialect();
final SqlResult myDel = my.renderDelete(q);
assertTrue(myDel.getSql().startsWith("DELETE FROM `users`"));
assertTrue(myDel.getSql().contains("LIMIT 5"));

final SqlDialect sqlite = new com.github.ezframework.javaquerybuilder.query.sql.sqlite.SqliteDialect();
final SqlResult sqDel = sqlite.renderDelete(q);
assertTrue(sqDel.getSql().startsWith("DELETE FROM \"users\""));
assertTrue(sqDel.getSql().contains("LIMIT 5"));

final DummyDialect d = new DummyDialect();
final SqlResult dDel = d.renderDelete(q);
assertTrue(dDel.getSql().startsWith("DELETE FROM users"));
assertFalse(dDel.getSql().contains("LIMIT"));
}

@Test
void inClauseGeneratesCorrectPlaceholderCountAndOrdering() {
final DummyDialect dialect = new DummyDialect();
final QueryBuilder qb = new QueryBuilder().from("items").whereIn("id", List.of(1, 2, 3));
final Query q = qb.build();
q.setTable("items");

final SqlResult del = dialect.renderDelete(q);
assertEquals("DELETE FROM items WHERE id IN (?, ?, ?)", del.getSql());
assertEquals(List.of(1, 2, 3), del.getParameters());
}
}
Loading