From 030fc0da0e705e424b5f9a942538a85131022111 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 21:57:13 +0200 Subject: [PATCH 1/2] docs: update documentation for PostgreSQL support (1.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sql-dialects.md: add POSTGRESQL to overview table, usage example, dialect matrix, AbstractSqlDialect section, and interface table - conditions.md: add ILIKE/NOT_ILIKE to Operator table and builder method reference table - dml-builders.md: add RETURNING clause section with DELETE/INSERT/UPDATE examples; add returning() to InsertBuilder, UpdateBuilder, DeleteBuilder method reference tables - index.md: bump badges and version snippet to 1.1.0, update operator count (16→18), dialect list (Three→Four, add POSTGRESQL), docs table - installation.md: bump Maven, Gradle, and GitHub Packages version snippets from 1.0.4 to 1.1.0 --- docs/conditions.md | 4 +++ docs/dml-builders.md | 56 ++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 12 ++++----- docs/installation.md | 6 ++--- docs/sql-dialects.md | 58 +++++++++++++++++++++++++------------------- 5 files changed, 102 insertions(+), 34 deletions(-) diff --git a/docs/conditions.md b/docs/conditions.md index b7fe103..91da784 100644 --- a/docs/conditions.md +++ b/docs/conditions.md @@ -50,6 +50,8 @@ conditions use `AND` by default; call the `orWhere*` variant to use `OR`. | `BETWEEN` | `col BETWEEN ? AND ?` | Inclusive range; value is a two-element `List` | | `EXISTS_SUBQUERY` | `EXISTS (SELECT ...)` | Value is a `Query`; column is `null` | | `NOT_EXISTS_SUBQUERY` | `NOT EXISTS (SELECT ...)` | Value is a `Query`; column is `null` | +| `ILIKE` | `col ILIKE ?` | Case-insensitive substring match — **PostgreSQL only** | +| `NOT_ILIKE` | `col NOT ILIKE ?` | Negated case-insensitive match — **PostgreSQL only** | --- @@ -69,6 +71,8 @@ constant and the SQL it generates. | `whereLessThanOrEquals(col, val)` | `LTE` | `col <= ?` | | `whereLike(col, val)` | `LIKE` | `col LIKE ?` | | `whereNotLike(col, val)` | `NOT_LIKE` | `col NOT LIKE ?` | +| `whereILike(col, val)` | `ILIKE` | `col ILIKE ?` (PostgreSQL only) | +| `orWhereILike(col, val)` | `ILIKE` | `OR col ILIKE ?` (PostgreSQL only) | | `whereNull(col)` | `IS_NULL` | `col IS NULL` | | `whereNotNull(col)` | `IS_NOT_NULL` | `col IS NOT NULL` | | `whereExists(col)` | `EXISTS` | `col IS NOT NULL` | diff --git a/docs/dml-builders.md b/docs/dml-builders.md index 68323dd..fbfaa58 100644 --- a/docs/dml-builders.md +++ b/docs/dml-builders.md @@ -61,6 +61,7 @@ SqlResult result = QueryBuilder.insertInto("users") |--------|---------|-------------| | `into(String table)` | `InsertBuilder` | Set target table (also available via factory) | | `value(String col, Object val)` | `InsertBuilder` | Add a column/value pair | +| `returning(String... cols)` | `InsertBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | | `build()` | `SqlResult` | Render with standard dialect | | `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | @@ -111,6 +112,7 @@ SqlResult result = QueryBuilder.update("users") | `whereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (AND) | | `orWhereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (OR) | | `whereGreaterThanOrEquals(String col, int val)` | `UpdateBuilder` | `WHERE col >= ?` (AND) | +| `returning(String... cols)` | `UpdateBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | | `build()` | `SqlResult` | Render with standard dialect | | `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | @@ -186,11 +188,65 @@ SqlResult result = SqlDialect.MYSQL.renderDelete(q); | `whereIn(col, List)` | `DeleteBuilder` | `WHERE col IN (...)` (AND) | | `whereNotIn(col, List)` | `DeleteBuilder` | `WHERE col NOT IN (...)` (AND) | | `whereBetween(col, from, to)` | `DeleteBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | +| `returning(String... cols)` | `DeleteBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only; use with `SqlDialect.POSTGRESQL`) | | `build()` | `SqlResult` | Render with standard dialect | | `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | --- +## RETURNING clause (PostgreSQL) +{: #returning-clause-postgresql } + +All three DML builders support a `RETURNING` clause for PostgreSQL, which +lets the database return column values from affected rows in a single round-trip. + +### DELETE … RETURNING + +`RETURNING` on `DELETE` is rendered by the dialect — it is only appended when +`SqlDialect.POSTGRESQL` (or another dialect that overrides `supportsReturning()`) +is active. + +```java +SqlResult result = QueryBuilder.deleteFrom("users") + .whereEquals("id", 99) + .returning("id", "email") + .build(SqlDialect.POSTGRESQL); + +// → DELETE FROM "users" WHERE "id" = ? RETURNING id, email +// Parameters: [99] +``` + +### INSERT … RETURNING + +`RETURNING` on `INSERT` is appended inline regardless of dialect — the caller +is responsible for using a PostgreSQL connection. + +```java +SqlResult result = QueryBuilder.insertInto("users") + .value("name", "Alice") + .value("email", "alice@example.com") + .returning("id", "created_at") + .build(); + +// → INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, created_at +// Parameters: ["Alice", "alice@example.com"] +``` + +### UPDATE … RETURNING + +```java +SqlResult result = QueryBuilder.update("users") + .set("status", "active") + .whereEquals("id", 7) + .returning("id", "updated_at") + .build(); + +// → UPDATE users SET status = ? WHERE id = ? RETURNING id, updated_at +// Parameters: ["active", 7] +``` + +--- + ## CreateBuilder ### Basic CREATE TABLE diff --git a/docs/index.md b/docs/index.md index 02bb48d..165c42a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ permalink: / # JavaQueryBuilder [![JitPack](https://jitpack.io/v/EzFramework/JavaQueryBuilder.svg)](https://jitpack.io/#EzFramework/JavaQueryBuilder) -[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-1.0.4-blue?logo=github)](https://github.com/EzFramework/JavaQueryBuilder/packages) +[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-1.1.0-blue?logo=github)](https://github.com/EzFramework/JavaQueryBuilder/packages) **JavaQueryBuilder** is a lightweight, fluent Java library for building parameterized SQL queries and filtering in-memory data. @@ -24,12 +24,12 @@ No runtime dependencies required. - **DML builders**: `InsertBuilder`, `UpdateBuilder`, `DeleteBuilder`, `CreateBuilder` - **Parameterized-only**: user values always go through `?` bind parameters; SQL injection is structurally impossible -- **16 operators**: equality, comparison, `LIKE`, `NULL` checks, `IN`, `BETWEEN`, +- **18 operators**: equality, comparison, `LIKE`, `ILIKE` (PostgreSQL), `NULL` checks, `IN`, `BETWEEN`, `EXISTS`, and subquery operators - **Subquery support**: `WHERE col IN (SELECT ...)`, `WHERE EXISTS (SELECT ...)`, `WHERE NOT EXISTS (SELECT ...)`, scalar `WHERE col = (SELECT ...)`, FROM-derived table, JOIN subquery, and scalar `SELECT` items -- **Three SQL dialects**: `STANDARD` (ANSI), `MYSQL` (back-tick quoting), `SQLITE` (double-quote) +- **Four SQL dialects**: `STANDARD` (ANSI), `MYSQL` (back-tick quoting), `SQLITE` (double-quote), `POSTGRESQL` (double-quote + `ILIKE` + `RETURNING`) - **Global and per-query configuration** via `QueryBuilderDefaults`: preset dialect, default columns, limit, offset, and LIKE wrapping once at application startup - **In-memory filtering**: `QueryableStorage` functional interface applies the same `Query` @@ -53,7 +53,7 @@ No runtime dependencies required. com.github.EzFramework JavaQueryBuilder - 1.0.4 + 1.1.0 ``` @@ -104,9 +104,9 @@ SqlResult update = QueryBuilder.update("users") | [Installation](installation) | Maven, Gradle, JitPack, GitHub Packages | | [Query Builder](query-builder) | SELECT: `from`, `select`, `where*`, `orderBy`, `build` | | [DML Builders](dml-builders) | `InsertBuilder`, `UpdateBuilder`, `DeleteBuilder`, `CreateBuilder` | -| [Conditions](conditions) | All 16 operators, `Condition`, `ConditionEntry`, `Connector` | +| [Conditions](conditions) | All 18 operators, `Condition`, `ConditionEntry`, `Connector` | | [Subqueries](subqueries) | All six subquery variants | -| [SQL Dialects](sql-dialects) | `STANDARD`, `MYSQL`, `SQLITE`, `SqlResult`, dialect matrix | +| [SQL Dialects](sql-dialects) | `STANDARD`, `MYSQL`, `SQLITE`, `POSTGRESQL`, `SqlResult`, dialect matrix | | [Configuration](configuration) | `QueryBuilderDefaults`: global and per-query dialect, columns, limit, LIKE wrapping | | [In-Memory Filtering](in-memory) | `QueryableStorage`: filter collections without a database | | [Exceptions](exceptions) | Error hierarchy and handling patterns | diff --git a/docs/installation.md b/docs/installation.md index 37937f5..0dbb5b8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -46,7 +46,7 @@ your classpath. com.github.EzFramework JavaQueryBuilder - 1.0.4 + 1.1.0 ``` @@ -66,7 +66,7 @@ repositories { ```kotlin dependencies { - implementation("com.github.EzFramework:JavaQueryBuilder:1.0.4") + implementation("com.github.EzFramework:JavaQueryBuilder:1.1.0") } ``` @@ -102,7 +102,7 @@ authenticate with a personal access token that has `read:packages` scope. com.github.EzFramework java-query-builder - 1.0.4 + 1.1.0 ``` diff --git a/docs/sql-dialects.md b/docs/sql-dialects.md index 1b7d6fc..74f6f38 100644 --- a/docs/sql-dialects.md +++ b/docs/sql-dialects.md @@ -21,11 +21,12 @@ description: "SqlDialect, SqlResult, AbstractSqlDialect, and the dialect matrix" parameterized `SqlResult`. Three built-in dialects are provided as constants on the interface: -| Constant | Identifier quoting | DELETE LIMIT | -|----------|--------------------|--------------| -| `SqlDialect.STANDARD` | None (ANSI) | Not supported | -| `SqlDialect.MYSQL` | Back-tick `` ` `` | Supported | -| `SqlDialect.SQLITE` | Double-quote `"` | Supported | +| Constant | Identifier quoting | DELETE LIMIT | ILIKE | RETURNING | +|----------|--------------------|--------------| ------|----------| +| `SqlDialect.STANDARD` | None (ANSI) | Not supported | No | No | +| `SqlDialect.MYSQL` | Back-tick `` ` `` | Supported | No | No | +| `SqlDialect.SQLITE` | Double-quote `"` | Supported | No | No | +| `SqlDialect.POSTGRESQL` | Double-quote `"` | Not supported | Yes | Yes (DELETE) | --- @@ -54,6 +55,14 @@ SqlResult r3 = new QueryBuilder() .whereEquals("id", 1) .buildSql(SqlDialect.SQLITE); // → SELECT * FROM "users" WHERE "id" = ? + +// PostgreSQL: double-quoted identifiers + ILIKE support +SqlResult r4 = new QueryBuilder() + .from("users") + .whereILike("email", "alice") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "users" WHERE "email" ILIKE ? +// Parameters: ["%alice%"] ``` --- @@ -87,7 +96,8 @@ List params = result.getParameters(); ## Rendering DELETE statements Use `renderDelete(Query)` on a dialect instance to produce `DELETE FROM ...` -statements. This respects the `LIMIT` clause on dialects that support it. +statements. This respects the `LIMIT` clause on dialects that support it and +the `RETURNING` clause on PostgreSQL. ```java Query q = new QueryBuilder() @@ -109,25 +119,30 @@ SqlResult sq = SqlDialect.SQLITE.renderDelete(q); // → DELETE FROM "sessions" WHERE "expired" = ? LIMIT 500 ``` +For PostgreSQL `RETURNING`, use `DeleteBuilder.returning()` — see [DML Builders](dml-builders#returning-clause-postgresql). + --- ## Dialect matrix The same `Query` produces different SQL across dialects due to identifier quoting: -| Feature | STANDARD | MYSQL | SQLITE | -|---------|----------|-------|--------| -| Table quoting | `users` | `` `users` `` | `"users"` | -| Column quoting | `id` | `` `id` `` | `"id"` | -| DELETE LIMIT | No | Yes | Yes | -| Parameter syntax | `?` | `?` | `?` | +| Feature | STANDARD | MYSQL | SQLITE | POSTGRESQL | +|---------|----------|-------|--------|------------| +| Table quoting | `users` | `` `users` `` | `"users"` | `"users"` | +| Column quoting | `id` | `` `id` `` | `"id"` | `"id"` | +| DELETE LIMIT | No | Yes | Yes | No | +| ILIKE / NOT ILIKE | No | No | No | Yes | +| RETURNING on DELETE | No | No | No | Yes | +| Parameter syntax | `?` | `?` | `?` | `?` | --- ## AbstractSqlDialect `AbstractSqlDialect` implements the shared rendering logic for SELECT and DELETE -queries. It is the base class for both `MySqlDialect` and `SqliteDialect`. +queries. It is the base class for `MySqlDialect`, `SqliteDialect`, and +`PostgreSqlDialect`. **Subquery parameter ordering**: parameters are collected depth-first in this order: @@ -137,17 +152,9 @@ order: 3. JOIN subquery parameters (left to right) 4. WHERE condition subquery parameters (top to bottom) -To create a custom dialect (e.g. PostgreSQL with `"..."` quoting), extend -`AbstractSqlDialect` and override `quoteIdentifier`: - -```java -public class PostgreSqlDialect extends AbstractSqlDialect { - @Override - protected String quoteIdentifier(String name) { - return '"' + name + '"'; - } -} -``` +To create a fully custom dialect, extend `AbstractSqlDialect` and override any +combination of `quoteIdentifier`, `supportsDeleteLimit`, `supportsReturning`, +and `appendConditionFragment`. --- @@ -158,5 +165,6 @@ public class PostgreSqlDialect extends AbstractSqlDialect { | `SqlDialect.STANDARD` | ANSI SQL constant instance | | `SqlDialect.MYSQL` | MySQL dialect constant instance | | `SqlDialect.SQLITE` | SQLite dialect constant instance | +| `SqlDialect.POSTGRESQL` | PostgreSQL dialect constant instance | | `render(Query)` | Render a `SELECT` query to `SqlResult` | -| `renderDelete(Query)` | Render a `DELETE` query to `SqlResult`; observes `LIMIT` on supporting dialects | +| `renderDelete(Query)` | Render a `DELETE` query to `SqlResult`; observes `LIMIT` and `RETURNING` on supporting dialects | From 0df46f8e7cd6049b8405876db66730971035b03f Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 22 Apr 2026 22:10:35 +0200 Subject: [PATCH 2/2] docs: split query and dialect docs into per-type submenu pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queries (nav_order 3, has_children): - queries/select.md — SELECT builder (was query-builder.md) - queries/insert.md — InsertBuilder - queries/update.md — UpdateBuilder - queries/delete.md — DeleteBuilder - queries/create.md — CreateBuilder (replaces query-builder.md + dml-builders.md) SQL Dialects (nav_order 6, has_children): - dialects/standard.md — ANSI / no quoting - dialects/mysql.md — back-tick quoting, DELETE LIMIT - dialects/sqlite.md — double-quote quoting, DELETE LIMIT - dialects/postgresql.md — double-quote, ILIKE, RETURNING (replaces sql-dialects.md) - Remove query-builder.md, dml-builders.md, sql-dialects.md - Shift nav_order of conditions (5→4), subqueries (6→5), configuration (8→7), in-memory (9→8), exceptions (10→9), api-reference (11→10) - Update docs/index.md table links to new paths --- docs/api-reference.md | 2 +- docs/conditions.md | 2 +- docs/configuration.md | 2 +- docs/{sql-dialects.md => dialects/index.md} | 108 ++----- docs/dialects/mysql.md | 87 ++++++ docs/dialects/postgresql.md | 162 ++++++++++ docs/dialects/sqlite.md | 87 ++++++ docs/dialects/standard.md | 107 +++++++ docs/dml-builders.md | 300 ------------------- docs/exceptions.md | 2 +- docs/in-memory.md | 2 +- docs/index.md | 5 +- docs/queries/create.md | 81 +++++ docs/queries/delete.md | 149 +++++++++ docs/queries/index.md | 42 +++ docs/queries/insert.md | 82 +++++ docs/{query-builder.md => queries/select.md} | 60 +++- docs/queries/update.md | 96 ++++++ docs/subqueries.md | 2 +- 19 files changed, 967 insertions(+), 411 deletions(-) rename docs/{sql-dialects.md => dialects/index.md} (54%) create mode 100644 docs/dialects/mysql.md create mode 100644 docs/dialects/postgresql.md create mode 100644 docs/dialects/sqlite.md create mode 100644 docs/dialects/standard.md delete mode 100644 docs/dml-builders.md create mode 100644 docs/queries/create.md create mode 100644 docs/queries/delete.md create mode 100644 docs/queries/index.md create mode 100644 docs/queries/insert.md rename docs/{query-builder.md => queries/select.md} (77%) create mode 100644 docs/queries/update.md diff --git a/docs/api-reference.md b/docs/api-reference.md index 7eee228..a1cae1f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,6 +1,6 @@ --- title: API Reference -nav_order: 11 +nav_order: 10 description: "Complete public method tables for every class and interface in JavaQueryBuilder" --- diff --git a/docs/conditions.md b/docs/conditions.md index 91da784..8d08cf6 100644 --- a/docs/conditions.md +++ b/docs/conditions.md @@ -1,6 +1,6 @@ --- title: Conditions -nav_order: 5 +nav_order: 4 description: "Operators, Condition, ConditionEntry, Connector AND/OR, and the orWhere* pattern" --- diff --git a/docs/configuration.md b/docs/configuration.md index 8c36f23..c82296a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ --- title: Configuration -nav_order: 8 +nav_order: 7 description: "QueryBuilderDefaults: global and per-query preset for dialect, columns, limit, offset, and LIKE wrapping" --- diff --git a/docs/sql-dialects.md b/docs/dialects/index.md similarity index 54% rename from docs/sql-dialects.md rename to docs/dialects/index.md index 74f6f38..35b1483 100644 --- a/docs/sql-dialects.md +++ b/docs/dialects/index.md @@ -1,7 +1,9 @@ --- title: SQL Dialects -nav_order: 7 -description: "SqlDialect, SqlResult, AbstractSqlDialect, and the dialect matrix" +nav_order: 6 +has_children: true +permalink: /sql-dialects/ +description: "SqlDialect, SqlResult, AbstractSqlDialect, and the dialect comparison matrix" --- # SQL Dialects @@ -18,52 +20,31 @@ description: "SqlDialect, SqlResult, AbstractSqlDialect, and the dialect matrix" ## Overview `SqlDialect` is a strategy interface that converts a `Query` object into a -parameterized `SqlResult`. Three built-in dialects are provided as constants +parameterized `SqlResult`. Four built-in dialects are provided as constants on the interface: -| Constant | Identifier quoting | DELETE LIMIT | ILIKE | RETURNING | -|----------|--------------------|--------------| ------|----------| -| `SqlDialect.STANDARD` | None (ANSI) | Not supported | No | No | -| `SqlDialect.MYSQL` | Back-tick `` ` `` | Supported | No | No | -| `SqlDialect.SQLITE` | Double-quote `"` | Supported | No | No | -| `SqlDialect.POSTGRESQL` | Double-quote `"` | Not supported | Yes | Yes (DELETE) | +| Constant | Page | Identifier quoting | DELETE LIMIT | ILIKE | RETURNING | +|----------|------|--------------------|--------------|-------|-----------| +| `SqlDialect.STANDARD` | [STANDARD]({{ site.baseurl }}/sql-dialects/standard/) | None (ANSI) | No | No | No | +| `SqlDialect.MYSQL` | [MySQL]({{ site.baseurl }}/sql-dialects/mysql/) | Back-tick `` ` `` | Yes | No | No | +| `SqlDialect.SQLITE` | [SQLite]({{ site.baseurl }}/sql-dialects/sqlite/) | Double-quote `"` | Yes | No | No | +| `SqlDialect.POSTGRESQL` | [PostgreSQL]({{ site.baseurl }}/sql-dialects/postgresql/) | Double-quote `"` | No | Yes | Yes (DELETE) | --- -## Using a dialect +## Dialect matrix -Pass a dialect to `buildSql()` on the builder: +The same `Query` produces different SQL across dialects due to identifier +quoting: -```java -// Standard ANSI -SqlResult r1 = new QueryBuilder() - .from("users") - .whereEquals("id", 1) - .buildSql(); -// → SELECT * FROM users WHERE id = ? - -// MySQL: back-tick quoted identifiers -SqlResult r2 = new QueryBuilder() - .from("users") - .whereEquals("id", 1) - .buildSql(SqlDialect.MYSQL); -// → SELECT * FROM `users` WHERE `id` = ? - -// SQLite: double-quoted identifiers -SqlResult r3 = new QueryBuilder() - .from("users") - .whereEquals("id", 1) - .buildSql(SqlDialect.SQLITE); -// → SELECT * FROM "users" WHERE "id" = ? - -// PostgreSQL: double-quoted identifiers + ILIKE support -SqlResult r4 = new QueryBuilder() - .from("users") - .whereILike("email", "alice") - .buildSql(SqlDialect.POSTGRESQL); -// → SELECT * FROM "users" WHERE "email" ILIKE ? -// Parameters: ["%alice%"] -``` +| Feature | STANDARD | MYSQL | SQLITE | POSTGRESQL | +|---------|----------|-------|--------|------------| +| Table quoting | `users` | `` `users` `` | `"users"` | `"users"` | +| Column quoting | `id` | `` `id` `` | `"id"` | `"id"` | +| DELETE LIMIT | No | Yes | Yes | No | +| ILIKE / NOT ILIKE | No | No | No | Yes | +| RETURNING on DELETE | No | No | No | Yes | +| Parameter syntax | `?` | `?` | `?` | `?` | --- @@ -93,51 +74,6 @@ List params = result.getParameters(); --- -## Rendering DELETE statements - -Use `renderDelete(Query)` on a dialect instance to produce `DELETE FROM ...` -statements. This respects the `LIMIT` clause on dialects that support it and -the `RETURNING` clause on PostgreSQL. - -```java -Query q = new QueryBuilder() - .from("sessions") - .whereEquals("expired", true) - .limit(500) - .build(); - -// Standard: LIMIT ignored -SqlResult std = SqlDialect.STANDARD.renderDelete(q); -// → DELETE FROM sessions WHERE expired = ? - -// MySQL: LIMIT honored -SqlResult my = SqlDialect.MYSQL.renderDelete(q); -// → DELETE FROM `sessions` WHERE `expired` = ? LIMIT 500 - -// SQLite: LIMIT honored -SqlResult sq = SqlDialect.SQLITE.renderDelete(q); -// → DELETE FROM "sessions" WHERE "expired" = ? LIMIT 500 -``` - -For PostgreSQL `RETURNING`, use `DeleteBuilder.returning()` — see [DML Builders](dml-builders#returning-clause-postgresql). - ---- - -## Dialect matrix - -The same `Query` produces different SQL across dialects due to identifier quoting: - -| Feature | STANDARD | MYSQL | SQLITE | POSTGRESQL | -|---------|----------|-------|--------|------------| -| Table quoting | `users` | `` `users` `` | `"users"` | `"users"` | -| Column quoting | `id` | `` `id` `` | `"id"` | `"id"` | -| DELETE LIMIT | No | Yes | Yes | No | -| ILIKE / NOT ILIKE | No | No | No | Yes | -| RETURNING on DELETE | No | No | No | Yes | -| Parameter syntax | `?` | `?` | `?` | `?` | - ---- - ## AbstractSqlDialect `AbstractSqlDialect` implements the shared rendering logic for SELECT and DELETE diff --git a/docs/dialects/mysql.md b/docs/dialects/mysql.md new file mode 100644 index 0000000..b375e01 --- /dev/null +++ b/docs/dialects/mysql.md @@ -0,0 +1,87 @@ +--- +title: MySQL +parent: SQL Dialects +nav_order: 2 +permalink: /sql-dialects/mysql/ +description: "MySQL dialect — back-tick identifier quoting and DELETE LIMIT support" +--- + +# MySQL Dialect +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`SqlDialect.MYSQL` wraps all table and column identifiers in back-ticks +(`` ` ``). It also supports the `LIMIT` clause on `DELETE` statements. +Quoting is applied to SELECT and DELETE queries rendered through this dialect. + +| Feature | Value | +|---------|-------| +| Identifier quoting | Back-tick `` ` `` | +| DELETE LIMIT | Supported | +| ILIKE | Not supported | +| RETURNING on DELETE | Not supported | + +--- + +## SELECT + +```java +SqlResult r = new QueryBuilder() + .from("users") + .select("id", "name", "email") + .whereEquals("status", "active") + .orderBy("name", true) + .buildSql(SqlDialect.MYSQL); +// → SELECT `id`, `name`, `email` FROM `users` WHERE `status` = ? ORDER BY `name` ASC +// Parameters: ["active"] +``` + +--- + +## DELETE with LIMIT + +```java +Query q = new QueryBuilder() + .from("logs") + .whereLessThan("created_at", "2025-01-01") + .limit(1000) + .build(); + +SqlResult result = SqlDialect.MYSQL.renderDelete(q); +// → DELETE FROM `logs` WHERE `created_at` < ? LIMIT 1000 +// Parameters: ["2025-01-01"] +``` + +Or via `DeleteBuilder`: + +```java +SqlResult result = QueryBuilder.deleteFrom("sessions") + .whereEquals("expired", true) + .build(SqlDialect.MYSQL); +// → DELETE FROM `sessions` WHERE `expired` = ? +``` + +--- + +## Identifier quoting coverage + +Back-tick quoting is applied by the dialect to identifiers in SELECT and DELETE +statements. INSERT and UPDATE builders render their own SQL and do not apply +dialect quoting to column or table names. + +--- + +## When to use + +- MySQL and MariaDB +- Any database that uses back-tick quoting convention +- When `DELETE … LIMIT` batching is needed diff --git a/docs/dialects/postgresql.md b/docs/dialects/postgresql.md new file mode 100644 index 0000000..c4d56e9 --- /dev/null +++ b/docs/dialects/postgresql.md @@ -0,0 +1,162 @@ +--- +title: PostgreSQL +parent: SQL Dialects +nav_order: 4 +permalink: /sql-dialects/postgresql/ +description: "PostgreSQL dialect — double-quote quoting, ILIKE operators, and RETURNING on DELETE" +--- + +# PostgreSQL Dialect +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`SqlDialect.POSTGRESQL` wraps all table and column identifiers in double-quotes +and adds two PostgreSQL-specific features: case-insensitive `ILIKE` / `NOT ILIKE` +operators on SELECT queries, and a `RETURNING` clause on `DELETE` statements. + +| Feature | Value | +|---------|-------| +| Identifier quoting | Double-quote `"` | +| DELETE LIMIT | Not supported | +| ILIKE / NOT ILIKE | Supported | +| RETURNING on DELETE | Supported | + +--- + +## SELECT with double-quote quoting + +```java +SqlResult r = new QueryBuilder() + .from("users") + .select("id", "name", "email") + .whereEquals("status", "active") + .orderBy("name", true) + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT "id", "name", "email" FROM "users" WHERE "status" = ? ORDER BY "name" ASC +// Parameters: ["active"] +``` + +--- + +## ILIKE — case-insensitive LIKE + +`ILIKE` is a PostgreSQL extension for case-insensitive pattern matching. +Use `whereILike` / `orWhereILike` on `QueryBuilder` or `SelectBuilder`: + +```java +// Match email containing "alice" (any case) +SqlResult r = new QueryBuilder() + .from("users") + .whereILike("email", "alice") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "users" WHERE "email" ILIKE ? +// Parameters: ["%alice%"] + +// Combine with other conditions +SqlResult r2 = new QueryBuilder() + .from("articles") + .whereEquals("published", true) + .whereILike("title", "java") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "articles" WHERE "published" = ? AND "title" ILIKE ? + +// OR variant +SqlResult r3 = new QueryBuilder() + .from("users") + .whereEquals("role", "admin") + .orWhereILike("name", "bot") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "users" WHERE "role" = ? OR "name" ILIKE ? +``` + +{: .note } +> `whereILike` and `orWhereILike` use the `ILIKE` operator which is only +> rendered correctly by `SqlDialect.POSTGRESQL`. Passing a different dialect +> will render the operator as-is without case-insensitive behaviour. + +--- + +## DELETE with RETURNING + +`RETURNING` on DELETE is rendered by the dialect — it is appended only when +`SqlDialect.POSTGRESQL` is active: + +```java +SqlResult result = QueryBuilder.deleteFrom("users") + .whereEquals("id", 99) + .returning("id", "email") + .build(SqlDialect.POSTGRESQL); + +// → DELETE FROM "users" WHERE "id" = ? RETURNING id, email +// Parameters: [99] +``` + +Without the PostgreSQL dialect the `RETURNING` columns are ignored even if set: + +```java +// RETURNING is silently dropped for non-supporting dialects +SqlResult result = QueryBuilder.deleteFrom("users") + .whereEquals("id", 99) + .returning("id", "email") + .build(SqlDialect.STANDARD); +// → DELETE FROM users WHERE id = ? +``` + +--- + +## INSERT with RETURNING + +For INSERT, `RETURNING` is appended inline regardless of dialect — the caller +is responsible for using a PostgreSQL connection: + +```java +SqlResult result = QueryBuilder.insertInto("users") + .value("name", "Alice") + .value("email", "alice@example.com") + .returning("id", "created_at") + .build(); +// → INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, created_at +// Parameters: ["Alice", "alice@example.com"] +``` + +--- + +## UPDATE with RETURNING + +Same inline behaviour as INSERT: + +```java +SqlResult result = QueryBuilder.update("users") + .set("status", "active") + .whereEquals("id", 7) + .returning("id", "updated_at") + .build(); +// → UPDATE users SET status = ? WHERE id = ? RETURNING id, updated_at +// Parameters: ["active", 7] +``` + +--- + +## Identifier quoting coverage + +Double-quote quoting is applied by the dialect to identifiers in SELECT and +DELETE statements. INSERT and UPDATE builders render their own SQL and do not +apply dialect quoting to column or table names. + +--- + +## When to use + +- PostgreSQL databases +- When you need case-insensitive LIKE matching (`ILIKE`) +- When you need to retrieve affected row values from a DELETE in a single + round-trip (`RETURNING`) diff --git a/docs/dialects/sqlite.md b/docs/dialects/sqlite.md new file mode 100644 index 0000000..5247cba --- /dev/null +++ b/docs/dialects/sqlite.md @@ -0,0 +1,87 @@ +--- +title: SQLite +parent: SQL Dialects +nav_order: 3 +permalink: /sql-dialects/sqlite/ +description: "SQLite dialect — double-quote identifier quoting and DELETE LIMIT support" +--- + +# SQLite Dialect +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`SqlDialect.SQLITE` wraps all table and column identifiers in double-quotes +(`"`), which is the SQL standard quoting character that SQLite follows. It also +supports the `LIMIT` clause on `DELETE` statements. + +| Feature | Value | +|---------|-------| +| Identifier quoting | Double-quote `"` | +| DELETE LIMIT | Supported | +| ILIKE | Not supported | +| RETURNING on DELETE | Not supported | + +--- + +## SELECT + +```java +SqlResult r = new QueryBuilder() + .from("users") + .select("id", "name", "email") + .whereEquals("status", "active") + .orderBy("name", true) + .buildSql(SqlDialect.SQLITE); +// → SELECT "id", "name", "email" FROM "users" WHERE "status" = ? ORDER BY "name" ASC +// Parameters: ["active"] +``` + +--- + +## DELETE with LIMIT + +```java +Query q = new QueryBuilder() + .from("cache") + .whereLessThan("expires_at", "2025-01-01") + .limit(500) + .build(); + +SqlResult result = SqlDialect.SQLITE.renderDelete(q); +// → DELETE FROM "cache" WHERE "expires_at" < ? LIMIT 500 +// Parameters: ["2025-01-01"] +``` + +Or via `DeleteBuilder`: + +```java +SqlResult result = QueryBuilder.deleteFrom("sessions") + .whereEquals("expired", true) + .build(SqlDialect.SQLITE); +// → DELETE FROM "sessions" WHERE "expired" = ? +``` + +--- + +## Identifier quoting coverage + +Double-quote quoting is applied by the dialect to identifiers in SELECT and +DELETE statements. INSERT and UPDATE builders render their own SQL and do not +apply dialect quoting to column or table names. + +--- + +## When to use + +- SQLite databases +- Any SQL-standard database that uses double-quote identifiers +- When `DELETE … LIMIT` batching is needed diff --git a/docs/dialects/standard.md b/docs/dialects/standard.md new file mode 100644 index 0000000..ae27da7 --- /dev/null +++ b/docs/dialects/standard.md @@ -0,0 +1,107 @@ +--- +title: STANDARD +parent: SQL Dialects +nav_order: 1 +permalink: /sql-dialects/standard/ +description: "ANSI SQL dialect — no identifier quoting, no dialect-specific extensions" +--- + +# STANDARD Dialect +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`SqlDialect.STANDARD` produces ANSI-compliant SQL with no identifier quoting. +It is the default dialect used by `buildSql()` and `build()` when no dialect +is specified. + +| Feature | Value | +|---------|-------| +| Identifier quoting | None | +| DELETE LIMIT | Not supported | +| ILIKE | Not supported | +| RETURNING on DELETE | Not supported | + +--- + +## SELECT + +`STANDARD` is the implicit default; you can pass it explicitly for clarity: + +```java +SqlResult r = new QueryBuilder() + .from("users") + .select("id", "name") + .whereEquals("status", "active") + .buildSql(SqlDialect.STANDARD); +// → SELECT id, name FROM users WHERE status = ? +// Parameters: ["active"] + +// Equivalent — no dialect argument means STANDARD +SqlResult r2 = new QueryBuilder() + .from("users") + .whereEquals("id", 1) + .buildSql(); +// → SELECT * FROM users WHERE id = ? +``` + +--- + +## DML statements + +`STANDARD` is also the default for all DML builders when calling `build()` with +no argument: + +```java +SqlResult insert = QueryBuilder.insertInto("orders") + .value("product_id", 7) + .value("qty", 2) + .build(); +// → INSERT INTO orders (product_id, qty) VALUES (?, ?) + +SqlResult update = QueryBuilder.update("orders") + .set("qty", 3) + .whereEquals("id", 1) + .build(); +// → UPDATE orders SET qty = ? WHERE id = ? + +SqlResult delete = QueryBuilder.deleteFrom("sessions") + .whereEquals("expired", true) + .build(); +// → DELETE FROM sessions WHERE expired = ? +``` + +--- + +## DELETE LIMIT + +`STANDARD` does not support a `LIMIT` clause on `DELETE`. If a limit is set on +the `Query` it is silently ignored: + +```java +Query q = new QueryBuilder() + .from("sessions") + .whereEquals("expired", true) + .limit(500) + .build(); + +SqlResult r = SqlDialect.STANDARD.renderDelete(q); +// → DELETE FROM sessions WHERE expired = ? +// (LIMIT 500 is dropped) +``` + +--- + +## When to use + +- Any ANSI-compatible database that does not need quoted identifiers +- Unit tests and integration tests where quoting does not matter +- When you want the simplest rendered SQL for debugging diff --git a/docs/dml-builders.md b/docs/dml-builders.md deleted file mode 100644 index fbfaa58..0000000 --- a/docs/dml-builders.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: DML Builders -nav_order: 4 -description: "INSERT, UPDATE, DELETE, and CREATE TABLE builders" ---- - -# DML Builders -{: .no_toc } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -`QueryBuilder` provides static factory methods that return dedicated builder -objects for every DML and DDL statement type: - -| Factory method | Builder | Statement | -|----------------|---------|-----------| -| `QueryBuilder.insertInto(table)` | `InsertBuilder` | `INSERT INTO` | -| `QueryBuilder.update(table)` | `UpdateBuilder` | `UPDATE` | -| `QueryBuilder.deleteFrom(table)` | `DeleteBuilder` | `DELETE FROM` | -| `QueryBuilder.createTable(table)` | `CreateBuilder` | `CREATE TABLE` | - -Each builder returns a `SqlResult` from its `build()` method, giving you the -rendered SQL string and the ordered bind-parameter list. - ---- - -## InsertBuilder - -### Basic insert - -```java -SqlResult result = QueryBuilder.insertInto("users") - .value("name", "Alice") - .value("email", "alice@example.com") - .value("age", 30) - .build(); - -// result.getSql() → INSERT INTO users (name, email, age) VALUES (?, ?, ?) -// result.getParameters() → ["Alice", "alice@example.com", 30] -``` - -### Dialect-aware insert - -```java -SqlResult result = QueryBuilder.insertInto("users") - .value("name", "Bob") - .build(SqlDialect.MYSQL); -``` - -### Method reference - -| Method | Returns | Description | -|--------|---------|-------------| -| `into(String table)` | `InsertBuilder` | Set target table (also available via factory) | -| `value(String col, Object val)` | `InsertBuilder` | Add a column/value pair | -| `returning(String... cols)` | `InsertBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | -| `build()` | `SqlResult` | Render with standard dialect | -| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | - ---- - -## UpdateBuilder - -### Basic update - -```java -SqlResult result = QueryBuilder.update("users") - .set("status", "inactive") - .set("updated_at", "2026-01-01") - .whereEquals("id", 42) - .build(); - -// → UPDATE users SET status = ?, updated_at = ? WHERE id = ? -// Parameters: ["inactive", "2026-01-01", 42] -``` - -### Multiple conditions - -```java -SqlResult result = QueryBuilder.update("products") - .set("price", 9.99) - .whereEquals("category", "sale") - .whereGreaterThanOrEquals("stock", 1) - .build(); -``` - -### OR condition - -```java -SqlResult result = QueryBuilder.update("users") - .set("role", "user") - .whereEquals("role", "guest") - .orWhereEquals("role", "temp") - .build(); -// → UPDATE users SET role = ? WHERE role = ? OR role = ? -``` - -### Method reference - -| Method | Returns | Description | -|--------|---------|-------------| -| `table(String table)` | `UpdateBuilder` | Set target table | -| `set(String col, Object val)` | `UpdateBuilder` | Add a SET column/value pair | -| `whereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (AND) | -| `orWhereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (OR) | -| `whereGreaterThanOrEquals(String col, int val)` | `UpdateBuilder` | `WHERE col >= ?` (AND) | -| `returning(String... cols)` | `UpdateBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | -| `build()` | `SqlResult` | Render with standard dialect | -| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | - ---- - -## DeleteBuilder - -### Basic delete - -```java -SqlResult result = QueryBuilder.deleteFrom("sessions") - .whereEquals("user_id", 99) - .build(); - -// → DELETE FROM sessions WHERE user_id = ? -// Parameters: [99] -``` - -### Multiple conditions - -```java -SqlResult result = QueryBuilder.deleteFrom("logs") - .whereLessThan("created_at", "2025-01-01") - .whereEquals("level", "debug") - .build(); -``` - -### IN / NOT IN - -```java -SqlResult result = QueryBuilder.deleteFrom("users") - .whereIn("status", List.of("banned", "deleted")) - .build(); -// → DELETE FROM users WHERE status IN (?, ?) -``` - -### BETWEEN - -```java -SqlResult result = QueryBuilder.deleteFrom("events") - .whereBetween("score", 0, 10) - .build(); -// → DELETE FROM events WHERE score BETWEEN ? AND ? -``` - -### Dialect-aware delete (with LIMIT support) - -MySQL and SQLite support a `LIMIT` clause on `DELETE`. Use `renderDelete` via -the dialect directly when you have a `Query` object: - -```java -Query q = new QueryBuilder() - .from("logs") - .whereLessThan("age", 30) - .limit(100) - .build(); - -SqlResult result = SqlDialect.MYSQL.renderDelete(q); -// → DELETE FROM `logs` WHERE `age` < ? LIMIT 100 -``` - -### Method reference - -| Method | Returns | Description | -|--------|---------|-------------| -| `from(String table)` | `DeleteBuilder` | Set target table | -| `whereEquals(col, val)` | `DeleteBuilder` | `WHERE col = ?` (AND) | -| `whereNotEquals(col, val)` | `DeleteBuilder` | `WHERE col != ?` (AND) | -| `whereGreaterThan(col, val)` | `DeleteBuilder` | `WHERE col > ?` (AND) | -| `whereGreaterThanOrEquals(col, val)` | `DeleteBuilder` | `WHERE col >= ?` (AND) | -| `whereLessThan(col, val)` | `DeleteBuilder` | `WHERE col < ?` (AND) | -| `whereLessThanOrEquals(col, val)` | `DeleteBuilder` | `WHERE col <= ?` (AND) | -| `whereIn(col, List)` | `DeleteBuilder` | `WHERE col IN (...)` (AND) | -| `whereNotIn(col, List)` | `DeleteBuilder` | `WHERE col NOT IN (...)` (AND) | -| `whereBetween(col, from, to)` | `DeleteBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | -| `returning(String... cols)` | `DeleteBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only; use with `SqlDialect.POSTGRESQL`) | -| `build()` | `SqlResult` | Render with standard dialect | -| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | - ---- - -## RETURNING clause (PostgreSQL) -{: #returning-clause-postgresql } - -All three DML builders support a `RETURNING` clause for PostgreSQL, which -lets the database return column values from affected rows in a single round-trip. - -### DELETE … RETURNING - -`RETURNING` on `DELETE` is rendered by the dialect — it is only appended when -`SqlDialect.POSTGRESQL` (or another dialect that overrides `supportsReturning()`) -is active. - -```java -SqlResult result = QueryBuilder.deleteFrom("users") - .whereEquals("id", 99) - .returning("id", "email") - .build(SqlDialect.POSTGRESQL); - -// → DELETE FROM "users" WHERE "id" = ? RETURNING id, email -// Parameters: [99] -``` - -### INSERT … RETURNING - -`RETURNING` on `INSERT` is appended inline regardless of dialect — the caller -is responsible for using a PostgreSQL connection. - -```java -SqlResult result = QueryBuilder.insertInto("users") - .value("name", "Alice") - .value("email", "alice@example.com") - .returning("id", "created_at") - .build(); - -// → INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, created_at -// Parameters: ["Alice", "alice@example.com"] -``` - -### UPDATE … RETURNING - -```java -SqlResult result = QueryBuilder.update("users") - .set("status", "active") - .whereEquals("id", 7) - .returning("id", "updated_at") - .build(); - -// → UPDATE users SET status = ? WHERE id = ? RETURNING id, updated_at -// Parameters: ["active", 7] -``` - ---- - -## CreateBuilder - -### Basic CREATE TABLE - -```java -SqlResult result = QueryBuilder.createTable("users") - .column("id", "INT") - .column("name", "VARCHAR(64)") - .column("email", "VARCHAR(255)") - .primaryKey("id") - .build(); - -// → CREATE TABLE users (id INT, name VARCHAR(64), email VARCHAR(255), PRIMARY KEY (id)) -``` - -### IF NOT EXISTS - -```java -SqlResult result = QueryBuilder.createTable("sessions") - .ifNotExists() - .column("token", "VARCHAR(128)") - .column("user_id", "INT") - .column("expires_at", "TIMESTAMP") - .primaryKey("token") - .build(); - -// → CREATE TABLE IF NOT EXISTS sessions (token VARCHAR(128), ..., PRIMARY KEY (token)) -``` - -### Composite primary key - -```java -SqlResult result = QueryBuilder.createTable("user_roles") - .column("user_id", "INT") - .column("role_id", "INT") - .primaryKey("user_id") - .primaryKey("role_id") - .build(); -// → ... PRIMARY KEY (user_id, role_id) -``` - -### Method reference - -| Method | Returns | Description | -|--------|---------|-------------| -| `table(String name)` | `CreateBuilder` | Set table name | -| `column(String name, String sqlType)` | `CreateBuilder` | Add column definition | -| `primaryKey(String name)` | `CreateBuilder` | Declare a primary key column | -| `ifNotExists()` | `CreateBuilder` | Add `IF NOT EXISTS` guard | -| `build()` | `SqlResult` | Render; throws `IllegalStateException` if table or columns are missing | -| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | diff --git a/docs/exceptions.md b/docs/exceptions.md index 6503ab2..ea56e14 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -1,6 +1,6 @@ --- title: Exceptions -nav_order: 10 +nav_order: 9 description: "Exception hierarchy, when each exception is thrown, and handling patterns" --- diff --git a/docs/in-memory.md b/docs/in-memory.md index dde83ec..83236aa 100644 --- a/docs/in-memory.md +++ b/docs/in-memory.md @@ -1,6 +1,6 @@ --- title: In-Memory Filtering -nav_order: 9 +nav_order: 8 description: "Filtering in-memory collections with QueryableStorage" --- diff --git a/docs/index.md b/docs/index.md index 165c42a..bfa3ed8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -102,11 +102,10 @@ SqlResult update = QueryBuilder.update("users") | Page | What it covers | |------|----------------| | [Installation](installation) | Maven, Gradle, JitPack, GitHub Packages | -| [Query Builder](query-builder) | SELECT: `from`, `select`, `where*`, `orderBy`, `build` | -| [DML Builders](dml-builders) | `InsertBuilder`, `UpdateBuilder`, `DeleteBuilder`, `CreateBuilder` | +| [Queries]({{ site.baseurl }}/queries/) | SELECT, INSERT, UPDATE, DELETE, CREATE TABLE builders | | [Conditions](conditions) | All 18 operators, `Condition`, `ConditionEntry`, `Connector` | | [Subqueries](subqueries) | All six subquery variants | -| [SQL Dialects](sql-dialects) | `STANDARD`, `MYSQL`, `SQLITE`, `POSTGRESQL`, `SqlResult`, dialect matrix | +| [SQL Dialects]({{ site.baseurl }}/sql-dialects/) | `STANDARD`, `MYSQL`, `SQLITE`, `POSTGRESQL`, `SqlResult`, dialect matrix | | [Configuration](configuration) | `QueryBuilderDefaults`: global and per-query dialect, columns, limit, LIKE wrapping | | [In-Memory Filtering](in-memory) | `QueryableStorage`: filter collections without a database | | [Exceptions](exceptions) | Error hierarchy and handling patterns | diff --git a/docs/queries/create.md b/docs/queries/create.md new file mode 100644 index 0000000..22faf4c --- /dev/null +++ b/docs/queries/create.md @@ -0,0 +1,81 @@ +--- +title: CREATE TABLE +parent: Queries +nav_order: 5 +permalink: /queries/create/ +description: "Building CREATE TABLE statements with CreateBuilder" +--- + +# CREATE TABLE +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`CreateBuilder` builds `CREATE TABLE` statements with optional `IF NOT EXISTS` +guards and composite primary keys: + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = QueryBuilder.createTable("users") + .column("id", "INT") + .column("name", "VARCHAR(64)") + .column("email", "VARCHAR(255)") + .primaryKey("id") + .build(); + +// → CREATE TABLE users (id INT, name VARCHAR(64), email VARCHAR(255), PRIMARY KEY (id)) +``` + +--- + +## IF NOT EXISTS + +```java +SqlResult result = QueryBuilder.createTable("sessions") + .ifNotExists() + .column("token", "VARCHAR(128)") + .column("user_id", "INT") + .column("expires_at", "TIMESTAMP") + .primaryKey("token") + .build(); + +// → CREATE TABLE IF NOT EXISTS sessions (token VARCHAR(128), user_id INT, expires_at TIMESTAMP, PRIMARY KEY (token)) +``` + +--- + +## Composite primary key + +```java +SqlResult result = QueryBuilder.createTable("user_roles") + .column("user_id", "INT") + .column("role_id", "INT") + .primaryKey("user_id") + .primaryKey("role_id") + .build(); + +// → CREATE TABLE user_roles (user_id INT, role_id INT, PRIMARY KEY (user_id, role_id)) +``` + +--- + +## Method reference + +| Method | Returns | Description | +|--------|---------|-------------| +| `table(String name)` | `CreateBuilder` | Set table name | +| `column(String name, String sqlType)` | `CreateBuilder` | Add a column definition | +| `primaryKey(String name)` | `CreateBuilder` | Declare a primary key column | +| `ifNotExists()` | `CreateBuilder` | Add `IF NOT EXISTS` guard | +| `build()` | `SqlResult` | Render; throws `IllegalStateException` if table or columns are missing | +| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | diff --git a/docs/queries/delete.md b/docs/queries/delete.md new file mode 100644 index 0000000..96077a5 --- /dev/null +++ b/docs/queries/delete.md @@ -0,0 +1,149 @@ +--- +title: DELETE +parent: Queries +nav_order: 4 +permalink: /queries/delete/ +description: "Building DELETE statements with DeleteBuilder" +--- + +# DELETE +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`DeleteBuilder` builds parameterized `DELETE FROM … WHERE` statements: + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = QueryBuilder.deleteFrom("sessions") + .whereEquals("user_id", 99) + .build(); + +// → DELETE FROM sessions WHERE user_id = ? +// Parameters: [99] +``` + +--- + +## Multiple conditions + +```java +SqlResult result = QueryBuilder.deleteFrom("logs") + .whereLessThan("created_at", "2025-01-01") + .whereEquals("level", "debug") + .build(); +// → DELETE FROM logs WHERE created_at < ? AND level = ? +``` + +--- + +## IN / NOT IN + +```java +SqlResult result = QueryBuilder.deleteFrom("users") + .whereIn("status", List.of("banned", "deleted")) + .build(); +// → DELETE FROM users WHERE status IN (?, ?) + +SqlResult result2 = QueryBuilder.deleteFrom("products") + .whereNotIn("category", List.of("archive", "draft")) + .build(); +// → DELETE FROM products WHERE category NOT IN (?, ?) +``` + +--- + +## BETWEEN + +```java +SqlResult result = QueryBuilder.deleteFrom("events") + .whereBetween("score", 0, 10) + .build(); +// → DELETE FROM events WHERE score BETWEEN ? AND ? +``` + +--- + +## Dialect-aware delete (LIMIT support) + +MySQL and SQLite support a `LIMIT` clause on `DELETE`. Pass a `Query` built with +a `limit()` call to `renderDelete` on the appropriate dialect: + +```java +Query q = new QueryBuilder() + .from("logs") + .whereLessThan("age", 30) + .limit(100) + .build(); + +// MySQL — LIMIT honored, back-tick quoting applied +SqlResult mysql = SqlDialect.MYSQL.renderDelete(q); +// → DELETE FROM `logs` WHERE `age` < ? LIMIT 100 + +// SQLite — LIMIT honored, double-quote quoting applied +SqlResult sqlite = SqlDialect.SQLITE.renderDelete(q); +// → DELETE FROM "logs" WHERE "age" < ? LIMIT 100 + +// STANDARD — LIMIT dropped +SqlResult std = SqlDialect.STANDARD.renderDelete(q); +// → DELETE FROM logs WHERE age < ? +``` + +Alternatively, use `DeleteBuilder.build(SqlDialect)` which also calls +`renderDelete` internally: + +```java +SqlResult result = QueryBuilder.deleteFrom("logs") + .whereLessThan("age", 30) + .build(SqlDialect.MYSQL); +// → DELETE FROM `logs` WHERE `age` < ? +// (limit requires a Query built via QueryBuilder.build().limit()) +``` + +--- + +## RETURNING (PostgreSQL) + +`RETURNING` on DELETE is rendered by the dialect — it is appended only when +`SqlDialect.POSTGRESQL` (or a dialect that overrides `supportsReturning()`) is +used. + +```java +SqlResult result = QueryBuilder.deleteFrom("users") + .whereEquals("id", 99) + .returning("id", "email") + .build(SqlDialect.POSTGRESQL); + +// → DELETE FROM "users" WHERE "id" = ? RETURNING id, email +// Parameters: [99] +``` + +--- + +## Method reference + +| Method | Returns | Description | +|--------|---------|-------------| +| `from(String table)` | `DeleteBuilder` | Set target table | +| `whereEquals(col, val)` | `DeleteBuilder` | `WHERE col = ?` (AND) | +| `whereNotEquals(col, val)` | `DeleteBuilder` | `WHERE col != ?` (AND) | +| `whereGreaterThan(col, val)` | `DeleteBuilder` | `WHERE col > ?` (AND) | +| `whereGreaterThanOrEquals(col, val)` | `DeleteBuilder` | `WHERE col >= ?` (AND) | +| `whereLessThan(col, val)` | `DeleteBuilder` | `WHERE col < ?` (AND) | +| `whereLessThanOrEquals(col, val)` | `DeleteBuilder` | `WHERE col <= ?` (AND) | +| `whereIn(col, List)` | `DeleteBuilder` | `WHERE col IN (...)` (AND) | +| `whereNotIn(col, List)` | `DeleteBuilder` | `WHERE col NOT IN (...)` (AND) | +| `whereBetween(col, from, to)` | `DeleteBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | +| `returning(String... cols)` | `DeleteBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only; use with `SqlDialect.POSTGRESQL`) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | diff --git a/docs/queries/index.md b/docs/queries/index.md new file mode 100644 index 0000000..cb01afc --- /dev/null +++ b/docs/queries/index.md @@ -0,0 +1,42 @@ +--- +title: Queries +nav_order: 3 +has_children: true +permalink: /queries/ +description: "Building SQL queries with JavaQueryBuilder's fluent API" +--- + +# Queries +{: .no_toc } + +`QueryBuilder` is the entry point for all statement types. It provides a fluent +API for SELECT queries on its instance, and static factory methods that return +dedicated builder objects for DML and DDL statements. + +--- + +## Statement types + +| Statement | Builder | Entry point | +|-----------|---------|-------------| +| [SELECT]({{ site.baseurl }}/queries/select/) | `QueryBuilder` | `new QueryBuilder()` | +| [INSERT]({{ site.baseurl }}/queries/insert/) | `InsertBuilder` | `QueryBuilder.insertInto(table)` | +| [UPDATE]({{ site.baseurl }}/queries/update/) | `UpdateBuilder` | `QueryBuilder.update(table)` | +| [DELETE]({{ site.baseurl }}/queries/delete/) | `DeleteBuilder` | `QueryBuilder.deleteFrom(table)` | +| [CREATE TABLE]({{ site.baseurl }}/queries/create/) | `CreateBuilder` | `QueryBuilder.createTable(table)` | + +--- + +## SqlResult + +Every builder returns a `SqlResult` from its `build()` or `buildSql()` method: + +| Method | Returns | Description | +|--------|---------|-------------| +| `getSql()` | `String` | The rendered SQL with `?` placeholders | +| `getParameters()` | `List` | Bind parameters in order of appearance | + +All user-supplied values are placed in the `?` bind-parameter list and are +**never** concatenated into the SQL string. See +[SQL Dialects]({{ site.baseurl }}/sql-dialects/) for how identifier quoting +behaves across database targets. diff --git a/docs/queries/insert.md b/docs/queries/insert.md new file mode 100644 index 0000000..d49e758 --- /dev/null +++ b/docs/queries/insert.md @@ -0,0 +1,82 @@ +--- +title: INSERT +parent: Queries +nav_order: 2 +permalink: /queries/insert/ +description: "Building INSERT statements with InsertBuilder" +--- + +# INSERT +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`InsertBuilder` builds parameterized `INSERT INTO` statements. Create one via +the `QueryBuilder` factory method: + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = QueryBuilder.insertInto("users") + .value("name", "Alice") + .value("email", "alice@example.com") + .value("age", 30) + .build(); + +// result.getSql() → INSERT INTO users (name, email, age) VALUES (?, ?, ?) +// result.getParameters() → ["Alice", "alice@example.com", 30] +``` + +--- + +## Basic insert + +```java +SqlResult result = QueryBuilder.insertInto("orders") + .value("product_id", 7) + .value("qty", 2) + .value("status", "pending") + .build(); +// → INSERT INTO orders (product_id, qty, status) VALUES (?, ?, ?) +// Parameters: [7, 2, "pending"] +``` + +--- + +## RETURNING (PostgreSQL) + +`RETURNING` is appended inline after the closing `)`. The caller is responsible +for using a PostgreSQL connection; the clause is emitted regardless of which +dialect is passed to `build()`. + +```java +SqlResult result = QueryBuilder.insertInto("users") + .value("name", "Alice") + .value("email", "alice@example.com") + .returning("id", "created_at") + .build(); + +// → INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, created_at +// Parameters: ["Alice", "alice@example.com"] +``` + +--- + +## Method reference + +| Method | Returns | Description | +|--------|---------|-------------| +| `into(String table)` | `InsertBuilder` | Set target table (also available via factory) | +| `value(String col, Object val)` | `InsertBuilder` | Add a column/value pair | +| `returning(String... cols)` | `InsertBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | diff --git a/docs/query-builder.md b/docs/queries/select.md similarity index 77% rename from docs/query-builder.md rename to docs/queries/select.md index 6041098..0670218 100644 --- a/docs/query-builder.md +++ b/docs/queries/select.md @@ -1,10 +1,12 @@ --- -title: Query Builder -nav_order: 3 +title: SELECT +parent: Queries +nav_order: 1 +permalink: /queries/select/ description: "Building SELECT queries with the fluent QueryBuilder API" --- -# Query Builder +# SELECT {: .no_toc } ## Table of contents @@ -17,10 +19,9 @@ description: "Building SELECT queries with the fluent QueryBuilder API" ## Overview -`QueryBuilder` is the main entry point for SELECT queries. Its fluent API lets -you compose any SELECT statement by chaining method calls and then calling -`build()` (returns a `Query` object) or `buildSql()` (returns a `SqlResult` -ready for execution). +`QueryBuilder` provides a fluent API for composing SELECT statements. Chain +method calls and finish with `build()` (returns a `Query` object) or +`buildSql()` (returns a `SqlResult` ready for execution). ```java import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; @@ -39,8 +40,11 @@ String sql = result.getSql(); List params = result.getParameters(); ``` -`QueryBuilder` is also the gateway to all DML builders via its static factory -methods. See [DML Builders](dml-builders). +For DML statements (INSERT, UPDATE, DELETE, CREATE TABLE) use the static factory +methods. See [INSERT]({{ site.baseurl }}/queries/insert/), +[UPDATE]({{ site.baseurl }}/queries/update/), +[DELETE]({{ site.baseurl }}/queries/delete/), and +[CREATE TABLE]({{ site.baseurl }}/queries/create/). --- @@ -55,7 +59,7 @@ new QueryBuilder().from("orders") ## Selecting columns ```java -// SELECT * (default - no columns specified) +// SELECT * (default — no columns specified) new QueryBuilder().from("users") // SELECT id, name @@ -130,6 +134,30 @@ new QueryBuilder() --- +## ILIKE (PostgreSQL) + +For case-insensitive LIKE matching on PostgreSQL, use `whereILike` and +`orWhereILike`: + +```java +SqlResult result = new QueryBuilder() + .from("users") + .whereILike("email", "alice") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "users" WHERE "email" ILIKE ? +// Parameters: ["%alice%"] + +// OR variant +new QueryBuilder() + .from("users") + .whereEquals("role", "admin") + .orWhereILike("name", "bot") + .buildSql(SqlDialect.POSTGRESQL); +// → SELECT * FROM "users" WHERE "role" = ? OR "name" ILIKE ? +``` + +--- + ## Ordering ```java @@ -185,7 +213,7 @@ Pass a raw SQL fragment with no value interpolation. Use static expressions only ## Building the result -### `build()` (returns a `Query`) +### `build()` — returns a `Query` `build()` produces a `Query` object which can be passed to a `SqlDialect` later, used for in-memory filtering with `QueryableStorage`, or inspected directly: @@ -197,13 +225,13 @@ Query q = new QueryBuilder() .build(); ``` -### `buildSql()` (returns a `SqlResult`) +### `buildSql()` — returns a `SqlResult` `buildSql()` renders the `Query` immediately using the standard ANSI dialect. Use the overloads to specify a table or dialect explicitly: ```java -// Uses table set via from(), standard dialect +// Table set via from(), standard dialect SqlResult r1 = builder.buildSql(); // Explicit table, standard dialect @@ -213,8 +241,8 @@ SqlResult r2 = builder.buildSql("orders"); SqlResult r3 = builder.buildSql("orders", SqlDialect.MYSQL); ``` -See [SQL Dialects](sql-dialects) for the dialect options and the rendered -identifier differences. +See [SQL Dialects]({{ site.baseurl }}/sql-dialects/) for the dialect options and +identifier-quoting differences across database targets. --- @@ -232,7 +260,7 @@ identifier differences. | `joinSubquery(subquery, alias, on)` | `INNER JOIN (SELECT ...) AS alias ON ...` | | `selectSubquery(subquery, alias)` | `(SELECT ...) AS alias` in SELECT clause | -See [Subqueries](subqueries) for full examples. +See [Subqueries]({{ site.baseurl }}/subqueries/) for full examples. --- diff --git a/docs/queries/update.md b/docs/queries/update.md new file mode 100644 index 0000000..360e925 --- /dev/null +++ b/docs/queries/update.md @@ -0,0 +1,96 @@ +--- +title: UPDATE +parent: Queries +nav_order: 3 +permalink: /queries/update/ +description: "Building UPDATE statements with UpdateBuilder" +--- + +# UPDATE +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`UpdateBuilder` builds parameterized `UPDATE … SET … WHERE` statements: + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = QueryBuilder.update("users") + .set("status", "inactive") + .set("updated_at", "2026-01-01") + .whereEquals("id", 42) + .build(); + +// → UPDATE users SET status = ?, updated_at = ? WHERE id = ? +// Parameters: ["inactive", "2026-01-01", 42] +``` + +--- + +## Multiple conditions + +```java +SqlResult result = QueryBuilder.update("products") + .set("price", 9.99) + .whereEquals("category", "sale") + .whereGreaterThanOrEquals("stock", 1) + .build(); +// → UPDATE products SET price = ? WHERE category = ? AND stock >= ? +``` + +--- + +## OR condition + +```java +SqlResult result = QueryBuilder.update("users") + .set("role", "user") + .whereEquals("role", "guest") + .orWhereEquals("role", "temp") + .build(); +// → UPDATE users SET role = ? WHERE role = ? OR role = ? +``` + +--- + +## RETURNING (PostgreSQL) + +`RETURNING` is appended inline after the `WHERE` clause. The caller is +responsible for using a PostgreSQL connection; the clause is emitted regardless +of which dialect is passed to `build()`. + +```java +SqlResult result = QueryBuilder.update("users") + .set("status", "active") + .whereEquals("id", 7) + .returning("id", "updated_at") + .build(); + +// → UPDATE users SET status = ? WHERE id = ? RETURNING id, updated_at +// Parameters: ["active", 7] +``` + +--- + +## Method reference + +| Method | Returns | Description | +|--------|---------|-------------| +| `table(String table)` | `UpdateBuilder` | Set target table | +| `set(String col, Object val)` | `UpdateBuilder` | Add a SET column/value pair | +| `whereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (AND) | +| `orWhereEquals(String col, Object val)` | `UpdateBuilder` | `WHERE col = ?` (OR) | +| `whereGreaterThanOrEquals(String col, int val)` | `UpdateBuilder` | `WHERE col >= ?` (AND) | +| `returning(String... cols)` | `UpdateBuilder` | Append `RETURNING col1, col2, ...` (PostgreSQL only) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | diff --git a/docs/subqueries.md b/docs/subqueries.md index e446027..108e14b 100644 --- a/docs/subqueries.md +++ b/docs/subqueries.md @@ -1,6 +1,6 @@ --- title: Subqueries -nav_order: 6 +nav_order: 5 description: "All six subquery variants: IN, EXISTS, NOT EXISTS, scalar, FROM-derived table, JOIN, and scalar SELECT" ---