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/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
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
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);
+ }
+ }
+}
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"));
+ }
+}