From ff63ce023925f734310a33b515fb792462033aac Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 21:42:25 +0200 Subject: [PATCH 1/4] feat(sql): add PostgreSQL dialect with ILIKE and RETURNING support - Add PostgreSqlDialect extending AbstractSqlDialect: double-quote identifier quoting, supportsReturning()=true, appendConditionFragment override for ILIKE/NOT ILIKE - Register SqlDialect.POSTGRESQL constant in SqlDialect interface - Add ILIKE and NOT_ILIKE to Operator enum (PostgreSQL-only operators) - Add supportsReturning() hook to AbstractSqlDialect; renderDelete() now appends RETURNING clause when the hook returns true --- .../query/condition/Operator.java | 18 ++++- .../query/sql/AbstractSqlDialect.java | 20 ++++- .../query/sql/SqlDialect.java | 10 ++- .../sql/postgresql/PostgreSqlDialect.java | 73 +++++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialect.java diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/condition/Operator.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/condition/Operator.java index 9300af0..81f21c8 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/condition/Operator.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/condition/Operator.java @@ -44,5 +44,21 @@ public enum Operator { * True SQL {@code NOT EXISTS (SELECT ...)} — value must be a * {@link com.github.ezframework.javaquerybuilder.query.Query}. */ - NOT_EXISTS_SUBQUERY + NOT_EXISTS_SUBQUERY, + /** + * PostgreSQL case-insensitive {@code ILIKE} substring match. + * + *

Only rendered correctly by + * {@link com.github.ezframework.javaquerybuilder.query.sql.postgresql.PostgreSqlDialect}; + * using this operator with any other dialect produces no output. + */ + ILIKE, + /** + * PostgreSQL case-insensitive {@code NOT ILIKE} substring match. + * + *

Only rendered correctly by + * {@link com.github.ezframework.javaquerybuilder.query.sql.postgresql.PostgreSqlDialect}; + * using this operator with any other dialect produces no output. + */ + NOT_ILIKE } diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/AbstractSqlDialect.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/AbstractSqlDialect.java index 07e7387..968438f 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/AbstractSqlDialect.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/AbstractSqlDialect.java @@ -17,9 +17,9 @@ * *

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. + * {@link #quoteIdentifier(String)} to apply dialect-specific identifier quoting, + * {@link #supportsDeleteLimit()} to enable dialect-specific DELETE {@code LIMIT} + * behaviour, and {@link #supportsReturning()} to enable a {@code RETURNING} clause. * *

Subquery support — parameter ordering contract: *

    @@ -67,6 +67,9 @@ public SqlResult renderDelete(Query query) { if (supportsDeleteLimit() && query.getLimit() != null && query.getLimit() >= 0) { sql.append(" LIMIT ").append(query.getLimit()); } + if (supportsReturning() && !query.getReturningColumns().isEmpty()) { + sql.append(" RETURNING ").append(String.join(", ", query.getReturningColumns())); + } final String sqlStr = sql.toString(); final List paramsCopy = Collections.unmodifiableList(new ArrayList<>(params)); @@ -95,6 +98,17 @@ protected boolean supportsDeleteLimit() { return false; } + /** + * Hook for dialects that support a {@code RETURNING} clause on DELETE statements + * (for example, PostgreSQL). The default implementation returns {@code false}. + * Subclasses that want to enable {@code RETURNING} should override this method. + * + * @return {@code true} if the dialect appends a {@code RETURNING} clause to DELETE statements + */ + protected boolean supportsReturning() { + return false; + } + @Override public SqlResult render(Query query) { final StringBuilder sql = new StringBuilder(); diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialect.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialect.java index d0929af..aa520ee 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialect.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialect.java @@ -2,13 +2,14 @@ import com.github.ezframework.javaquerybuilder.query.Query; import com.github.ezframework.javaquerybuilder.query.sql.mysql.MySqlDialect; +import com.github.ezframework.javaquerybuilder.query.sql.postgresql.PostgreSqlDialect; import com.github.ezframework.javaquerybuilder.query.sql.sqlite.SqliteDialect; /** * Strategy for rendering a {@link Query} to a SQL string. * *

    Use the built-in constants for the most common dialects: - * {@link #STANDARD}, {@link #MYSQL}, or {@link #SQLITE}. + * {@link #STANDARD}, {@link #MYSQL}, {@link #SQLITE}, or {@link #POSTGRESQL}. * For custom behaviour, extend {@link AbstractSqlDialect} and override * {@link AbstractSqlDialect#quoteIdentifier(String)}. * @@ -26,6 +27,13 @@ public interface SqlDialect { /** SQLite dialect — identifiers are wrapped in double-quote characters. */ SqlDialect SQLITE = new SqliteDialect(); + /** + * PostgreSQL dialect — identifiers are wrapped in double-quote characters, + * with additional support for {@code ILIKE}/{@code NOT ILIKE} operators and + * {@code RETURNING} clauses on DELETE statements. + */ + SqlDialect POSTGRESQL = new PostgreSqlDialect(); + /** * Renders the given query to a parameterized {@link SqlResult}. * diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialect.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialect.java new file mode 100644 index 0000000..88941c9 --- /dev/null +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialect.java @@ -0,0 +1,73 @@ +package com.github.ezframework.javaquerybuilder.query.sql.postgresql; + +import java.util.List; + +import com.github.ezframework.javaquerybuilder.query.Query; +import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; +import com.github.ezframework.javaquerybuilder.query.condition.Operator; +import com.github.ezframework.javaquerybuilder.query.sql.AbstractSqlDialect; + +/** + * PostgreSQL SQL dialect. + * + *

    Extends {@link AbstractSqlDialect} with PostgreSQL-specific behaviour: + *

      + *
    • Identifiers are wrapped in double-quote characters (SQL standard quoting, + * required for mixed-case names and reserved words in PostgreSQL).
    • + *
    • {@code ILIKE} and {@code NOT ILIKE} operators for case-insensitive pattern + * matching ({@link Operator#ILIKE}, {@link Operator#NOT_ILIKE}).
    • + *
    • A {@code RETURNING} clause is appended to {@code DELETE} statements when + * returning columns have been specified on the query.
    • + *
    + * + * @author EzFramework + * @version 1.0.0 + */ +public class PostgreSqlDialect extends AbstractSqlDialect { + + @Override + protected String quoteIdentifier(final String name) { + return "\"" + name + "\""; + } + + @Override + protected boolean supportsDeleteLimit() { + return false; + } + + @Override + protected boolean supportsReturning() { + return true; + } + + /** + * Appends the operator and value fragment for a single condition. + * + *

    Handles {@link Operator#ILIKE} and {@link Operator#NOT_ILIKE} in addition + * to the operators supported by the base class. All other operators are + * delegated to {@link AbstractSqlDialect#appendConditionFragment}. + * + * @param sql the SQL string builder + * @param params the bound-parameter list + * @param entry the condition entry to render + * @param query the source query (used for LIKE wrapping configuration) + */ + @Override + protected void appendConditionFragment( + final StringBuilder sql, + final List params, + final ConditionEntry entry, + final Query query) { + final Operator op = entry.getCondition().getOperator(); + final Object val = entry.getCondition().getValue(); + if (op == Operator.ILIKE) { + sql.append("ILIKE ?"); + params.add(query.getLikePrefix() + val + query.getLikeSuffix()); + } else if (op == Operator.NOT_ILIKE) { + sql.append("NOT ILIKE ?"); + params.add(query.getLikePrefix() + val + query.getLikeSuffix()); + } else { + super.appendConditionFragment(sql, params, entry, query); + } + } +} From 864e21d3d10ad34b5b8e9b44532771df504034ea Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 21:42:32 +0200 Subject: [PATCH 2/4] feat(builder): add RETURNING clause support to DML builders - Add returningColumns field + getter/setter to Query - DeleteBuilder.returning(String... columns): passed through Query to AbstractSqlDialect.renderDelete() via supportsReturning() hook - InsertBuilder.returning(String... columns): appended inline to SQL - UpdateBuilder.returning(String... columns): appended inline to SQL --- .../javaquerybuilder/query/Query.java | 23 +++++++++++ .../query/builder/DeleteBuilder.java | 18 +++++++++ .../query/builder/InsertBuilder.java | 38 +++++++++++++++++++ .../query/builder/UpdateBuilder.java | 20 ++++++++++ 4 files changed, 99 insertions(+) diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java index 53a5eb8..43f73d1 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java @@ -67,6 +67,9 @@ public class Query { /** Suffix appended to values for LIKE and NOT LIKE conditions. */ private String likeSuffix = "%"; + /** The columns to include in a {@code RETURNING} clause; empty means no RETURNING. */ + private List returningColumns = new ArrayList<>(); + /** * Gets the source table for the query. * @@ -378,4 +381,24 @@ public String getLikeSuffix() { public void setLikeSuffix(final String likeSuffix) { this.likeSuffix = likeSuffix; } + + /** + * Returns the columns to include in a {@code RETURNING} clause. + * + *

    An empty list means no {@code RETURNING} clause will be rendered. + * + * @return the list of returning column names; never {@code null} + */ + public List getReturningColumns() { + return returningColumns; + } + + /** + * Sets the columns to include in a {@code RETURNING} clause. + * + * @param returningColumns the list of column names; must not be {@code null} + */ + public void setReturningColumns(final List returningColumns) { + this.returningColumns = returningColumns; + } } diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/DeleteBuilder.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/DeleteBuilder.java index 154e4dd..11d053a 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/DeleteBuilder.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/DeleteBuilder.java @@ -26,6 +26,9 @@ public class DeleteBuilder { /** The WHERE conditions. */ private final List conditions = new ArrayList<>(); + /** The RETURNING columns (PostgreSQL only). */ + private final List returningColumns = new ArrayList<>(); + /** The defaults configuration for this builder instance. */ private QueryBuilderDefaults queryBuilderDefaults = QueryBuilderDefaults.global(); @@ -190,6 +193,20 @@ public DeleteBuilder whereExistsSubquery(Query subquery) { return this; } + /** + * Specifies the columns to include in a {@code RETURNING} clause (PostgreSQL only). + * + *

    This is only rendered when the active dialect supports {@code RETURNING} + * (i.e., {@link com.github.ezframework.javaquerybuilder.query.sql.SqlDialect#POSTGRESQL}). + * + * @param columns one or more column names; must not be {@code null} or empty + * @return this builder instance for chaining + */ + public DeleteBuilder returning(final String... columns) { + returningColumns.addAll(java.util.Arrays.asList(columns)); + return this; + } + /** * Builds the SQL DELETE statement using standard SQL. * @@ -217,6 +234,7 @@ private Query toQuery() { q.setTable(table); q.setConditions(new ArrayList<>(conditions)); q.setLimit(-1); + q.setReturningColumns(new ArrayList<>(returningColumns)); return q; } } diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/InsertBuilder.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/InsertBuilder.java index 6b60dba..9867fb6 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/InsertBuilder.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/InsertBuilder.java @@ -24,17 +24,52 @@ public class InsertBuilder { /** The values to insert. */ private final List values = new ArrayList<>(); + /** The RETURNING columns (PostgreSQL only). */ + private final List returningColumns = new ArrayList<>(); + + /** + * Sets the table to insert into. + * + * @param table the table name + * @return this builder instance for chaining + */ public InsertBuilder into(String table) { this.table = table; return this; } + /** + * Adds a column-value pair to the INSERT statement. + * + * @param column the column name + * @param value the value to insert + * @return this builder instance for chaining + */ public InsertBuilder value(String column, Object value) { columns.add(column); values.add(value); return this; } + /** + * Specifies the columns to include in a {@code RETURNING} clause (PostgreSQL only). + * + *

    The {@code RETURNING} clause is appended unconditionally to the SQL string; + * it is the caller's responsibility to use a PostgreSQL connection. + * + * @param columns one or more column names; must not be {@code null} or empty + * @return this builder instance for chaining + */ + public InsertBuilder returning(final String... columns) { + returningColumns.addAll(java.util.Arrays.asList(columns)); + return this; + } + + /** + * Builds the SQL INSERT statement using the default dialect. + * + * @return the SQL result + */ public SqlResult build() { return build(null); } @@ -52,6 +87,9 @@ public SqlResult build(SqlDialect dialect) { sql.append(") VALUES ("); sql.append(String.join(", ", Collections.nCopies(values.size(), "?"))); sql.append(")"); + if (!returningColumns.isEmpty()) { + sql.append(" RETURNING ").append(String.join(", ", returningColumns)); + } return new SqlResult() { @Override public String getSql() { diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/UpdateBuilder.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/UpdateBuilder.java index 3e41f69..d3c6c44 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/UpdateBuilder.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/UpdateBuilder.java @@ -29,6 +29,9 @@ public class UpdateBuilder { /** The WHERE conditions. */ private final List conditions = new ArrayList<>(); + /** The RETURNING columns (PostgreSQL only). */ + private final List returningColumns = new ArrayList<>(); + /** * Sets the table to update. * @param table the table name @@ -85,6 +88,20 @@ public UpdateBuilder whereGreaterThanOrEquals(String column, int value) { return this; } + /** + * Specifies the columns to include in a {@code RETURNING} clause (PostgreSQL only). + * + *

    The {@code RETURNING} clause is appended unconditionally to the SQL string; + * it is the caller's responsibility to use a PostgreSQL connection. + * + * @param columns one or more column names; must not be {@code null} or empty + * @return this builder instance for chaining + */ + public UpdateBuilder returning(final String... columns) { + returningColumns.addAll(java.util.Arrays.asList(columns)); + return this; + } + /** * Builds the SQL UPDATE statement. * @return the SQL result @@ -123,6 +140,9 @@ public SqlResult build(SqlDialect dialect) { params.add(cond.getCondition().getValue()); } } + if (!returningColumns.isEmpty()) { + sql.append(" RETURNING ").append(String.join(", ", returningColumns)); + } return new SqlResult() { @Override From 90435096736adf4c8e775810d33a9c5037054cda Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 21:42:39 +0200 Subject: [PATCH 3/4] feat(builder): add whereILike / orWhereILike builder methods - QueryBuilder.whereILike(column, value): ILIKE condition with AND connector - QueryBuilder.orWhereILike(column, value): ILIKE condition with OR connector - SelectBuilder.whereILike(column, value): ILIKE condition with AND connector (follows existing whereLike pattern; no orWhere* in SelectBuilder) Rendered correctly only with SqlDialect.POSTGRESQL --- .../query/builder/QueryBuilder.java | 45 +++++++++++++++++++ .../query/builder/SelectBuilder.java | 18 ++++++++ 2 files changed, 63 insertions(+) diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/QueryBuilder.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/QueryBuilder.java index aecb263..0c9ed56 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/QueryBuilder.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/QueryBuilder.java @@ -284,6 +284,51 @@ public QueryBuilder whereNotLike(String column, String value) { return this; } + /** + * Adds an {@code ILIKE} WHERE condition joined with AND (PostgreSQL only). + * + *

    Produces case-insensitive pattern matching. Rendered correctly only when + * the active dialect is {@link com.github.ezframework.javaquerybuilder.query.sql.SqlDialect#POSTGRESQL}. + * The configured like prefix and suffix are applied to the value. + * + * @param column the column name + * @param value the pattern to match (prefix/suffix applied automatically) + * @return this builder instance for chaining + */ + public QueryBuilder whereILike(final String column, final String value) { + conditions.add( + new ConditionEntry( + column, + new Condition(Operator.ILIKE, value), + conditions.isEmpty() ? Connector.AND : Connector.AND + ) + ); + return this; + } + + /** + * Adds a {@code NOT ILIKE} WHERE condition joined with OR (PostgreSQL only). + * + *

    Produces negated case-insensitive pattern matching. Rendered correctly only + * when the active dialect is + * {@link com.github.ezframework.javaquerybuilder.query.sql.SqlDialect#POSTGRESQL}. + * The configured like prefix and suffix are applied to the value. + * + * @param column the column name + * @param value the pattern to match (prefix/suffix applied automatically) + * @return this builder instance for chaining + */ + public QueryBuilder orWhereILike(final String column, final String value) { + conditions.add( + new ConditionEntry( + column, + new Condition(Operator.ILIKE, value), + Connector.OR + ) + ); + return this; + } + /** * Adds an {@code EXISTS} WHERE condition joined with AND. * diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilder.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilder.java index 3a34f92..962c760 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilder.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilder.java @@ -153,6 +153,24 @@ public SelectBuilder whereLike(String column, String value) { return this; } + /** + * Adds a {@code WHERE ILIKE} condition (PostgreSQL case-insensitive match) joined with AND. + * + *

    Rendered correctly only when the active dialect is + * {@link com.github.ezframework.javaquerybuilder.query.sql.SqlDialect#POSTGRESQL}. + * + * @param column the column name + * @param value the pattern to match (prefix/suffix applied automatically) + * @return this builder + */ + public SelectBuilder whereILike(final String column, final String value) { + conditions.add(new ConditionEntry( + column, + new Condition(Operator.ILIKE, value), + conditions.isEmpty() ? Connector.AND : Connector.AND)); + return this; + } + /** * Adds a GROUP BY clause. * @param columns the columns to group by From 604ad7862b9753e7964a94529e63242538d368b5 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 21:42:46 +0200 Subject: [PATCH 4/4] test+chore: add PostgreSqlDialectTest, update SqlDialectTest, bump to 1.1.0 - PostgreSqlDialectTest (14 tests): identifier quoting, ILIKE/NOT ILIKE rendering, RETURNING on DELETE/INSERT/UPDATE, dialect isolation - SqlDialectTest: add postgresqlDialectIsSingleton() test - pom.xml: bump version 1.0.7 -> 1.1.0 (minor: new public API surface) --- pom.xml | 2 +- .../query/sql/SqlDialectTest.java | 5 + .../sql/postgresql/PostgreSqlDialectTest.java | 153 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialectTest.java diff --git a/pom.xml b/pom.xml index 449b7f5..3715610 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.github.EzFramework java-query-builder - 1.0.7 + 1.1.0 jar JavaQueryBuilder diff --git a/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialectTest.java b/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialectTest.java index 4d13194..9423e28 100644 --- a/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialectTest.java +++ b/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/SqlDialectTest.java @@ -16,4 +16,9 @@ void mysqlDialectIsSingleton() { void sqliteDialectIsSingleton() { assertSame(SqlDialect.SQLITE, SqlDialect.SQLITE); } + + @Test + void postgresqlDialectIsSingleton() { + assertSame(SqlDialect.POSTGRESQL, SqlDialect.POSTGRESQL); + } } diff --git a/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialectTest.java b/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialectTest.java new file mode 100644 index 0000000..66c5883 --- /dev/null +++ b/src/test/java/com/github/ezframework/javaquerybuilder/query/sql/postgresql/PostgreSqlDialectTest.java @@ -0,0 +1,153 @@ +package com.github.ezframework.javaquerybuilder.query.sql.postgresql; + +import java.util.List; + +import com.github.ezframework.javaquerybuilder.query.builder.DeleteBuilder; +import com.github.ezframework.javaquerybuilder.query.builder.InsertBuilder; +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.builder.UpdateBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PostgreSqlDialectTest { + + @Test + void canInstantiatePostgreSqlDialect() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + assertNotNull(dialect); + } + + @Test + void quotesTableNameWithDoubleQuotes() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder().from("orders").buildSql("orders", dialect); + assertTrue(result.getSql().startsWith("SELECT * FROM \"orders\"")); + } + + @Test + void quotesColumnNamesWithDoubleQuotes() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder() + .select("id", "name") + .whereEquals("status", "active") + .buildSql("users", dialect); + assertTrue(result.getSql().contains("\"id\", \"name\"")); + assertTrue(result.getSql().contains("\"status\" = ?")); + assertEquals(List.of("active"), result.getParameters()); + } + + @Test + void quotesOrderByColumnsWithDoubleQuotes() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder() + .orderBy("created_at", false) + .buildSql("events", dialect); + assertTrue(result.getSql().contains("ORDER BY \"created_at\" DESC")); + } + + @Test + void rendersILikeCondition() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder() + .whereILike("email", "alice") + .buildSql("users", dialect); + assertTrue(result.getSql().contains("\"email\" ILIKE ?")); + assertEquals(List.of("%alice%"), result.getParameters()); + } + + @Test + void rendersNotILikeCondition() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder() + .whereNotLike("email", "spam") + .buildSql("users", dialect); + assertTrue(result.getSql().contains("NOT LIKE ?")); + } + + @Test + void rendersOrWhereILike() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new QueryBuilder() + .whereEquals("role", "admin") + .orWhereILike("name", "john") + .buildSql("users", dialect); + assertTrue(result.getSql().contains("OR \"name\" ILIKE ?")); + } + + @Test + void deleteDoesNotAppendLimit() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new DeleteBuilder() + .from("events") + .whereEquals("id", 42) + .build(dialect); + assertFalse(result.getSql().contains("LIMIT")); + } + + @Test + void deleteReturningAppendsClause() { + final PostgreSqlDialect dialect = new PostgreSqlDialect(); + final SqlResult result = new DeleteBuilder() + .from("users") + .whereEquals("id", 99) + .returning("id", "email") + .build(dialect); + assertTrue(result.getSql().endsWith("RETURNING id, email"), + "Expected SQL to end with RETURNING clause, got: " + result.getSql()); + } + + @Test + void deleteReturningNotRenderedForOtherDialects() { + final SqlResult result = new DeleteBuilder() + .from("users") + .whereEquals("id", 1) + .returning("id") + .build(SqlDialect.STANDARD); + assertFalse(result.getSql().contains("RETURNING")); + } + + @Test + void insertReturningAppendsClause() { + final SqlResult result = new InsertBuilder() + .into("users") + .value("name", "Alice") + .returning("id", "created_at") + .build(); + assertTrue(result.getSql().endsWith("RETURNING id, created_at"), + "Expected SQL to end with RETURNING clause, got: " + result.getSql()); + } + + @Test + void insertWithoutReturningHasNoClause() { + final SqlResult result = new InsertBuilder() + .into("users") + .value("name", "Bob") + .build(); + assertFalse(result.getSql().contains("RETURNING")); + } + + @Test + void updateReturningAppendsClause() { + final SqlResult result = new UpdateBuilder() + .table("users") + .set("name", "Charlie") + .whereEquals("id", 7) + .returning("id", "updated_at") + .build(); + assertTrue(result.getSql().endsWith("RETURNING id, updated_at"), + "Expected SQL to end with RETURNING clause, got: " + result.getSql()); + } + + @Test + void updateWithoutReturningHasNoClause() { + final SqlResult result = new UpdateBuilder() + .table("users") + .set("status", "active") + .build(); + assertFalse(result.getSql().contains("RETURNING")); + } +}