Skip to content

Commit 348706c

Browse files
authored
Merge pull request #20 from EzFramework/feature/delete-from
Dialect-aware DELETE-query rendering & DELETE LIMIT support
2 parents 410507e + 6e2c34d commit 348706c

8 files changed

Lines changed: 194 additions & 19 deletions

File tree

‎.github/workflows/code-quality.yml‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,8 @@ jobs:
5252
with:
5353
name: jacoco-report
5454
path: target/site/jacoco/
55-
- name: Comment JaCoCo coverage summary in PR
56-
if: github.event_name == 'pull_request'
57-
uses: madrapps/jacoco-report@v1.7.2
55+
- name: Upload coverage to Codecov
56+
uses: codecov/codecov-action@v5
5857
with:
59-
paths: target/site/jacoco/jacoco.xml
60-
token: ${{ secrets.GITHUB_TOKEN }}
61-
min-coverage-overall: 0
62-
min-coverage-changed-files: 0
58+
files: target/site/jacoco/jacoco.xml
59+
token: ${{ secrets.CODECOV_TOKEN }}

‎README.md‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Add the dependency to your `pom.xml`:
2020
<dependency>
2121
<groupId>com.github.EzFramework</groupId>
2222
<artifactId>JavaQueryBuilder</artifactId>
23-
<version>1.0.3</version>
23+
<version>1.0.4</version>
2424
</dependency>
2525
```
2626

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

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

330330
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.
331331

332+
### Dialect-aware DELETE rendering
333+
334+
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.
335+
336+
```java
337+
// Obtain a Query from any QueryBuilder call
338+
Query query = new QueryBuilder()
339+
.from("users")
340+
.whereEquals("id", 42)
341+
.limit(1)
342+
.build();
343+
344+
// Standard — no quoting, no LIMIT
345+
SqlDialect.STANDARD.renderDelete(query);
346+
// → DELETE FROM users WHERE id = ? params=[42]
347+
348+
// MySQL — back-tick quoting + LIMIT
349+
SqlDialect.MYSQL.renderDelete(query);
350+
// → DELETE FROM `users` WHERE `id` = ? LIMIT 1 params=[42]
351+
352+
// SQLite — double-quote quoting + LIMIT
353+
SqlDialect.SQLITE.renderDelete(query);
354+
// → DELETE FROM "users" WHERE "id" = ? LIMIT 1 params=[42]
355+
```
356+
332357
## How SQL Generation Works
333358

334359
`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`.

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.github.EzFramework</groupId>
77
<artifactId>java-query-builder</artifactId>
8-
<version>1.0.3</version>
8+
<version>1.0.4</version>
99
<packaging>jar</packaging>
1010

1111
<name>JavaQueryBuilder</name>

‎src/main/java/com/github/ezframework/javaquerybuilder/query/sql/AbstractSqlDialect.java‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
import com.github.ezframework.javaquerybuilder.query.condition.Operator;
1212

1313
/**
14-
* Base SQL rendering logic for SELECT queries.
14+
* Base SQL rendering logic for SELECT and DELETE queries.
1515
*
16-
* <p>Implements the standard (ANSI) SQL dialect. Subclasses may override
17-
* {@link #quoteIdentifier(String)} to apply dialect-specific identifier quoting.
16+
* <p>Implements the standard (ANSI) SQL dialect and provides shared helpers for
17+
* rendering {@code WHERE} clauses. Subclasses may override
18+
* {@link #quoteIdentifier(String)} to apply dialect-specific identifier quoting
19+
* and {@link #supportsDeleteLimit()} to enable dialect-specific DELETE
20+
* {@code LIMIT} behaviour.
1821
*
1922
* @author EzFramework
2023
* @version 1.0.0
@@ -44,6 +47,44 @@ protected String quoteIdentifier(String name) {
4447
return name;
4548
}
4649

50+
@Override
51+
public SqlResult renderDelete(Query query) {
52+
final StringBuilder sql = new StringBuilder();
53+
final List<Object> params = new ArrayList<>();
54+
sql.append("DELETE FROM ").append(quoteIdentifier(query.getTable()));
55+
appendWhereClause(sql, params, query);
56+
57+
if (supportsDeleteLimit() && query.getLimit() != null && query.getLimit() >= 0) {
58+
sql.append(" LIMIT ").append(query.getLimit());
59+
}
60+
61+
final String sqlStr = sql.toString();
62+
final List<Object> paramsCopy = Collections.unmodifiableList(new ArrayList<>(params));
63+
return new SqlResult() {
64+
@Override
65+
public String getSql() {
66+
return sqlStr;
67+
}
68+
69+
@Override
70+
public List<Object> getParameters() {
71+
return paramsCopy;
72+
}
73+
};
74+
}
75+
76+
/**
77+
* Hook for dialects that support a `LIMIT` clause on DELETE statements
78+
* (for example, MySQL). The default implementation returns {@code false},
79+
* meaning the base renderer will ignore `limit` on `Query` for DELETE.
80+
* Subclasses that want to enable `LIMIT` should override this method.
81+
*
82+
* @return {@code true} if the dialect appends a `LIMIT` to DELETE statements
83+
*/
84+
protected boolean supportsDeleteLimit() {
85+
return false;
86+
}
87+
4788
@Override
4889
public SqlResult render(Query query) {
4990
final StringBuilder sql = new StringBuilder();
@@ -94,7 +135,7 @@ private void appendSelectColumns(StringBuilder sql, Query query) {
94135
}
95136
}
96137

97-
private void appendWhereClause(StringBuilder sql, List<Object> params, Query query) {
138+
protected void appendWhereClause(StringBuilder sql, List<Object> params, Query query) {
98139
final List<ConditionEntry> conditions = query.getConditions();
99140
if (conditions.isEmpty()) {
100141
return;
@@ -111,7 +152,7 @@ private void appendWhereClause(StringBuilder sql, List<Object> params, Query que
111152
}
112153

113154
@SuppressWarnings("unchecked")
114-
private void appendConditionFragment(StringBuilder sql, List<Object> params, ConditionEntry entry) {
155+
protected void appendConditionFragment(StringBuilder sql, List<Object> params, ConditionEntry entry) {
115156
final Operator op = entry.getCondition().getOperator();
116157
if (COMPARISON_OPERATORS.containsKey(op)) {
117158
sql.append(COMPARISON_OPERATORS.get(op));

‎src/main/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialect.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,17 @@ public interface SqlDialect {
3333
* @return the SQL result containing the SQL string and bound parameters
3434
*/
3535
SqlResult render(Query query);
36+
37+
/**
38+
* Renders a DELETE statement for the given query (DELETE FROM ... WHERE ...).
39+
*
40+
* @param query the query describing the delete target and conditions
41+
* @return the SQL result containing the SQL string and bound parameters
42+
*
43+
* <p>Default behaviour is implemented by {@link AbstractSqlDialect#renderDelete(Query)},
44+
* which renders {@code DELETE FROM <table> WHERE ...} and preserves parameter
45+
* ordering to match {@link #render(Query)}. Dialects that support a DELETE
46+
* {@code LIMIT} may enable it by overriding {@link AbstractSqlDialect#supportsDeleteLimit()}.
47+
*/
48+
SqlResult renderDelete(Query query);
3649
}

‎src/main/java/com/github/ezframework/javaquerybuilder/query/sql/mysql/MySqlDialect.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public class MySqlDialect extends AbstractSqlDialect {
1717
protected String quoteIdentifier(String name) {
1818
return "`" + name + "`";
1919
}
20+
21+
@Override
22+
protected boolean supportsDeleteLimit() {
23+
return true;
24+
}
2025
}

‎src/main/java/com/github/ezframework/javaquerybuilder/query/sql/sqlite/SqliteDialect.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public class SqliteDialect extends AbstractSqlDialect {
1717
protected String quoteIdentifier(String name) {
1818
return "\"" + name + "\"";
1919
}
20+
21+
@Override
22+
protected boolean supportsDeleteLimit() {
23+
return true;
24+
}
2025
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.github.ezframework.javaquerybuilder.query.sql;
2+
3+
import java.util.List;
4+
5+
import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder;
6+
import com.github.ezframework.javaquerybuilder.query.Query;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
public class AbstractSqlDialectDeleteTest {
13+
14+
static class DummyDialect extends AbstractSqlDialect {}
15+
16+
@Test
17+
void deleteParametersMatchSelectParameters() {
18+
final DummyDialect dialect = new DummyDialect();
19+
final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry ae =
20+
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("a",
21+
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
22+
com.github.ezframework.javaquerybuilder.query.condition.Operator.EQ, 1),
23+
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);
24+
25+
final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry ce =
26+
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("c",
27+
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
28+
com.github.ezframework.javaquerybuilder.query.condition.Operator.GT, 2),
29+
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);
30+
31+
final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry inE =
32+
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("l",
33+
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
34+
com.github.ezframework.javaquerybuilder.query.condition.Operator.IN, List.of(6, 7)),
35+
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);
36+
37+
final com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry betweenE =
38+
new com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry("n",
39+
new com.github.ezframework.javaquerybuilder.query.condition.Condition(
40+
com.github.ezframework.javaquerybuilder.query.condition.Operator.BETWEEN, List.of(10, 11)),
41+
com.github.ezframework.javaquerybuilder.query.condition.Connector.AND);
42+
43+
final Query q = new Query();
44+
q.setTable("t");
45+
q.setConditions(List.of(ae, ce, inE, betweenE));
46+
47+
final SqlResult select = dialect.render(q);
48+
final SqlResult del = dialect.renderDelete(q);
49+
50+
assertTrue(del.getSql().startsWith("DELETE FROM t"));
51+
assertTrue(del.getSql().contains("WHERE"));
52+
assertTrue(del.getSql().contains("?"));
53+
assertEquals(select.getParameters(), del.getParameters());
54+
}
55+
56+
@Test
57+
void sqliteAndMysqlApplyQuotingAndLimitBehavior() {
58+
final QueryBuilder qb = new QueryBuilder().from("users").whereEquals("id", 42).limit(5);
59+
final Query q = qb.build();
60+
q.setTable("users");
61+
final com.github.ezframework.javaquerybuilder.query.sql.mysql.MySqlDialect my =
62+
new com.github.ezframework.javaquerybuilder.query.sql.mysql.MySqlDialect();
63+
final SqlResult myDel = my.renderDelete(q);
64+
assertTrue(myDel.getSql().startsWith("DELETE FROM `users`"));
65+
assertTrue(myDel.getSql().contains("LIMIT 5"));
66+
67+
final SqlDialect sqlite = new com.github.ezframework.javaquerybuilder.query.sql.sqlite.SqliteDialect();
68+
final SqlResult sqDel = sqlite.renderDelete(q);
69+
assertTrue(sqDel.getSql().startsWith("DELETE FROM \"users\""));
70+
assertTrue(sqDel.getSql().contains("LIMIT 5"));
71+
72+
final DummyDialect d = new DummyDialect();
73+
final SqlResult dDel = d.renderDelete(q);
74+
assertTrue(dDel.getSql().startsWith("DELETE FROM users"));
75+
assertFalse(dDel.getSql().contains("LIMIT"));
76+
}
77+
78+
@Test
79+
void inClauseGeneratesCorrectPlaceholderCountAndOrdering() {
80+
final DummyDialect dialect = new DummyDialect();
81+
final QueryBuilder qb = new QueryBuilder().from("items").whereIn("id", List.of(1, 2, 3));
82+
final Query q = qb.build();
83+
q.setTable("items");
84+
85+
final SqlResult del = dialect.renderDelete(q);
86+
assertEquals("DELETE FROM items WHERE id IN (?, ?, ?)", del.getSql());
87+
assertEquals(List.of(1, 2, 3), del.getParameters());
88+
}
89+
}

0 commit comments

Comments
 (0)