From 246eef53a966b05919e18b1ec400d2c3507f6b78 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 18:06:22 +0200 Subject: [PATCH 01/11] docs: added public Markdown documentation through Github Pages --- .github/workflows/docs.yml | 42 +++ .github/workflows/lint.yml | 21 ++ .markdownlint.yml | 12 + README.md | 4 + docs/Gemfile | 4 + docs/_config.yml | 63 ++++ .../_sass/color_schemes/javaquerybuilder.scss | 42 +++ docs/api-reference.md | 330 ++++++++++++++++++ docs/conditions.md | 159 +++++++++ docs/dml-builders.md | 244 +++++++++++++ docs/exceptions.md | 169 +++++++++ docs/in-memory.md | 165 +++++++++ docs/index.md | 110 ++++++ docs/installation.md | 127 +++++++ docs/query-builder.md | 260 ++++++++++++++ docs/sql-dialects.md | 162 +++++++++ docs/subqueries.md | 218 ++++++++++++ 17 files changed, 2132 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .markdownlint.yml create mode 100644 docs/Gemfile create mode 100644 docs/_config.yml create mode 100644 docs/_sass/color_schemes/javaquerybuilder.scss create mode 100644 docs/api-reference.md create mode 100644 docs/conditions.md create mode 100644 docs/dml-builders.md create mode 100644 docs/exceptions.md create mode 100644 docs/in-memory.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/query-builder.md create mode 100644 docs/sql-dialects.md create mode 100644 docs/subqueries.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9879e5a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,42 @@ +name: Documentation + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..10962b1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + pull_request: + paths: + - "docs/**/*.md" + - ".markdownlint.yml" + +jobs: + markdown: + name: Validate Markdown + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Lint Markdown + uses: DavidAnson/markdownlint-cli2-action@v16 + with: + globs: "docs/**/*.md" + config: ".markdownlint.yml" diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..39e5272 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,12 @@ +default: true +MD013: false +MD022: false +MD025: + front_matter_title: "" +MD033: false +MD036: false +MD041: false +MD024: + siblings_only: true +MD007: + indent: 2 diff --git a/README.md b/README.md index 919624c..542caae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # JavaQueryBuilder +[![JitPack](https://jitpack.io/v/EzFramework/JavaQueryBuilder.svg)](https://jitpack.io/#EzFramework/JavaQueryBuilder) +[![GitHub Packages](https://img.shields.io/github/v/release/EzFramework/JavaQueryBuilder?label=GitHub%20Packages&logo=github)](https://github.com/EzFramework/JavaQueryBuilder/packages) +[![codecov](https://codecov.io/gh/EzFramework/JavaQueryBuilder/branch/main/graph/badge.svg)](https://codecov.io/gh/EzFramework/JavaQueryBuilder) + A lightweight, fluent Java library for building parameterized SQL queries and filtering in-memory data, no runtime dependencies required. ## Features diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..7359a80 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "github-pages", group: :jekyll_plugins +gem "just-the-docs" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..01c03d0 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,63 @@ +title: JavaQueryBuilder +description: >- + A lightweight, fluent Java library for building parameterized SQL queries + and filtering in-memory data. No runtime dependencies required. + +url: "https://ezframework.github.io" +baseurl: "/JavaQueryBuilder" + +# ── Appearance ──────────────────────────────────────────────────────────────── +color_scheme: javaquerybuilder +heading_anchors: true + +# ── Header links ────────────────────────────────────────────────────────────── +aux_links: + "GitHub": + - "https://github.com/EzFramework/JavaQueryBuilder" + "JitPack": + - "https://jitpack.io/#EzFramework/JavaQueryBuilder" + +aux_links_new_tab: true + +# ── Navigation ──────────────────────────────────────────────────────────────── +nav_sort: case_insensitive +nav_external_links: + - title: Changelog + url: "https://github.com/EzFramework/JavaQueryBuilder/releases" + hide_icon: false + +# ── Search ──────────────────────────────────────────────────────────────────── +search_enabled: true +search: + heading_level: 2 + previews: 3 + preview_words_before: 5 + preview_words_after: 10 + tokenizer_separator: /[\s/]+/ + +# ── Footer ──────────────────────────────────────────────────────────────────── +back_to_top: true +back_to_top_text: "Back to top" + +footer_content: >- + Copyright © 2024–2026 Gyvex. + Distributed under the + MIT License. + +# ── Kramdown ────────────────────────────────────────────────────────────────── +kramdown: + syntax_highlighter_opts: + block: + line_numbers: false + +# ── Plugins ─────────────────────────────────────────────────────────────────── +plugins: + - jekyll-remote-theme + - jekyll-seo-tag + +# ── Build exclusions ────────────────────────────────────────────────────────── +exclude: + - Gemfile + - Gemfile.lock + +remote_theme: just-the-docs/just-the-docs diff --git a/docs/_sass/color_schemes/javaquerybuilder.scss b/docs/_sass/color_schemes/javaquerybuilder.scss new file mode 100644 index 0000000..15a9e01 --- /dev/null +++ b/docs/_sass/color_schemes/javaquerybuilder.scss @@ -0,0 +1,42 @@ +// JavaQueryBuilder — dark/blue/white color scheme for just-the-docs +// +// Palette: +// Background #141414 (body) +// Surface #1c1c1c (sidebar, cards) +// Elevated #222222 (code blocks, search, table rows) +// Border #2e2e2e +// Text #d0d0d0 (body) / #ffffff (headings) +// Accent #4d9de0 (blue — links, nav highlight, buttons) +// Accent dim #3a7bbf (hover state) + +$color-scheme: dark; + +// ── Surfaces ───────────────────────────────────────────────────────────────── +$body-background-color: #141414; +$sidebar-color: #1c1c1c; +$feedback-color: #181818; + +// ── Typography ──────────────────────────────────────────────────────────────── +$body-text-color: #d0d0d0; +$body-heading-color: #ffffff; + +// ── Links & accent ─────────────────────────────────────────────────────────── +$link-color: #4d9de0; +$btn-primary-color: #4d9de0; + +// ── Borders ─────────────────────────────────────────────────────────────────── +$border-color: #2e2e2e; + +// ── Code ────────────────────────────────────────────────────────────────────── +$code-background-color: #222222; + +// ── Tables ──────────────────────────────────────────────────────────────────── +$table-background-color: #1a1a1a; + +// ── Search ──────────────────────────────────────────────────────────────────── +$search-background-color: #222222; +$search-foreground-color: #c0c0c0; +$search-border-color: #333333; + +// ── Buttons ─────────────────────────────────────────────────────────────────── +$base-button-color: #252525; diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..3558521 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,330 @@ +--- +title: API Reference +nav_order: 10 +description: "Complete public method tables for every class and interface in JavaQueryBuilder" +--- + +# API Reference +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## builder + +### `QueryBuilder` + +Main entry point for SELECT queries and static gateway to DML builders. + +**Static factory methods** + +| Method | Returns | Description | +|--------|---------|-------------| +| `insert()` | `InsertBuilder` | New `InsertBuilder` | +| `insertInto(String table)` | `InsertBuilder` | New `InsertBuilder` pre-set to `table` | +| `update()` | `UpdateBuilder` | New `UpdateBuilder` | +| `update(String table)` | `UpdateBuilder` | New `UpdateBuilder` pre-set to `table` | +| `delete()` | `DeleteBuilder` | New `DeleteBuilder` | +| `deleteFrom(String table)` | `DeleteBuilder` | New `DeleteBuilder` pre-set to `table` | +| `createTable()` | `CreateBuilder` | New `CreateBuilder` | +| `createTable(String table)` | `CreateBuilder` | New `CreateBuilder` pre-set to `table` | + +**SELECT builder methods** + +| Method | Returns | Description | +|--------|---------|-------------| +| `from(String table)` | `QueryBuilder` | Set source table | +| `select(String... columns)` | `QueryBuilder` | Add columns to SELECT clause; omit for `SELECT *` | +| `distinct()` | `QueryBuilder` | Add `DISTINCT` to SELECT | +| `whereEquals(col, val)` | `QueryBuilder` | `WHERE col = ?` (AND) | +| `orWhereEquals(col, val)` | `QueryBuilder` | `WHERE col = ?` (OR) | +| `whereNotEquals(col, val)` | `QueryBuilder` | `WHERE col != ?` (AND) | +| `whereGreaterThan(col, val)` | `QueryBuilder` | `WHERE col > ?` (AND) | +| `whereGreaterThanOrEquals(col, val)` | `QueryBuilder` | `WHERE col >= ?` (AND) | +| `whereLessThan(col, val)` | `QueryBuilder` | `WHERE col < ?` (AND) | +| `whereLessThanOrEquals(col, val)` | `QueryBuilder` | `WHERE col <= ?` (AND) | +| `whereLike(col, String val)` | `QueryBuilder` | `WHERE col LIKE ?` (AND) | +| `whereNotLike(col, String val)` | `QueryBuilder` | `WHERE col NOT LIKE ?` (AND) | +| `whereNull(col)` | `QueryBuilder` | `WHERE col IS NULL` (AND) | +| `whereNotNull(col)` | `QueryBuilder` | `WHERE col IS NOT NULL` (AND) | +| `whereExists(col)` | `QueryBuilder` | `WHERE col IS NOT NULL` (AND) | +| `whereIn(col, List)` | `QueryBuilder` | `WHERE col IN (...)` (AND) | +| `whereNotIn(col, List)` | `QueryBuilder` | `WHERE col NOT IN (...)` (AND) | +| `whereBetween(col, a, b)` | `QueryBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | +| `whereInSubquery(col, Query)` | `QueryBuilder` | `WHERE col IN (SELECT ...)` (AND) | +| `whereEqualsSubquery(col, Query)` | `QueryBuilder` | `WHERE col = (SELECT ...)` (AND) | +| `whereExistsSubquery(Query)` | `QueryBuilder` | `WHERE EXISTS (SELECT ...)` (AND) | +| `whereNotExistsSubquery(Query)` | `QueryBuilder` | `WHERE NOT EXISTS (SELECT ...)` (AND) | +| `fromSubquery(Query, String alias)` | `QueryBuilder` | Replace `FROM` with a derived-table subquery | +| `joinSubquery(Query, String alias, String on)` | `QueryBuilder` | `INNER JOIN (SELECT ...) AS alias ON ...` | +| `selectSubquery(Query, String alias)` | `QueryBuilder` | Add `(SELECT ...) AS alias` to SELECT list | +| `groupBy(String... columns)` | `QueryBuilder` | Add `GROUP BY` columns | +| `havingRaw(String clause)` | `QueryBuilder` | Set raw `HAVING` SQL fragment | +| `orderBy(String col, boolean asc)` | `QueryBuilder` | Add `ORDER BY` column; `true` = ASC | +| `limit(int n)` | `QueryBuilder` | Set `LIMIT` | +| `offset(int n)` | `QueryBuilder` | Set `OFFSET` | +| `build()` | `Query` | Build a `Query` object (no SQL rendered yet) | +| `buildSql()` | `SqlResult` | Render SELECT using table set via `from()`, standard dialect | +| `buildSql(String table)` | `SqlResult` | Render SELECT for explicit `table`, standard dialect | +| `buildSql(String table, SqlDialect)` | `SqlResult` | Render SELECT for explicit `table` and dialect | + +--- + +### `SelectBuilder` + +Lower-level SELECT builder that produces `SqlResult` directly (no `Query` intermediary). + +| Method | Returns | Description | +|--------|---------|-------------| +| `from(String table)` | `SelectBuilder` | Set source table | +| `select(String... columns)` | `SelectBuilder` | Add SELECT columns | +| `distinct()` | `SelectBuilder` | Add `DISTINCT` | +| `whereEquals(col, val)` | `SelectBuilder` | `WHERE col = ?` | +| `whereIn(col, List)` | `SelectBuilder` | `WHERE col IN (...)` | +| `whereLike(col, String val)` | `SelectBuilder` | `WHERE col LIKE ?` | +| `groupBy(String... columns)` | `SelectBuilder` | Add `GROUP BY` | +| `orderBy(String col, boolean asc)` | `SelectBuilder` | Add `ORDER BY` | +| `limit(int n)` | `SelectBuilder` | Set `LIMIT` | +| `offset(int n)` | `SelectBuilder` | Set `OFFSET` | +| `build(SqlDialect)` | `SqlResult` | Render SELECT with given dialect | + +--- + +### `InsertBuilder` + +| Method | Returns | Description | +|--------|---------|-------------| +| `into(String table)` | `InsertBuilder` | Set target table | +| `value(String col, Object val)` | `InsertBuilder` | Add a column/value pair | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect)` | `SqlResult` | Render with specified dialect | + +--- + +### `UpdateBuilder` + +| Method | Returns | Description | +|--------|---------|-------------| +| `table(String table)` | `UpdateBuilder` | Set target table | +| `set(String col, Object val)` | `UpdateBuilder` | Add a SET pair | +| `whereEquals(col, val)` | `UpdateBuilder` | `WHERE col = ?` (AND) | +| `orWhereEquals(col, val)` | `UpdateBuilder` | `WHERE col = ?` (OR) | +| `whereGreaterThanOrEquals(col, int val)` | `UpdateBuilder` | `WHERE col >= ?` (AND) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect)` | `SqlResult` | Render with specified dialect | + +--- + +### `DeleteBuilder` + +| 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); throws `IllegalArgumentException` if list is null/empty | +| `whereNotIn(col, List)` | `DeleteBuilder` | `WHERE col NOT IN (...)` (AND); throws `IllegalArgumentException` if list is null/empty | +| `whereBetween(col, from, to)` | `DeleteBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect)` | `SqlResult` | Render with specified dialect | + +--- + +### `CreateBuilder` + +| 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` | +| `build()` | `SqlResult` | Render; throws `IllegalStateException` if table or columns are missing | +| `build(SqlDialect)` | `SqlResult` | Render with specified dialect | + +--- + +## condition + +### `Operator` + +Enum of comparison operators. See [Conditions](conditions) for the full table. + +| Constant | SQL | +|----------|-----| +| `EQ` | `= ?` | +| `NEQ` | `!= ?` | +| `GT` | `> ?` | +| `GTE` | `>= ?` | +| `LT` | `< ?` | +| `LTE` | `<= ?` | +| `LIKE` | `LIKE ?` | +| `NOT_LIKE` | `NOT LIKE ?` | +| `EXISTS` | `IS NOT NULL` | +| `IS_NULL` | `IS NULL` | +| `IS_NOT_NULL` | `IS NOT NULL` | +| `IN` | `IN (...)` | +| `NOT_IN` | `NOT IN (...)` | +| `BETWEEN` | `BETWEEN ? AND ?` | +| `EXISTS_SUBQUERY` | `EXISTS (SELECT ...)` | +| `NOT_EXISTS_SUBQUERY` | `NOT EXISTS (SELECT ...)` | + +--- + +### `Condition` + +| Member | Description | +|--------|-------------| +| `Condition(Operator op, Object value)` | Create a condition; `value` may be `null` | +| `getOperator()` | Returns the `Operator` | +| `getValue()` | Returns the comparison value (`null`, scalar, `List`, or `Query`) | +| `matches(Map map, String key)` | Evaluate against an in-memory attribute map | + +--- + +### `ConditionEntry` + +| Member | Description | +|--------|-------------| +| `ConditionEntry(String col, Condition cond, Connector connector)` | Create a condition entry | +| `getColumn()` | Column name (`null` for EXISTS-subquery conditions) | +| `getCondition()` | The wrapped `Condition` | +| `getConnector()` | `AND` or `OR` | + +--- + +### `Connector` + +| Constant | SQL keyword | +|----------|-------------| +| `AND` | `AND` | +| `OR` | `OR` | + +--- + +## query + +### `Query` + +Immutable data holder produced by `QueryBuilder.build()`. All fields have +getters and setters; setters are used exclusively by the builders. + +| Getter | Type | Description | +|--------|------|-------------| +| `getTable()` | `String` | Source table name | +| `getSelectColumns()` | `List` | Columns in SELECT clause; empty = `SELECT *` | +| `isDistinct()` | `boolean` | Whether `DISTINCT` is active | +| `getConditions()` | `List` | WHERE conditions | +| `getGroupBy()` | `List` | GROUP BY columns | +| `getHavingRaw()` | `String` | Raw HAVING fragment | +| `getOrderBy()` | `List` | ORDER BY columns | +| `getOrderByAsc()` | `List` | True = ASC per ORDER BY entry | +| `getLimit()` | `Integer` | LIMIT value; `0` or negative = no limit | +| `getOffset()` | `Integer` | OFFSET value | +| `getFromSubquery()` | `Query` | FROM-derived subquery; `null` for plain table | +| `getFromAlias()` | `String` | Alias for FROM subquery | +| `getJoins()` | `List` | JOIN clauses | +| `getSelectSubqueries()` | `List` | Scalar SELECT subquery items | + +--- + +### `JoinClause` + +| Member | Description | +|--------|-------------| +| `JoinClause(Type, String table, String on)` | Plain-table join | +| `JoinClause(Type, Query subquery, String alias, String on)` | Subquery (derived-table) join | +| `getType()` | `JoinClause.Type` — `INNER`, `LEFT`, `RIGHT`, or `CROSS` | +| `getTable()` | Table name for plain-table join; `null` for subquery join | +| `getSubquery()` | Subquery for derived-table join; `null` for plain-table join | +| `getAlias()` | Alias for derived-table join | +| `getOnCondition()` | Raw SQL `ON` fragment | + +--- + +### `ScalarSelectItem` + +| Member | Description | +|--------|-------------| +| `ScalarSelectItem(Query subquery, String alias)` | Create a scalar SELECT item | +| `getSubquery()` | The subquery to embed | +| `getAlias()` | Column alias in SELECT clause | + +--- + +### `QueryableStorage` + +Functional interface for in-memory filtering. + +```java +@FunctionalInterface +public interface QueryableStorage { + List query(Query q) throws Exception; +} +``` + +--- + +## sql + +### `SqlDialect` + +| Member | Description | +|--------|-------------| +| `STANDARD` | ANSI SQL — no identifier quoting | +| `MYSQL` | MySQL — back-tick quoting; DELETE LIMIT supported | +| `SQLITE` | SQLite — double-quote quoting; DELETE LIMIT supported | +| `render(Query)` | Render a SELECT query to `SqlResult` | +| `renderDelete(Query)` | Render a DELETE query to `SqlResult` | + +--- + +### `SqlResult` + +| Method | Returns | Description | +|--------|---------|-------------| +| `getSql()` | `String` | Rendered SQL with `?` placeholders | +| `getParameters()` | `List` | Bind parameters in placeholder order | + +--- + +## exception + +### `QueryBuilderException` + +| Constructor | Description | +|-------------|-------------| +| `QueryBuilderException()` | No-message default | +| `QueryBuilderException(String message)` | Simple message | +| `QueryBuilderException(String message, Throwable cause)` | Wraps another exception | +| `QueryBuilderException(Throwable cause)` | Re-throws | + +--- + +### `QueryException` + +| Constructor | Description | +|-------------|-------------| +| `QueryException()` | No-message default | +| `QueryException(String message)` | Simple message | +| `QueryException(String message, Throwable cause)` | Wraps another exception | +| `QueryException(Throwable cause)` | Re-throws | + +--- + +### `QueryRenderException` + +| Constructor | Description | +|-------------|-------------| +| `QueryRenderException()` | No-message default | +| `QueryRenderException(String message)` | Simple message | +| `QueryRenderException(String message, Throwable cause)` | Wraps another exception | +| `QueryRenderException(Throwable cause)` | Re-throws | diff --git a/docs/conditions.md b/docs/conditions.md new file mode 100644 index 0000000..2474755 --- /dev/null +++ b/docs/conditions.md @@ -0,0 +1,159 @@ +--- +title: Conditions +nav_order: 5 +description: "Operators, Condition, ConditionEntry, Connector AND/OR, and the orWhere* pattern" +--- + +# Conditions +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Every `where*` call on a builder creates a `ConditionEntry` — a triple of: + +- a **column name** (or `null` for EXISTS subquery conditions) +- a **`Condition`** (operator + value) +- a **`Connector`** (`AND` or `OR`) + +The first condition in a builder always uses `AND` as its connector. Subsequent +conditions use `AND` by default; call the `orWhere*` variant to use `OR`. + +--- + +## Operator enum + +`Operator` is the single source of truth for all supported comparison operators. + +| Constant | SQL rendering | Notes | +|----------|---------------|-------| +| `EQ` | `col = ?` | Equality | +| `NEQ` | `col != ?` | Not equal | +| `GT` | `col > ?` | Greater than | +| `GTE` | `col >= ?` | Greater than or equal | +| `LT` | `col < ?` | Less than | +| `LTE` | `col <= ?` | Less than or equal | +| `LIKE` | `col LIKE ?` | Substring match | +| `NOT_LIKE` | `col NOT LIKE ?` | Negated substring match | +| `EXISTS` | `col IS NOT NULL` | Column existence check (alias for `IS_NOT_NULL`) | +| `IS_NULL` | `col IS NULL` | Column is null | +| `IS_NOT_NULL` | `col IS NOT NULL` | Column is not null | +| `IN` | `col IN (...)` | Collection membership; value is a `List` or a `Query` subquery | +| `NOT_IN` | `col NOT IN (...)` | Negated collection membership | +| `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` | + +--- + +## Builder method to operator mapping + +The table below maps every `QueryBuilder` `where*` method to its `Operator` +constant and the SQL it generates. + +| Builder method | Operator | Generated SQL fragment | +|----------------|----------|------------------------| +| `whereEquals(col, val)` | `EQ` | `col = ?` | +| `orWhereEquals(col, val)` | `EQ` | `OR col = ?` | +| `whereNotEquals(col, val)` | `NEQ` | `col != ?` | +| `whereGreaterThan(col, val)` | `GT` | `col > ?` | +| `whereGreaterThanOrEquals(col, val)` | `GTE` | `col >= ?` | +| `whereLessThan(col, val)` | `LT` | `col < ?` | +| `whereLessThanOrEquals(col, val)` | `LTE` | `col <= ?` | +| `whereLike(col, val)` | `LIKE` | `col LIKE ?` | +| `whereNotLike(col, val)` | `NOT_LIKE` | `col NOT LIKE ?` | +| `whereNull(col)` | `IS_NULL` | `col IS NULL` | +| `whereNotNull(col)` | `IS_NOT_NULL` | `col IS NOT NULL` | +| `whereExists(col)` | `EXISTS` | `col IS NOT NULL` | +| `whereIn(col, List)` | `IN` | `col IN (?, ?, ...)` | +| `whereNotIn(col, List)` | `NOT_IN` | `col NOT IN (?, ?, ...)` | +| `whereBetween(col, a, b)` | `BETWEEN` | `col BETWEEN ? AND ?` | +| `whereInSubquery(col, Query)` | `IN` | `col IN (SELECT ...)` | +| `whereEqualsSubquery(col, Query)` | `EQ` | `col = (SELECT ...)` | +| `whereExistsSubquery(Query)` | `EXISTS_SUBQUERY` | `EXISTS (SELECT ...)` | +| `whereNotExistsSubquery(Query)` | `NOT_EXISTS_SUBQUERY` | `NOT EXISTS (SELECT ...)` | + +--- + +## AND vs OR connector + +### Default — AND + +All `where*` methods use `AND`: + +```java +new QueryBuilder() + .from("users") + .whereEquals("role", "admin") + .whereEquals("active", true) +// → WHERE role = ? AND active = ? +``` + +### OR — use the `orWhere*` variant + +```java +new QueryBuilder() + .from("users") + .whereEquals("role", "admin") + .orWhereEquals("role", "moderator") +// → WHERE role = ? OR role = ? +``` + +### Mixing AND and OR + +Conditions are rendered in the order they are added. There is no explicit +grouping with parentheses at the builder level. + +```java +new QueryBuilder() + .from("users") + .whereEquals("active", true) + .whereEquals("role", "admin") + .orWhereEquals("role", "moderator") +// → WHERE active = ? AND role = ? OR role = ? +``` + +--- + +## Condition class + +`Condition` pairs an `Operator` with its comparison value. + +| Member | Type | Description | +|--------|------|-------------| +| `Condition(Operator op, Object value)` | constructor | Create a condition; `value` may be `null` for `IS_NULL`, `IS_NOT_NULL`, `EXISTS` | +| `getOperator()` | `Operator` | The operator for this condition | +| `getValue()` | `Object` | The comparison value (`null`, scalar, `List`, or `Query`) | +| `matches(Map map, String key)` | `boolean` | Evaluate against an in-memory attribute map | + +The `matches` method is used by `QueryableStorage` for in-memory filtering +without a database. + +--- + +## ConditionEntry class + +`ConditionEntry` wraps a `Condition` with its column name and `Connector`. + +| Member | Type | Description | +|--------|------|-------------| +| `ConditionEntry(String column, Condition condition, Connector connector)` | constructor | Create a condition entry | +| `getColumn()` | `String` | The column name (`null` for `EXISTS_SUBQUERY` / `NOT_EXISTS_SUBQUERY`) | +| `getCondition()` | `Condition` | The wrapped condition | +| `getConnector()` | `Connector` | `AND` or `OR` | + +--- + +## Connector enum + +| Constant | SQL keyword | +|----------|-------------| +| `AND` | `AND` | +| `OR` | `OR` | diff --git a/docs/dml-builders.md b/docs/dml-builders.md new file mode 100644 index 0000000..68323dd --- /dev/null +++ b/docs/dml-builders.md @@ -0,0 +1,244 @@ +--- +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 | +| `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) | +| `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) | +| `build()` | `SqlResult` | Render with standard dialect | +| `build(SqlDialect dialect)` | `SqlResult` | Render with specified dialect | + +--- + +## 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 new file mode 100644 index 0000000..de54e23 --- /dev/null +++ b/docs/exceptions.md @@ -0,0 +1,169 @@ +--- +title: Exceptions +nav_order: 9 +description: "Exception hierarchy, when each exception is thrown, and handling patterns" +--- + +# Exceptions +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Hierarchy + +The library defines three standalone checked exceptions in the +`com.github.ezframework.javaquerybuilder.query.exception` package. +None extends the others — each signals a distinct failure mode. + +```text +Exception + ├── QueryBuilderException — general builder or configuration error + ├── QueryException — query-level runtime error + └── QueryRenderException — SQL rendering error +``` + +All three share the same four constructor signatures. + +--- + +## QueryBuilderException + +Thrown for general errors produced during query building or configuration. + +```java +try { + // ... builder usage that may throw +} catch (QueryBuilderException e) { + log.error("Builder error: {}", e.getMessage(), e); +} +``` + +**Constructors:** + +| Constructor | Use case | +|-------------|----------| +| `QueryBuilderException()` | No-message default | +| `QueryBuilderException(String message)` | Simple message | +| `QueryBuilderException(String message, Throwable cause)` | Wraps another exception | +| `QueryBuilderException(Throwable cause)` | Re-throws without adding a message | + +--- + +## QueryException + +Thrown for runtime errors at the query level — for example, when a +`QueryableStorage` implementation encounters an error during in-memory +evaluation. + +```java +try { + List ids = store.query(q); +} catch (QueryException e) { + log.error("Query evaluation failed: {}", e.getMessage(), e); +} +``` + +**Constructors:** + +| Constructor | Use case | +|-------------|----------| +| `QueryException()` | No-message default | +| `QueryException(String message)` | Simple message | +| `QueryException(String message, Throwable cause)` | Wraps another exception | +| `QueryException(Throwable cause)` | Re-throws without adding a message | + +--- + +## QueryRenderException + +Thrown when a `Query` cannot be rendered to SQL — for example, if required +fields are missing or the query state is inconsistent at render time. + +```java +try { + SqlResult result = dialect.render(query); +} catch (QueryRenderException e) { + log.error("SQL rendering failed: {}", e.getMessage(), e); +} +``` + +**Constructors:** + +| Constructor | Use case | +|-------------|----------| +| `QueryRenderException()` | No-message default | +| `QueryRenderException(String message)` | Simple message | +| `QueryRenderException(String message, Throwable cause)` | Wraps another exception | +| `QueryRenderException(Throwable cause)` | Re-throws without adding a message | + +--- + +## Best practices + +### Catch the most specific type first + +```java +try { + SqlResult result = dialect.render(query); + List ids = store.query(q); +} +catch (QueryRenderException e) { + // Rendering failed — log and return a safe error response +} +catch (QueryException e) { + // In-memory evaluation failed +} +catch (QueryBuilderException e) { + // Configuration or builder error +} +``` + +### Do not expose raw exception messages to API callers + +Exception messages may contain internal column names or values. Map exceptions +to safe, generic responses before returning them to external clients. + +```java +// CORRECT — map to a safe API response +catch (QueryRenderException e) { + return Response.serverError().entity("Query rendering error").build(); +} + +// WRONG — leaks internal details +catch (QueryRenderException e) { + return Response.serverError().entity(e.getMessage()).build(); +} +``` + +### `CreateBuilder` throws `IllegalStateException` + +`CreateBuilder.build()` throws `IllegalStateException` (not a checked exception) +when the table name or columns are missing. Guard these calls with a null/empty +check before building: + +```java +if (table != null && !columns.isEmpty()) { + SqlResult result = QueryBuilder.createTable(table) + // ... columns ... + .build(); +} +``` + +### `DeleteBuilder` throws `IllegalArgumentException` + +`DeleteBuilder.whereIn()` and `whereNotIn()` throw `IllegalArgumentException` +when the value list is `null` or empty. Validate the list before calling: + +```java +if (ids != null && !ids.isEmpty()) { + SqlResult result = QueryBuilder.deleteFrom("users") + .whereIn("id", ids) + .build(); +} +``` diff --git a/docs/in-memory.md b/docs/in-memory.md new file mode 100644 index 0000000..ef48c7b --- /dev/null +++ b/docs/in-memory.md @@ -0,0 +1,165 @@ +--- +title: In-Memory Filtering +nav_order: 8 +description: "Filtering in-memory collections with QueryableStorage" +--- + +# In-Memory Filtering +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`QueryableStorage` is a functional interface that lets you apply the same +`Query` object you would pass to a SQL database to an in-memory collection +instead. This is useful for unit testing, local caching layers, or any scenario +where you want a consistent filtering API regardless of the backing store. + +```java +@FunctionalInterface +public interface QueryableStorage { + List query(Query q) throws Exception; +} +``` + +The interface intentionally returns `List` (string IDs) so that the +caller controls how records are loaded by ID after filtering. + +--- + +## How it works + +Each `ConditionEntry` in the `Query` holds a `Condition` with an `Operator` and +a value. `Condition.matches(Map, String key)` evaluates the +condition against an attribute map — no SQL dialect or database connection is +required. + +The `QueryableStorage` implementation is responsible for: + +1. Iterating over the local collection. +2. Calling `condition.matches(attributes, column)` for each item. +3. Respecting `AND` / `OR` connectors between conditions. +4. Applying `ORDER BY`, `LIMIT`, and `OFFSET` if desired. +5. Returning the IDs of matching records. + +--- + +## Example implementation + +```java +import com.github.ezframework.javaquerybuilder.query.Query; +import com.github.ezframework.javaquerybuilder.query.QueryableStorage; +import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; +import com.github.ezframework.javaquerybuilder.query.condition.Connector; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class InMemoryStore implements QueryableStorage { + + private final Map> data; + + public InMemoryStore(Map> data) { + this.data = data; + } + + @Override + public List query(Query q) { + final List result = new ArrayList<>(); + + for (Map.Entry> entry : data.entrySet()) { + final String id = entry.getKey(); + final Map attrs = entry.getValue(); + + if (matches(attrs, q.getConditions())) { + result.add(id); + } + } + + // Respect LIMIT / OFFSET + final int offset = q.getOffset() != null ? q.getOffset() : 0; + final int limit = q.getLimit() != null ? q.getLimit() : 0; + + final List sliced = result.subList( + Math.min(offset, result.size()), + result.size() + ); + return limit > 0 + ? sliced.subList(0, Math.min(limit, sliced.size())) + : sliced; + } + + private boolean matches(Map attrs, List conditions) { + if (conditions.isEmpty()) { + return true; + } + boolean result = true; + for (int i = 0; i < conditions.size(); i++) { + final ConditionEntry entry = conditions.get(i); + final boolean condResult = + entry.getCondition().matches(attrs, entry.getColumn()); + + if (i == 0) { + result = condResult; + } else if (entry.getConnector() == Connector.OR) { + result = result || condResult; + } else { + result = result && condResult; + } + } + return result; + } +} +``` + +--- + +## Using the store + +```java +Map> data = Map.of( + "1", Map.of("name", "Alice", "role", "admin", "active", true), + "2", Map.of("name", "Bob", "role", "user", "active", true), + "3", Map.of("name", "Carol", "role", "admin", "active", false) +); + +QueryableStorage store = new InMemoryStore(data); + +Query q = new QueryBuilder() + .whereEquals("role", "admin") + .whereEquals("active", true) + .build(); + +List ids = store.query(q); +// → ["1"] (Alice: admin + active) +``` + +--- + +## Supported operators in-memory + +`Condition.matches` evaluates the following operators against an attribute map: + +| Operator | In-memory behaviour | +|----------|---------------------| +| `EQ` | `Objects.equals(stored, value)` | +| `NEQ` | `!Objects.equals(stored, value)` | +| `GT` / `GTE` / `LT` / `LTE` | Numeric comparison; coerces `Long`/`Integer`/`Double` as needed | +| `LIKE` | `stored.toString().contains(value)` — substring match | +| `NOT_LIKE` | Negated `LIKE` | +| `IS_NULL` | `!map.containsKey(key) \|\| map.get(key) == null` | +| `IS_NOT_NULL` / `EXISTS` | `map.containsKey(key) && map.get(key) != null` | +| `IN` | `((List) value).contains(stored)` | +| `NOT_IN` | `!((List) value).contains(stored)` | +| `BETWEEN` | `stored >= list.get(0) && stored <= list.get(1)` | + +Subquery operators (`EXISTS_SUBQUERY`, `NOT_EXISTS_SUBQUERY`) are not meaningful +in an in-memory context and return `false` by default. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..53ccc1a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,110 @@ +--- +layout: home +title: JavaQueryBuilder +nav_order: 1 +description: "A lightweight, fluent Java library for building parameterized SQL queries" +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) + +**JavaQueryBuilder** is a lightweight, fluent Java library for building +parameterized SQL queries and filtering in-memory data. +No runtime dependencies required. + +--- + +## Features + +- **Fluent SELECT builder** — `from`, `select`, `distinct`, `where*`, + `orderBy`, `groupBy`, `havingRaw`, `limit`, `offset` +- **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`, + `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) +- **In-memory filtering** — `QueryableStorage` functional interface applies the same `Query` + to flat-map collections without touching a database +- **Zero runtime dependencies** — pure Java 25+, nothing to shade or exclude + +--- + +## Quick start + +**1. Add JavaQueryBuilder via JitPack:** + +```xml + + + jitpack.io + https://jitpack.io + + + + + com.github.EzFramework + JavaQueryBuilder + 1.0.4 + +``` + +**2. Build a SELECT query:** + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = new QueryBuilder() + .from("users") + .select("id", "name", "email") + .whereEquals("status", "active") + .whereGreaterThan("age", 18) + .orderBy("name", true) + .limit(20) + .buildSql(SqlDialect.MYSQL); + +String sql = result.getSql(); // SELECT `id`, `name`, `email` FROM `users` WHERE ... +List params = result.getParameters(); // ["active", 18] +``` + +**3. Build an INSERT:** + +```java +SqlResult insert = QueryBuilder.insertInto("users") + .value("name", "Alice") + .value("email", "alice@example.com") + .build(); +``` + +**4. Update with a condition:** + +```java +SqlResult update = QueryBuilder.update("users") + .set("status", "inactive") + .whereEquals("id", 42) + .build(); +``` + +--- + +## Documentation + +| 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` | +| [Conditions](conditions) | All 16 operators, `Condition`, `ConditionEntry`, `Connector` | +| [Subqueries](subqueries) | All six subquery variants | +| [SQL Dialects](sql-dialects) | `STANDARD`, `MYSQL`, `SQLITE`, `SqlResult`, dialect matrix | +| [In-Memory Filtering](in-memory) | `QueryableStorage` — filter collections without a database | +| [Exceptions](exceptions) | Error hierarchy and handling patterns | +| [API Reference](api-reference) | Full public-method tables for every class | diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..899bf9a --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,127 @@ +--- +title: Installation +nav_order: 2 +description: "Add JavaQueryBuilder to your Java project via JitPack or GitHub Packages" +--- + +# Installation +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Requirements + +| Requirement | Minimum version | +|-------------|----------------| +| Java | **25** | +| Build tool | Maven **3.8+** or Gradle **8+** | + +JavaQueryBuilder has **zero runtime dependencies**. Nothing extra is pulled into +your classpath. + +--- + +## Maven + +### 1. Add the JitPack repository + +```xml + + + jitpack.io + https://jitpack.io + + +``` + +### 2. Add the dependency + +```xml + + com.github.EzFramework + JavaQueryBuilder + 1.0.4 + +``` + +--- + +## Gradle (Kotlin DSL) + +### 1. Add the JitPack repository + +```kotlin +repositories { + maven("https://jitpack.io") +} +``` + +### 2. Add the dependency + +```kotlin +dependencies { + implementation("com.github.EzFramework:JavaQueryBuilder:1.0.4") +} +``` + +--- + +## GitHub Packages + +JavaQueryBuilder is also published to GitHub Packages. To consume it from there, +authenticate with a personal access token that has `read:packages` scope. + +**`~/.m2/settings.xml`:** + +```xml + + + github + YOUR_GITHUB_USERNAME + YOUR_GITHUB_PAT + + +``` + +**`pom.xml`:** + +```xml + + + github + https://maven.pkg.github.com/EzFramework/JavaQueryBuilder + + + + + com.github.EzFramework + java-query-builder + 1.0.4 + +``` + +--- + +## Verifying the installation + +Add this snippet to a test class — it should compile and run without errors: + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = new QueryBuilder() + .from("test") + .whereEquals("id", 1) + .buildSql(); + +System.out.println(result.getSql()); // SELECT * FROM test WHERE id = ? +System.out.println(result.getParameters()); // [1] +System.out.println("JavaQueryBuilder is wired correctly."); +``` diff --git a/docs/query-builder.md b/docs/query-builder.md new file mode 100644 index 0000000..d362577 --- /dev/null +++ b/docs/query-builder.md @@ -0,0 +1,260 @@ +--- +title: Query Builder +nav_order: 3 +description: "Building SELECT queries with the fluent QueryBuilder API" +--- + +# Query Builder +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## 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). + +```java +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +SqlResult result = new QueryBuilder() + .from("users") + .select("id", "name", "email") + .whereEquals("status", "active") + .orderBy("name", true) + .limit(50) + .buildSql(SqlDialect.MYSQL); + +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). + +--- + +## Setting the table + +```java +new QueryBuilder().from("orders") +``` + +--- + +## Selecting columns + +```java +// SELECT * (default — no columns specified) +new QueryBuilder().from("users") + +// SELECT id, name +new QueryBuilder().from("users").select("id", "name") + +// DISTINCT +new QueryBuilder().from("users").distinct().select("country") +``` + +--- + +## Filtering with `where*` + +All `where*` methods are joined with `AND` by default. Use the `orWhere*` +variants to join with `OR`. + +```java +// WHERE status = 'active' +.whereEquals("status", "active") + +// WHERE status != 'banned' +.whereNotEquals("status", "banned") + +// WHERE age > 18 +.whereGreaterThan("age", 18) + +// WHERE age >= 18 +.whereGreaterThanOrEquals("age", 18) + +// WHERE price < 100 +.whereLessThan("price", 100) + +// WHERE price <= 100 +.whereLessThanOrEquals("price", 100) + +// WHERE name LIKE '%Alice%' +.whereLike("name", "Alice") + +// WHERE name NOT LIKE '%bot%' +.whereNotLike("name", "bot") + +// WHERE deleted_at IS NULL +.whereNull("deleted_at") + +// WHERE verified_at IS NOT NULL +.whereNotNull("verified_at") + +// WHERE country IS NOT NULL (alias for whereNotNull) +.whereExists("country") + +// WHERE status IN ('active', 'pending') +.whereIn("status", List.of("active", "pending")) + +// WHERE status NOT IN ('banned', 'deleted') +.whereNotIn("status", List.of("banned", "deleted")) + +// WHERE price BETWEEN 10 AND 99 +.whereBetween("price", 10, 99) +``` + +### OR conditions + +Every `where*` method has an `orWhere*` counterpart: + +```java +new QueryBuilder() + .from("users") + .whereEquals("role", "admin") + .orWhereEquals("role", "moderator") +// → WHERE role = ? OR role = ? +``` + +--- + +## Ordering + +```java +// ORDER BY name ASC +.orderBy("name", true) + +// ORDER BY created_at DESC +.orderBy("created_at", false) + +// Multiple columns: ORDER BY level DESC, name ASC +.orderBy("level", false) +.orderBy("name", true) +``` + +--- + +## Grouping + +```java +// GROUP BY country +.groupBy("country") + +// GROUP BY country, city +.groupBy("country", "city") +``` + +### HAVING + +Pass a raw SQL fragment — no value interpolation; use static expressions only: + +```java +.groupBy("category") +.havingRaw("COUNT(*) > 5") +``` + +{: .warning } +> `havingRaw` accepts a raw SQL string. Never pass user-supplied input here. +> Use only static, known-safe expressions. + +--- + +## LIMIT and OFFSET + +```java +// First 20 rows +.limit(20) + +// Rows 41–60 (page 3 of 20) +.limit(20).offset(40) +``` + +--- + +## Building the result + +### `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: + +```java +Query q = new QueryBuilder() + .from("products") + .whereGreaterThan("stock", 0) + .build(); +``` + +### `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 +SqlResult r1 = builder.buildSql(); + +// Explicit table, standard dialect +SqlResult r2 = builder.buildSql("orders"); + +// Explicit table and dialect +SqlResult r3 = builder.buildSql("orders", SqlDialect.MYSQL); +``` + +See [SQL Dialects](sql-dialects) for the dialect options and the rendered +identifier differences. + +--- + +## Subquery methods + +`QueryBuilder` also exposes methods for embedding subqueries: + +| Method | What it adds | +|--------|-------------| +| `whereInSubquery(col, subquery)` | `WHERE col IN (SELECT ...)` | +| `whereEqualsSubquery(col, subquery)` | `WHERE col = (SELECT ...)` | +| `whereExistsSubquery(subquery)` | `WHERE EXISTS (SELECT ...)` | +| `whereNotExistsSubquery(subquery)` | `WHERE NOT EXISTS (SELECT ...)` | +| `fromSubquery(subquery, alias)` | `FROM (SELECT ...) AS alias` | +| `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. + +--- + +## Security + +Every value passed to a `where*` method is placed in the `?` bind-parameter +list of the rendered `SqlResult` — it is never concatenated into the SQL string. + +```java +// Safe even if userInput contains SQL metacharacters +String userInput = "'; DROP TABLE users; --"; + +SqlResult r = new QueryBuilder() + .from("users") + .whereEquals("name", userInput) + .buildSql(); + +// r.getSql() → "SELECT * FROM users WHERE name = ?" +// r.getParameters() → ["'; DROP TABLE users; --"] +``` + +{: .warning } +> Column names and table names are **not** parameterized. Always use static, +> known-safe strings for those arguments — never forward user input as a +> column or table name. diff --git a/docs/sql-dialects.md b/docs/sql-dialects.md new file mode 100644 index 0000000..4986874 --- /dev/null +++ b/docs/sql-dialects.md @@ -0,0 +1,162 @@ +--- +title: SQL Dialects +nav_order: 7 +description: "SqlDialect, SqlResult, AbstractSqlDialect, and the dialect matrix" +--- + +# SQL Dialects +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`SqlDialect` is a strategy interface that converts a `Query` object into a +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 | + +--- + +## Using a dialect + +Pass a dialect to `buildSql()` on the builder: + +```java +// Standard ANSI +SqlResult r1 = new QueryBuilder() + .from("users") + .whereEquals("id", 1) + .buildSql(); +// → SELECT * FROM users WHERE id = ? + +// MySQL — identifiers back-tick quoted +SqlResult r2 = new QueryBuilder() + .from("users") + .whereEquals("id", 1) + .buildSql(SqlDialect.MYSQL); +// → SELECT * FROM `users` WHERE `id` = ? + +// SQLite — identifiers double-quoted +SqlResult r3 = new QueryBuilder() + .from("users") + .whereEquals("id", 1) + .buildSql(SqlDialect.SQLITE); +// → SELECT * FROM "users" WHERE "id" = ? +``` + +--- + +## SqlResult + +`SqlResult` is returned by every `build()` / `buildSql()` call. It carries the +rendered SQL string and the ordered bind-parameter list. + +| Method | Returns | Description | +|--------|---------|-------------| +| `getSql()` | `String` | The rendered SQL with `?` placeholders | +| `getParameters()` | `List` | Bind parameters in the order they appear in the SQL | + +```java +SqlResult result = new QueryBuilder() + .from("products") + .whereEquals("active", true) + .whereGreaterThan("stock", 0) + .buildSql(SqlDialect.MYSQL); + +String sql = result.getSql(); +// → SELECT * FROM `products` WHERE `active` = ? AND `stock` > ? + +List params = result.getParameters(); +// → [true, 0] +``` + +--- + +## 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. + +```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 +``` + +--- + +## 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 | `?` | `?` | `?` | + +--- + +## AbstractSqlDialect + +`AbstractSqlDialect` implements the shared rendering logic for SELECT and DELETE +queries. It is the base class for both `MySqlDialect` and `SqliteDialect`. + +**Subquery parameter ordering** — parameters are collected depth-first in this +order: + +1. SELECT-list scalar subquery parameters (left to right) +2. FROM subquery parameters +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 + '"'; + } +} +``` + +--- + +## SqlDialect interface + +| Member | Description | +|--------|-------------| +| `SqlDialect.STANDARD` | ANSI SQL constant instance | +| `SqlDialect.MYSQL` | MySQL dialect constant instance | +| `SqlDialect.SQLITE` | SQLite dialect constant instance | +| `render(Query)` | Render a `SELECT` query to `SqlResult` | +| `renderDelete(Query)` | Render a `DELETE` query to `SqlResult`; observes `LIMIT` on supporting dialects | diff --git a/docs/subqueries.md b/docs/subqueries.md new file mode 100644 index 0000000..ea70ddd --- /dev/null +++ b/docs/subqueries.md @@ -0,0 +1,218 @@ +--- +title: Subqueries +nav_order: 6 +description: "All six subquery variants — IN, EXISTS, NOT EXISTS, scalar, FROM-derived table, JOIN, and scalar SELECT" +--- + +# Subqueries +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +JavaQueryBuilder supports six distinct ways to embed a subquery into a SELECT +statement. Every subquery is represented as a `Query` object built by a nested +`QueryBuilder` call. + +| Method | SQL produced | +|--------|-------------| +| `whereInSubquery(col, sub)` | `WHERE col IN (SELECT ...)` | +| `whereEqualsSubquery(col, sub)` | `WHERE col = (SELECT ...)` | +| `whereExistsSubquery(sub)` | `WHERE EXISTS (SELECT ...)` | +| `whereNotExistsSubquery(sub)` | `WHERE NOT EXISTS (SELECT ...)` | +| `fromSubquery(sub, alias)` | `FROM (SELECT ...) AS alias` | +| `joinSubquery(sub, alias, on)` | `INNER JOIN (SELECT ...) AS alias ON ...` | +| `selectSubquery(sub, alias)` | `(SELECT ...) AS alias` in SELECT clause | + +--- + +## WHERE col IN (SELECT ...) + +Use `whereInSubquery` to filter rows where a column value appears in the result +set of another query. + +```java +Query activeUserIds = new QueryBuilder() + .from("users") + .select("id") + .whereEquals("active", true) + .build(); + +SqlResult result = new QueryBuilder() + .from("orders") + .whereInSubquery("user_id", activeUserIds) + .buildSql(); +// → SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE active = ?) +// Parameters: [true] +``` + +--- + +## WHERE col = (SELECT ...) + +Use `whereEqualsSubquery` when the subquery returns a single scalar value to +compare against. + +```java +Query maxPrice = new QueryBuilder() + .from("products") + .select("MAX(price)") + .build(); + +SqlResult result = new QueryBuilder() + .from("products") + .whereEqualsSubquery("price", maxPrice) + .buildSql(); +// → SELECT * FROM products WHERE price = (SELECT MAX(price) FROM products) +``` + +--- + +## WHERE EXISTS (SELECT ...) + +Use `whereExistsSubquery` to test whether the subquery returns at least one row. + +```java +Query sub = new QueryBuilder() + .from("orders") + .select("1") + .whereEquals("status", "pending") + .build(); + +SqlResult result = new QueryBuilder() + .from("users") + .whereExistsSubquery(sub) + .buildSql(); +// → SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE status = ?) +// Parameters: ["pending"] +``` + +--- + +## WHERE NOT EXISTS (SELECT ...) + +Use `whereNotExistsSubquery` to select rows only when the subquery returns no +results. + +```java +Query sub = new QueryBuilder() + .from("orders") + .select("1") + .whereEquals("user_id", 7) + .build(); + +SqlResult result = new QueryBuilder() + .from("users") + .whereNotExistsSubquery(sub) + .buildSql(); +// → SELECT * FROM users WHERE NOT EXISTS (SELECT 1 FROM orders WHERE user_id = ?) +// Parameters: [7] +``` + +--- + +## FROM derived table + +Use `fromSubquery` to replace the table source with a subquery. + +```java +Query inner = new QueryBuilder() + .from("events") + .select("user_id", "COUNT(*) AS event_count") + .groupBy("user_id") + .build(); + +SqlResult result = new QueryBuilder() + .fromSubquery(inner, "event_stats") + .select("user_id", "event_count") + .whereGreaterThan("event_count", 5) + .buildSql(); +// → SELECT user_id, event_count +// FROM (SELECT user_id, COUNT(*) AS event_count FROM events GROUP BY user_id) event_stats +// WHERE event_count > ? +// Parameters: [5] +``` + +--- + +## INNER JOIN subquery + +Use `joinSubquery` to join a derived table against the main query: + +```java +Query teamCounts = new QueryBuilder() + .from("memberships") + .select("team_id", "COUNT(*) AS member_count") + .groupBy("team_id") + .build(); + +SqlResult result = new QueryBuilder() + .from("teams") + .select("teams.name", "tc.member_count") + .joinSubquery(teamCounts, "tc", "tc.team_id = teams.id") + .buildSql(); +// → SELECT teams.name, tc.member_count +// FROM teams +// INNER JOIN (SELECT team_id, COUNT(*) AS member_count FROM memberships GROUP BY team_id) tc +// ON tc.team_id = teams.id +``` + +{: .note } +> `joinSubquery` always produces `INNER JOIN`. For other join types, construct +> a `JoinClause` manually using `JoinClause.Type.LEFT`, `RIGHT`, or `CROSS`. + +--- + +## Scalar SELECT item + +Use `selectSubquery` to embed a scalar subquery as a computed column in the +SELECT list. + +```java +Query orderCount = new QueryBuilder() + .from("orders") + .select("COUNT(*)") + .whereEqualsSubquery("user_id", + new QueryBuilder().from("users").select("id").whereEquals("id", 1).build()) + .build(); + +SqlResult result = new QueryBuilder() + .from("users") + .select("id", "name") + .selectSubquery(orderCount, "order_count") + .buildSql(); +// → SELECT id, name, (SELECT COUNT(*) FROM orders WHERE user_id = (SELECT id FROM users WHERE id = ?)) AS order_count +// FROM users +``` + +--- + +## Parameter ordering + +When subqueries are nested, parameters are collected depth-first in the order +the subqueries appear in the rendered SQL. The outermost query's own parameters +follow after all subquery parameters at the same level. + +```java +Query sub = new QueryBuilder() + .from("teams") + .select("id") + .whereEquals("name", "Engineering") + .build(); + +SqlResult result = new QueryBuilder() + .from("users") + .whereInSubquery("team_id", sub) + .whereEquals("active", true) + .buildSql(); + +// Parameters: ["Engineering", true] +// ^^^^^^^^^^^^^^ from sub ^^^^^ from outer +``` From fb77bae6922e55ee6034ca7f5f0d2a3505e0a6a3 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 18:26:11 +0200 Subject: [PATCH 02/11] ci: fix workflows --- .github/workflows/code-quality.yml | 5 ----- .github/workflows/unit-tests.yml | 12 ------------ 2 files changed, 17 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 719bee8..5f249f9 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -47,11 +47,6 @@ jobs: java-version: '25' - name: Build with coverage run: mvn --batch-mode clean verify - - name: Upload JaCoCo coverage report - uses: actions/upload-artifact@v7 - with: - name: jacoco-report - path: target/site/jacoco/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 929a21f..c01fb91 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,8 +1,6 @@ name: Unit Tests permissions: - checks: write - pull-requests: write contents: read on: @@ -32,11 +30,6 @@ jobs: with: name: java-query-builder-jar path: target/*.jar - - name: Publish JUnit test results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: (!cancelled()) - with: - files: target/surefire-reports/*.xml feature-tests: runs-on: ubuntu-latest @@ -53,8 +46,3 @@ jobs: java-version: ${{ matrix.java-version }} - name: Run feature tests run: mvn --batch-mode -Dtest='feature.**.*Test' test - - name: Publish Feature Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: (!cancelled()) - with: - files: target/surefire-reports/*.xml From da694d9aaf966ed6e83c6dedbfeb01fc6ead732a Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 18:34:53 +0200 Subject: [PATCH 03/11] fix: bump version to 1.0.5 --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 542caae..36af384 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add the dependency to your `pom.xml`: com.github.EzFramework JavaQueryBuilder - 1.0.4 + 1.0.5 ``` diff --git a/pom.xml b/pom.xml index 186c300..77b83f4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.github.EzFramework java-query-builder - 1.0.4 + 1.0.5 jar JavaQueryBuilder From ce26d8cd190f5b98c1ad72eae61bba1192f30ba2 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:00:33 +0200 Subject: [PATCH 04/11] feat: introduce QueryBuilderDefaults configuration class Immutable config holder with JVM-wide global (volatile static) and per-instance override support (builder.withDefaults(...)). Configures: - SQL dialect (default: STANDARD) - Default SELECT column expression (default: "*") - Default LIMIT / OFFSET (default: -1, i.e. none) - LIKE prefix / suffix (default: "%" / "%") Global is captured at builder construction time so behaviour is predictable across concurrent updates. --- .../query/QueryBuilderDefaults.java | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 src/main/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaults.java diff --git a/src/main/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaults.java b/src/main/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaults.java new file mode 100644 index 0000000..3c95289 --- /dev/null +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaults.java @@ -0,0 +1,365 @@ +package com.github.ezframework.javaquerybuilder.query; + +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; + +/** + * Immutable configuration object that holds the default values applied by + * every builder ({@code QueryBuilder}, {@code DeleteBuilder}, + * {@code SelectBuilder}) when no explicit value is provided. + * + *

Usage — configure once at application startup: + *

{@code
+ * QueryBuilderDefaults.setGlobal(
+ *     QueryBuilderDefaults.builder()
+ *         .dialect(SqlDialect.MYSQL)
+ *         .defaultColumns("id, name")
+ *         .defaultLimit(100)
+ *         .likePrefix("%")
+ *         .likeSuffix("%")
+ *         .build()
+ * );
+ * }
+ * + *

Override per-builder-instance: + *

{@code
+ * SqlResult result = new QueryBuilder()
+ *     .withDefaults(QueryBuilderDefaults.builder(QueryBuilderDefaults.global())
+ *         .dialect(SqlDialect.SQLITE)
+ *         .build())
+ *     .from("users")
+ *     .buildSql();
+ * }
+ * + * @author EzFramework + * @version 1.0.0 + */ +public final class QueryBuilderDefaults { + + /** Default SELECT-all token used when no columns are specified. */ + static final String WILDCARD = "*"; + + /** Sentinel value meaning "no limit / no offset". */ + static final int NO_LIMIT = -1; + + /** JVM-wide defaults instance; captured by builders at construction time. */ + private static volatile QueryBuilderDefaults globalInstance = new QueryBuilderDefaults(); + + /** The SQL dialect used for rendering. */ + private final SqlDialect dialect; + + /** Column list used when the builder has no explicit {@code select()} call. */ + private final String defaultColumns; + + /** + * Default LIMIT applied when the builder has no explicit {@code limit()} call. + * {@code -1} means no default limit is applied. + */ + private final int defaultLimit; + + /** + * Default OFFSET applied when the builder has no explicit {@code offset()} call. + * {@code -1} means no default offset is applied. + */ + private final int defaultOffset; + + /** Prefix prepended to the value for {@code LIKE} and {@code NOT LIKE} conditions. */ + private final String likePrefix; + + /** Suffix appended to the value for {@code LIKE} and {@code NOT LIKE} conditions. */ + private final String likeSuffix; + + /** + * Private constructor used by {@link Builder#build()}. + * All fields default to the canonical out-of-the-box values when created + * via the no-arg path. + */ + private QueryBuilderDefaults() { + this.dialect = SqlDialect.STANDARD; + this.defaultColumns = WILDCARD; + this.defaultLimit = NO_LIMIT; + this.defaultOffset = NO_LIMIT; + this.likePrefix = "%"; + this.likeSuffix = "%"; + } + + /** + * Private constructor used by {@link Builder#build()} to set all fields. + * + * @param builder the builder whose values are copied into this instance + */ + private QueryBuilderDefaults(final Builder builder) { + this.dialect = builder.dialect; + this.defaultColumns = builder.defaultColumns; + this.defaultLimit = builder.defaultLimit; + this.defaultOffset = builder.defaultOffset; + this.likePrefix = builder.likePrefix; + this.likeSuffix = builder.likeSuffix; + } + + // ----------------------------------------------------------------------- + // Global accessor + // ----------------------------------------------------------------------- + + /** + * Returns the current JVM-wide defaults instance. + * Every builder captures this value at construction time. + * + * @return the global {@link QueryBuilderDefaults} instance; never {@code null} + */ + public static QueryBuilderDefaults global() { + return globalInstance; + } + + /** + * Replaces the JVM-wide defaults instance. + * The change is visible to all builders created after this call. + * Builders already constructed are unaffected. + * + * @param defaults the new global defaults; must not be {@code null} + * @throws NullPointerException if {@code defaults} is {@code null} + */ + public static void setGlobal(final QueryBuilderDefaults defaults) { + if (defaults == null) { + throw new NullPointerException("Global QueryBuilderDefaults must not be null"); + } + globalInstance = defaults; + } + + // ----------------------------------------------------------------------- + // Factory methods + // ----------------------------------------------------------------------- + + /** + * Creates a new {@link Builder} pre-filled with the canonical out-of-the-box + * defaults (STANDARD dialect, {@code "*"} columns, no limit, no offset, + * {@code "%"} LIKE wrapping). + * + * @return a new builder initialised with default values + */ + public static Builder builder() { + return new Builder(new QueryBuilderDefaults()); + } + + /** + * Creates a new {@link Builder} pre-filled with all values copied from + * {@code source}. Useful for partial-override patterns. + * + * @param source the instance whose values are copied; must not be {@code null} + * @return a new builder initialised from {@code source} + * @throws NullPointerException if {@code source} is {@code null} + */ + public static Builder builder(final QueryBuilderDefaults source) { + if (source == null) { + throw new NullPointerException("Source QueryBuilderDefaults must not be null"); + } + return new Builder(source); + } + + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + /** + * Returns the SQL dialect used by builders that do not receive an explicit + * dialect at build time. + * + * @return the configured dialect; never {@code null} + */ + public SqlDialect getDialect() { + return dialect; + } + + /** + * Returns the column expression used in {@code SELECT} when no columns are + * specified on the builder. + * + * @return the default column expression; never {@code null} + */ + public String getDefaultColumns() { + return defaultColumns; + } + + /** + * Returns the default {@code LIMIT} value. + * {@code -1} means no limit is applied as a default. + * + * @return the default limit; {@code -1} if none + */ + public int getDefaultLimit() { + return defaultLimit; + } + + /** + * Returns the default {@code OFFSET} value. + * {@code -1} means no offset is applied as a default. + * + * @return the default offset; {@code -1} if none + */ + public int getDefaultOffset() { + return defaultOffset; + } + + /** + * Returns the prefix prepended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @return the LIKE prefix; never {@code null} + */ + public String getLikePrefix() { + return likePrefix; + } + + /** + * Returns the suffix appended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @return the LIKE suffix; never {@code null} + */ + public String getLikeSuffix() { + return likeSuffix; + } + + // ----------------------------------------------------------------------- + // Inner Builder + // ----------------------------------------------------------------------- + + /** + * Mutable builder for constructing {@link QueryBuilderDefaults} instances. + * Obtain via {@link QueryBuilderDefaults#builder()} or + * {@link QueryBuilderDefaults#builder(QueryBuilderDefaults)}. + * + * @author EzFramework + * @version 1.0.0 + */ + public static final class Builder { + + /** The SQL dialect; defaults to {@code SqlDialect.STANDARD}. */ + private SqlDialect dialect; + + /** The column expression for SELECT *; defaults to {@code "*"}. */ + private String defaultColumns; + + /** The default LIMIT; defaults to {@code -1}. */ + private int defaultLimit; + + /** The default OFFSET; defaults to {@code -1}. */ + private int defaultOffset; + + /** The LIKE prefix; defaults to {@code "%"}. */ + private String likePrefix; + + /** The LIKE suffix; defaults to {@code "%"}. */ + private String likeSuffix; + + /** + * Constructs a {@code Builder} by copying all values from {@code source}. + * + * @param source the template instance; must not be {@code null} + */ + private Builder(final QueryBuilderDefaults source) { + this.dialect = source.dialect; + this.defaultColumns = source.defaultColumns; + this.defaultLimit = source.defaultLimit; + this.defaultOffset = source.defaultOffset; + this.likePrefix = source.likePrefix; + this.likeSuffix = source.likeSuffix; + } + + /** + * Sets the SQL dialect. + * + * @param sqlDialect the dialect to use; must not be {@code null} + * @return this builder for chaining + * @throws NullPointerException if {@code sqlDialect} is {@code null} + */ + public Builder dialect(final SqlDialect sqlDialect) { + if (sqlDialect == null) { + throw new NullPointerException("Dialect must not be null"); + } + this.dialect = sqlDialect; + return this; + } + + /** + * Sets the default column expression used in {@code SELECT} when no + * explicit columns are provided. + * + * @param columns the column expression, e.g. {@code "*"} or {@code "id, name"} + * @return this builder for chaining + * @throws NullPointerException if {@code columns} is {@code null} + */ + public Builder defaultColumns(final String columns) { + if (columns == null) { + throw new NullPointerException("Default columns must not be null"); + } + this.defaultColumns = columns; + return this; + } + + /** + * Sets the default {@code LIMIT} applied when no explicit limit is set on + * a builder. Use {@code -1} to disable the default limit. + * + * @param limit the default limit value; {@code -1} for none + * @return this builder for chaining + */ + public Builder defaultLimit(final int limit) { + this.defaultLimit = limit; + return this; + } + + /** + * Sets the default {@code OFFSET} applied when no explicit offset is set + * on a builder. Use {@code -1} to disable the default offset. + * + * @param offset the default offset value; {@code -1} for none + * @return this builder for chaining + */ + public Builder defaultOffset(final int offset) { + this.defaultOffset = offset; + return this; + } + + /** + * Sets the prefix prepended to values in {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @param prefix the LIKE prefix; must not be {@code null} + * @return this builder for chaining + * @throws NullPointerException if {@code prefix} is {@code null} + */ + public Builder likePrefix(final String prefix) { + if (prefix == null) { + throw new NullPointerException("LIKE prefix must not be null"); + } + this.likePrefix = prefix; + return this; + } + + /** + * Sets the suffix appended to values in {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @param suffix the LIKE suffix; must not be {@code null} + * @return this builder for chaining + * @throws NullPointerException if {@code suffix} is {@code null} + */ + public Builder likeSuffix(final String suffix) { + if (suffix == null) { + throw new NullPointerException("LIKE suffix must not be null"); + } + this.likeSuffix = suffix; + return this; + } + + /** + * Builds a new {@link QueryBuilderDefaults} from the current state of + * this builder. + * + * @return a new immutable {@link QueryBuilderDefaults} instance + */ + public QueryBuilderDefaults build() { + return new QueryBuilderDefaults(this); + } + } +} From f6787724cfb75ecd897549ace4eef809eb34856c Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:00:39 +0200 Subject: [PATCH 05/11] feat: add rendering-hint fields to Query Three new fields propagated from QueryBuilderDefaults into the Query data object at build() time so dialect-agnostic renderers can read them: - defaultSelectColumns (fallback when selectColumns is empty) - likePrefix (prepended to LIKE / NOT LIKE values) - likeSuffix (appended to LIKE / NOT LIKE values) All fields default to the existing hardcoded behaviour ("*", "%", "%") so there is no change to query output without an explicit configuration. --- .../javaquerybuilder/query/Query.java | 72 +++++++++++++++++++ 1 file changed, 72 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 4e8dafd..53a5eb8 100644 --- a/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java +++ b/src/main/java/com/github/ezframework/javaquerybuilder/query/Query.java @@ -55,6 +55,18 @@ public class Query { /** The list of scalar subquery items appended to the SELECT clause. */ private List selectSubqueries = new ArrayList<>(); + /** + * The column expression rendered when {@code selectColumns} is empty. + * Defaults to {@code "*"}; overridable via {@link QueryBuilderDefaults}. + */ + private String defaultSelectColumns = "*"; + + /** Prefix prepended to values for LIKE and NOT LIKE conditions. */ + private String likePrefix = "%"; + + /** Suffix appended to values for LIKE and NOT LIKE conditions. */ + private String likeSuffix = "%"; + /** * Gets the source table for the query. * @@ -306,4 +318,64 @@ public List getSelectSubqueries() { public void setSelectSubqueries(List selectSubqueries) { this.selectSubqueries = selectSubqueries; } + + /** + * Returns the column expression used in {@code SELECT} when no explicit + * columns are configured on the builder. + * + * @return the default column expression; never {@code null} + */ + public String getDefaultSelectColumns() { + return defaultSelectColumns; + } + + /** + * Sets the column expression used in {@code SELECT} when no explicit + * columns are configured. + * + * @param defaultSelectColumns the column expression; must not be {@code null} + */ + public void setDefaultSelectColumns(final String defaultSelectColumns) { + this.defaultSelectColumns = defaultSelectColumns; + } + + /** + * Returns the prefix prepended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @return the LIKE prefix; never {@code null} + */ + public String getLikePrefix() { + return likePrefix; + } + + /** + * Sets the prefix prepended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @param likePrefix the LIKE prefix; must not be {@code null} + */ + public void setLikePrefix(final String likePrefix) { + this.likePrefix = likePrefix; + } + + /** + * Returns the suffix appended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @return the LIKE suffix; never {@code null} + */ + public String getLikeSuffix() { + return likeSuffix; + } + + /** + * Sets the suffix appended to values for {@code LIKE} and + * {@code NOT LIKE} conditions. + * + * @param likeSuffix the LIKE suffix; must not be {@code null} + */ + public void setLikeSuffix(final String likeSuffix) { + this.likeSuffix = likeSuffix; + } } From 59a076e5f88e9d3b4b7fc1f99929a04d36ca5f32 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:00:47 +0200 Subject: [PATCH 06/11] feat: drive SELECT * and LIKE wrapping from Query rendering hints AbstractSqlDialect now reads from the Query object instead of using hardcoded literals: - appendSelectColumns: uses query.getDefaultSelectColumns() instead of "*" - LIKE / NOT LIKE params: uses query.getLikePrefix()/getLikeSuffix() instead of hardcoded "%" + val + "%" appendConditionFragment gains a Query parameter to pass the hints through to appendNonComparisonFragment. --- .../query/sql/AbstractSqlDialect.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 6de5d45..07e7387 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 @@ -141,7 +141,7 @@ private void appendSelectColumns(StringBuilder sql, List params, Query q final List cols = query.getSelectColumns(); final List subItems = query.getSelectSubqueries(); if (cols.isEmpty() && subItems.isEmpty()) { - sql.append("*"); + sql.append(query.getDefaultSelectColumns()); return; } final List fragments = new ArrayList<>(); @@ -207,7 +207,7 @@ protected void appendWhereClause(StringBuilder sql, List params, Query q if (entry.getColumn() != null) { sql.append(quoteIdentifier(entry.getColumn())).append(" "); } - appendConditionFragment(sql, params, entry); + appendConditionFragment(sql, params, entry, query); } } @@ -219,9 +219,11 @@ protected void appendWhereClause(StringBuilder sql, List params, Query q * @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) */ @SuppressWarnings("unchecked") - protected void appendConditionFragment(StringBuilder sql, List params, ConditionEntry entry) { + protected void appendConditionFragment( + StringBuilder sql, List params, ConditionEntry entry, Query query) { final Operator op = entry.getCondition().getOperator(); final Object val = entry.getCondition().getValue(); @@ -234,7 +236,7 @@ protected void appendConditionFragment(StringBuilder sql, List params, C params.add(val); return; } - appendNonComparisonFragment(sql, params, op, val); + appendNonComparisonFragment(sql, params, op, val, query); } private void appendSubqueryCondition( @@ -284,15 +286,15 @@ private void appendSubqueryComparison( @SuppressWarnings("unchecked") private void appendNonComparisonFragment( - StringBuilder sql, List params, Operator op, Object val) { + StringBuilder sql, List params, Operator op, Object val, Query query) { switch (op) { case LIKE: sql.append("LIKE ?"); - params.add("%" + val + "%"); + params.add(query.getLikePrefix() + val + query.getLikeSuffix()); break; case NOT_LIKE: sql.append("NOT LIKE ?"); - params.add("%" + val + "%"); + params.add(query.getLikePrefix() + val + query.getLikeSuffix()); break; case EXISTS: sql.append("IS NOT NULL"); From c4ce70f8acad1575842af065e5515782f45d963e Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:00:57 +0200 Subject: [PATCH 07/11] feat: integrate QueryBuilderDefaults into all SELECT/DELETE builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QueryBuilder, DeleteBuilder, and SelectBuilder each gain: - A queryBuilderDefaults field initialised from QueryBuilderDefaults.global() at construction time (snapshot semantics — not live). - A withDefaults(QueryBuilderDefaults) fluent method for per-instance override without touching the global. Behavioural changes when a non-default QueryBuilderDefaults is active: - Dialect: buildSql() / build() null-fallback now routes to the configured dialect instead of hardcoded SqlDialect.STANDARD. - Default columns: rendered when no explicit select() columns are set. - Default limit / offset: applied when builder sentinel (-1) is present and the defaults carry a non-negative value (explicit wins). - LIKE wrapping: SelectBuilder.whereLike() now honours likePrefix / likeSuffix (previously had no wrapping at all — aligned with AbstractSqlDialect behaviour). --- .../query/builder/DeleteBuilder.java | 26 +++++++++++-- .../query/builder/QueryBuilder.java | 35 +++++++++++++++-- .../query/builder/SelectBuilder.java | 38 ++++++++++++++++--- 3 files changed, 87 insertions(+), 12 deletions(-) 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 362db8c..154e4dd 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 @@ -4,6 +4,7 @@ import java.util.List; import com.github.ezframework.javaquerybuilder.query.Query; +import com.github.ezframework.javaquerybuilder.query.QueryBuilderDefaults; import com.github.ezframework.javaquerybuilder.query.condition.Condition; import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; import com.github.ezframework.javaquerybuilder.query.condition.Connector; @@ -25,6 +26,24 @@ public class DeleteBuilder { /** The WHERE conditions. */ private final List conditions = new ArrayList<>(); + /** The defaults configuration for this builder instance. */ + private QueryBuilderDefaults queryBuilderDefaults = QueryBuilderDefaults.global(); + + /** + * Overrides the defaults configuration for this builder instance. + * + * @param defaults the defaults to apply; must not be {@code null} + * @return this builder instance for chaining + * @throws NullPointerException if {@code defaults} is {@code null} + */ + public DeleteBuilder withDefaults(final QueryBuilderDefaults defaults) { + if (defaults == null) { + throw new NullPointerException("QueryBuilderDefaults must not be null"); + } + this.queryBuilderDefaults = defaults; + return this; + } + /** * Sets the table to delete from. * @param table the table name @@ -182,14 +201,15 @@ public SqlResult build() { /** * Builds the SQL DELETE statement using the given dialect. - * When {@code dialect} is {@code null}, {@link SqlDialect#STANDARD} is used. + * When {@code dialect} is {@code null}, the dialect from the configured + * {@link QueryBuilderDefaults} is used. * - * @param dialect the SQL dialect (may be null for standard SQL) + * @param dialect the SQL dialect (may be {@code null} to use the configured default) * @return the SQL result */ public SqlResult build(final SqlDialect dialect) { final Query q = toQuery(); - return (dialect != null ? dialect : SqlDialect.STANDARD).renderDelete(q); + return (dialect != null ? dialect : queryBuilderDefaults.getDialect()).renderDelete(q); } private Query toQuery() { 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 6352c51..aecb263 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 @@ -6,6 +6,7 @@ import com.github.ezframework.javaquerybuilder.query.JoinClause; import com.github.ezframework.javaquerybuilder.query.Query; +import com.github.ezframework.javaquerybuilder.query.QueryBuilderDefaults; import com.github.ezframework.javaquerybuilder.query.ScalarSelectItem; import com.github.ezframework.javaquerybuilder.query.condition.Condition; import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; @@ -82,6 +83,27 @@ public class QueryBuilder { /** The scalar subquery items appended to the SELECT clause. */ private final List selectSubqueries = new ArrayList<>(); + /** The defaults configuration for this builder instance. */ + private QueryBuilderDefaults queryBuilderDefaults = QueryBuilderDefaults.global(); + + /** + * Overrides the defaults configuration for this builder instance. + * + *

The change applies only to this instance; the JVM-wide global is not + * modified. Calling this method after construction is safe and fluent. + * + * @param defaults the defaults to apply; must not be {@code null} + * @return this builder instance for chaining + * @throws NullPointerException if {@code defaults} is {@code null} + */ + public QueryBuilder withDefaults(final QueryBuilderDefaults defaults) { + if (defaults == null) { + throw new NullPointerException("QueryBuilderDefaults must not be null"); + } + this.queryBuilderDefaults = defaults; + return this; + } + /** * Returns a new {@link InsertBuilder}. * @@ -576,8 +598,6 @@ public QueryBuilder havingRaw(String clause) { */ public Query build() { final Query q = new Query(); - q.setLimit(limit); - q.setOffset(offset); q.setConditions(new ArrayList<>(conditions)); q.setGroupBy(new ArrayList<>(groupByColumns)); q.setOrderBy(new ArrayList<>(orderByColumns)); @@ -590,6 +610,15 @@ public Query build() { q.setFromAlias(fromAlias); q.setJoins(new ArrayList<>(joins)); q.setSelectSubqueries(new ArrayList<>(selectSubqueries)); + q.setDefaultSelectColumns(queryBuilderDefaults.getDefaultColumns()); + q.setLikePrefix(queryBuilderDefaults.getLikePrefix()); + q.setLikeSuffix(queryBuilderDefaults.getLikeSuffix()); + final int resolvedLimit = (limit == -1 && queryBuilderDefaults.getDefaultLimit() >= 0) + ? queryBuilderDefaults.getDefaultLimit() : limit; + final int resolvedOffset = (offset == -1 && queryBuilderDefaults.getDefaultOffset() >= 0) + ? queryBuilderDefaults.getDefaultOffset() : offset; + q.setLimit(resolvedLimit); + q.setOffset(resolvedOffset); return q; } @@ -626,6 +655,6 @@ public SqlResult buildSql(String table) { public SqlResult buildSql(String table, SqlDialect dialect) { final Query q = build(); q.setTable(table); - return (dialect != null ? dialect : SqlDialect.STANDARD).render(q); + return (dialect != null ? dialect : queryBuilderDefaults.getDialect()).render(q); } } 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 5a9304b..3a34f92 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 @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.List; +import com.github.ezframework.javaquerybuilder.query.QueryBuilderDefaults; import com.github.ezframework.javaquerybuilder.query.condition.Condition; import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; import com.github.ezframework.javaquerybuilder.query.condition.Connector; @@ -63,6 +64,24 @@ public class SelectBuilder { */ private boolean distinct = false; + /** The defaults configuration for this builder instance. */ + private QueryBuilderDefaults queryBuilderDefaults = QueryBuilderDefaults.global(); + + /** + * Overrides the defaults configuration for this builder instance. + * + * @param defaults the defaults to apply; must not be {@code null} + * @return this builder instance for chaining + * @throws NullPointerException if {@code defaults} is {@code null} + */ + public SelectBuilder withDefaults(final QueryBuilderDefaults defaults) { + if (defaults == null) { + throw new NullPointerException("QueryBuilderDefaults must not be null"); + } + this.queryBuilderDefaults = defaults; + return this; + } + /** * Sets the table to select from. * @param table the table name @@ -223,7 +242,7 @@ private void buildSelectClause(final StringBuilder sql) { sql.append("DISTINCT "); } if (columns.isEmpty()) { - sql.append("*"); + sql.append(queryBuilderDefaults.getDefaultColumns()); } else { sql.append(String.join(", ", columns)); } @@ -261,7 +280,10 @@ private void buildWhereClause(final StringBuilder sql, final List params sql.append(")"); } else if (op == Operator.LIKE) { sql.append("LIKE ?"); - params.add(cond.getCondition().getValue()); + final String likeVal = queryBuilderDefaults.getLikePrefix() + + cond.getCondition().getValue() + + queryBuilderDefaults.getLikeSuffix(); + params.add(likeVal); } else { throw new UnsupportedOperationException( "Operator not supported: " + cond.getCondition().getOperator()); @@ -288,11 +310,15 @@ private void buildOrderByClause(final StringBuilder sql) { } private void buildLimitOffsetClause(final StringBuilder sql) { - if (limit >= 0) { - sql.append(" LIMIT ").append(limit); + final int resolvedLimit = (limit == -1 && queryBuilderDefaults.getDefaultLimit() >= 0) + ? queryBuilderDefaults.getDefaultLimit() : limit; + final int resolvedOffset = (offset == -1 && queryBuilderDefaults.getDefaultOffset() >= 0) + ? queryBuilderDefaults.getDefaultOffset() : offset; + if (resolvedLimit >= 0) { + sql.append(" LIMIT ").append(resolvedLimit); } - if (offset >= 0) { - sql.append(" OFFSET ").append(offset); + if (resolvedOffset >= 0) { + sql.append(" OFFSET ").append(resolvedOffset); } } } From 789a55f19e0d101dd257f14d104f96b4cc383baa Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:01:06 +0200 Subject: [PATCH 08/11] test: add QueryBuilderDefaultsTest; align SelectBuilderTest LIKE wrapping QueryBuilderDefaultsTest (23 tests) covers: - Canonical global defaults (STANDARD, "*", -1, -1, "%", "%") - builder() and builder(source) factory methods - setGlobal(null) NPE guard - withDefaults(null) NPE guard on all three builders - Global dialect applied to newly created QueryBuilder - Per-instance withDefaults() overrides global for that instance only - Explicit buildSql(table, dialect) parameter wins over configured default - defaultColumns / explicit select() winner semantics - defaultLimit / defaultOffset applied and overridden correctly - Custom likePrefix / likeSuffix applied (QueryBuilder + SelectBuilder) - DeleteBuilder dialect routing via withDefaults() - SelectBuilder defaultColumns, defaultLimit, LIKE wrapping SelectBuilderTest.testWhereLike: updated to pass the raw value ("bob") instead of pre-wrapped "%bob%" now that SelectBuilder auto-wraps using the configured likePrefix/likeSuffix (consistent with QueryBuilder). --- .../query/QueryBuilderDefaultsTest.java | 331 ++++++++++++++++++ .../query/builder/SelectBuilderTest.java | 2 +- 2 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java diff --git a/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java b/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java new file mode 100644 index 0000000..31b3cc8 --- /dev/null +++ b/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java @@ -0,0 +1,331 @@ +package com.github.ezframework.javaquerybuilder.query; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.ezframework.javaquerybuilder.query.builder.DeleteBuilder; +import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; +import com.github.ezframework.javaquerybuilder.query.builder.SelectBuilder; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; +import com.github.ezframework.javaquerybuilder.query.sql.SqlResult; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QueryBuilderDefaultsTest { + + private QueryBuilderDefaults savedGlobal; + + @BeforeEach + void saveGlobal() { + savedGlobal = QueryBuilderDefaults.global(); + } + + @AfterEach + void restoreGlobal() { + QueryBuilderDefaults.setGlobal(savedGlobal); + } + + // --- Default values --- + + @Test + void globalHasCanonicalDefaultValues() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.global(); + assertSame(SqlDialect.STANDARD, defaults.getDialect()); + assertEquals("*", defaults.getDefaultColumns()); + assertEquals(-1, defaults.getDefaultLimit()); + assertEquals(-1, defaults.getDefaultOffset()); + assertEquals("%", defaults.getLikePrefix()); + assertEquals("%", defaults.getLikeSuffix()); + } + + @Test + void builderProducesInstanceWithCanonicalDefaults() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder().build(); + assertSame(SqlDialect.STANDARD, defaults.getDialect()); + assertEquals("*", defaults.getDefaultColumns()); + assertEquals(-1, defaults.getDefaultLimit()); + assertEquals(-1, defaults.getDefaultOffset()); + assertEquals("%", defaults.getLikePrefix()); + assertEquals("%", defaults.getLikeSuffix()); + } + + @Test + void builderFromSourceCopiesAllFields() { + QueryBuilderDefaults source = QueryBuilderDefaults.builder() + .dialect(SqlDialect.MYSQL) + .defaultColumns("id, name") + .defaultLimit(50) + .defaultOffset(10) + .likePrefix(">>") + .likeSuffix("<<") + .build(); + + QueryBuilderDefaults copy = QueryBuilderDefaults.builder(source).build(); + + assertSame(SqlDialect.MYSQL, copy.getDialect()); + assertEquals("id, name", copy.getDefaultColumns()); + assertEquals(50, copy.getDefaultLimit()); + assertEquals(10, copy.getDefaultOffset()); + assertEquals(">>", copy.getLikePrefix()); + assertEquals("<<", copy.getLikeSuffix()); + assertNotSame(source, copy); + } + + // --- Null guards --- + + @Test + void setGlobalNullThrowsNullPointerException() { + assertThrows(NullPointerException.class, () -> QueryBuilderDefaults.setGlobal(null)); + } + + @Test + void withDefaultsNullThrowsNullPointerExceptionOnQueryBuilder() { + assertThrows(NullPointerException.class, + () -> new QueryBuilder().withDefaults(null)); + } + + @Test + void withDefaultsNullThrowsNullPointerExceptionOnDeleteBuilder() { + assertThrows(NullPointerException.class, + () -> new DeleteBuilder().withDefaults(null)); + } + + @Test + void withDefaultsNullThrowsNullPointerExceptionOnSelectBuilder() { + assertThrows(NullPointerException.class, + () -> new SelectBuilder().withDefaults(null)); + } + + // --- Global dialect applied by new builders --- + + @Test + void globalDialectIsUsedByNewlyCreatedQueryBuilder() { + QueryBuilderDefaults.setGlobal( + QueryBuilderDefaults.builder().dialect(SqlDialect.MYSQL).build()); + + SqlResult result = new QueryBuilder().from("users").buildSql(); + + // MySQL dialect wraps identifiers in backticks + assertTrue(result.getSql().contains("`users`"), + "Expected MySQL backtick quoting, got: " + result.getSql()); + } + + @Test + void setGlobalUpdatesGlobalInstance() { + QueryBuilderDefaults custom = QueryBuilderDefaults.builder() + .dialect(SqlDialect.SQLITE).build(); + QueryBuilderDefaults.setGlobal(custom); + assertSame(custom, QueryBuilderDefaults.global()); + } + + // --- Per-instance withDefaults overrides global --- + + @Test + void withDefaultsDialectOverridesGlobalForThatInstance() { + // Global stays STANDARD; only this instance uses MYSQL + QueryBuilderDefaults mysql = QueryBuilderDefaults.builder() + .dialect(SqlDialect.MYSQL).build(); + + SqlResult mysqlResult = new QueryBuilder().withDefaults(mysql).from("orders").buildSql(); + SqlResult stdResult = new QueryBuilder().from("orders").buildSql(); + + assertTrue(mysqlResult.getSql().contains("`orders`"), + "Expected MySQL quoting: " + mysqlResult.getSql()); + assertTrue(stdResult.getSql().contains("orders") && !stdResult.getSql().contains("`orders`"), + "Expected standard quoting: " + stdResult.getSql()); + } + + @Test + void explicitDialectParameterWinsOverDefaultsDialect() { + // withDefaults sets MYSQL, but explicit buildSql(table, SQLITE) should win + QueryBuilderDefaults mysql = QueryBuilderDefaults.builder() + .dialect(SqlDialect.MYSQL).build(); + + SqlResult result = new QueryBuilder() + .withDefaults(mysql) + .buildSql("users", SqlDialect.SQLITE); + + // SQLite wraps identifiers in double-quotes + assertTrue(result.getSql().contains("\"users\""), + "Expected SQLite double-quote quoting: " + result.getSql()); + } + + // --- Default SELECT columns --- + + @Test + void defaultColumnsAppearsInSelectWhenNoneSpecified() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultColumns("id, name").build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults).from("users").buildSql(); + + assertTrue(result.getSql().startsWith("SELECT id, name FROM"), + "Expected custom default columns: " + result.getSql()); + } + + @Test + void explicitSelectColumnsWinsOverDefaultColumns() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultColumns("id, name").build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults) + .select("age") + .from("users").buildSql(); + + assertTrue(result.getSql().startsWith("SELECT age FROM"), + "Explicit select should win: " + result.getSql()); + } + + // --- Default limit and offset --- + + @Test + void defaultLimitAppliedWhenBuilderLimitNotSet() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultLimit(100).build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults).from("users").buildSql(); + + assertTrue(result.getSql().contains("LIMIT 100"), + "Expected LIMIT 100: " + result.getSql()); + } + + @Test + void explicitLimitWinsOverDefaultLimit() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultLimit(100).build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults) + .limit(25) + .from("users").buildSql(); + + assertTrue(result.getSql().contains("LIMIT 25"), + "Expected LIMIT 25: " + result.getSql()); + assertTrue(!result.getSql().contains("LIMIT 100"), + "Default limit must not appear when explicit limit is set"); + } + + @Test + void defaultOffsetAppliedWhenBuilderOffsetNotSet() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultOffset(20).build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults).from("users").buildSql(); + + assertTrue(result.getSql().contains("OFFSET 20"), + "Expected OFFSET 20: " + result.getSql()); + } + + @Test + void explicitOffsetWinsOverDefaultOffset() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultOffset(20).build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults) + .offset(5) + .from("users").buildSql(); + + assertTrue(result.getSql().contains("OFFSET 5"), + "Expected OFFSET 5: " + result.getSql()); + } + + // --- LIKE prefix / suffix --- + + @Test + void likeParamUsesCustomPrefixAndSuffix() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .likePrefix(">>") + .likeSuffix("<<") + .build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults) + .whereLike("name", "bob") + .from("users").buildSql(); + + List params = result.getParameters(); + assertEquals(1, params.size()); + assertEquals(">>bob<<", params.get(0)); + } + + @Test + void likeParamWithEmptyPrefixAndSuffixIsUnwrapped() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .likePrefix("") + .likeSuffix("") + .build(); + + SqlResult result = new QueryBuilder().withDefaults(defaults) + .whereLike("name", "bob") + .from("users").buildSql(); + + List params = result.getParameters(); + assertEquals(1, params.size()); + assertEquals("bob", params.get(0)); + } + + // --- DeleteBuilder --- + + @Test + void deleteBuilderWithDefaultsDialectIsUsed() { + QueryBuilderDefaults mysql = QueryBuilderDefaults.builder() + .dialect(SqlDialect.MYSQL).build(); + + SqlResult result = new DeleteBuilder() + .withDefaults(mysql) + .from("orders") + .whereEquals("id", 1) + .build(); + + assertTrue(result.getSql().contains("`orders`"), + "Expected MySQL backtick quoting on table: " + result.getSql()); + assertTrue(result.getSql().contains("`id`"), + "Expected MySQL backtick quoting on column: " + result.getSql()); + } + + // --- SelectBuilder --- + + @Test + void selectBuilderDefaultColumnsApplied() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultColumns("id, status").build(); + + SqlResult result = new SelectBuilder().withDefaults(defaults).from("users").build(); + + assertTrue(result.getSql().startsWith("SELECT id, status FROM"), + "Expected custom default columns: " + result.getSql()); + } + + @Test + void selectBuilderDefaultLimitApplied() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultLimit(30).build(); + + SqlResult result = new SelectBuilder().withDefaults(defaults).from("users").build(); + + assertTrue(result.getSql().contains("LIMIT 30"), + "Expected LIMIT 30: " + result.getSql()); + } + + @Test + void selectBuilderLikeWrapsWithDefaults() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .likePrefix("{{") + .likeSuffix("}}") + .build(); + + SqlResult result = new SelectBuilder().withDefaults(defaults) + .from("users") + .whereLike("name", "alice") + .build(); + + List params = result.getParameters(); + assertEquals(1, params.size()); + assertEquals("{{alice}}", params.get(0)); + } +} diff --git a/src/test/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilderTest.java b/src/test/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilderTest.java index d2971e6..d46750b 100644 --- a/src/test/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilderTest.java +++ b/src/test/java/com/github/ezframework/javaquerybuilder/query/builder/SelectBuilderTest.java @@ -67,7 +67,7 @@ void testWhereIn() { void testWhereLike() { SqlResult sql = new SelectBuilder() .from("users") - .whereLike("name", "%bob%") + .whereLike("name", "bob") .build(); assertEquals("SELECT * FROM users WHERE name LIKE ?", sql.getSql()); assertEquals(Collections.singletonList("%bob%"), sql.getParameters()); From 06a7522f1048ba7374869f5957eed76e7bfca910 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:11:57 +0200 Subject: [PATCH 09/11] test: add null-guard and SelectBuilder offset tests for full coverage Cover the five NullPointerException guards in QueryBuilderDefaults that were previously unreachable in tests: - builder(null) on the static factory - Builder.dialect(null) - Builder.defaultColumns(null) - Builder.likePrefix(null) - Builder.likeSuffix(null) Also add selectBuilderDefaultOffsetApplied to hit the true-branch of the defaultOffset ternary in SelectBuilder.buildLimitOffsetClause(). QueryBuilderDefaults now reports 100% line coverage; SelectBuilder reaches 97.7% (only the unreachable UnsupportedOperationException path for unknown operators remains uncovered). --- .../query/QueryBuilderDefaultsTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java b/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java index 31b3cc8..6d285a6 100644 --- a/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java +++ b/src/test/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaultsTest.java @@ -85,6 +85,35 @@ void setGlobalNullThrowsNullPointerException() { assertThrows(NullPointerException.class, () -> QueryBuilderDefaults.setGlobal(null)); } + @Test + void builderFromNullSourceThrowsNullPointerException() { + assertThrows(NullPointerException.class, () -> QueryBuilderDefaults.builder(null)); + } + + @Test + void builderDialectSetToNullThrowsNullPointerException() { + assertThrows(NullPointerException.class, + () -> QueryBuilderDefaults.builder().dialect(null)); + } + + @Test + void builderDefaultColumnsSetToNullThrowsNullPointerException() { + assertThrows(NullPointerException.class, + () -> QueryBuilderDefaults.builder().defaultColumns(null)); + } + + @Test + void builderLikePrefixSetToNullThrowsNullPointerException() { + assertThrows(NullPointerException.class, + () -> QueryBuilderDefaults.builder().likePrefix(null)); + } + + @Test + void builderLikeSuffixSetToNullThrowsNullPointerException() { + assertThrows(NullPointerException.class, + () -> QueryBuilderDefaults.builder().likeSuffix(null)); + } + @Test void withDefaultsNullThrowsNullPointerExceptionOnQueryBuilder() { assertThrows(NullPointerException.class, @@ -312,6 +341,19 @@ void selectBuilderDefaultLimitApplied() { "Expected LIMIT 30: " + result.getSql()); } + @Test + void selectBuilderDefaultOffsetApplied() { + QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() + .defaultLimit(10) + .defaultOffset(5) + .build(); + + SqlResult result = new SelectBuilder().withDefaults(defaults).from("users").build(); + + assertTrue(result.getSql().contains("OFFSET 5"), + "Expected OFFSET 5: " + result.getSql()); + } + @Test void selectBuilderLikeWrapsWithDefaults() { QueryBuilderDefaults defaults = QueryBuilderDefaults.builder() From cb58a740ee9f130f953e408babbe804e18d74e31 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:54:29 +0200 Subject: [PATCH 10/11] docs: improved readability --- README.md | 44 +++++++++ docs/api-reference.md | 58 ++++++++++-- docs/conditions.md | 6 +- docs/configuration.md | 205 ++++++++++++++++++++++++++++++++++++++++++ docs/exceptions.md | 20 ++--- docs/in-memory.md | 6 +- docs/index.md | 23 ++--- docs/installation.md | 2 +- docs/query-builder.md | 14 +-- docs/sql-dialects.md | 12 +-- docs/subqueries.md | 2 +- 11 files changed, 346 insertions(+), 46 deletions(-) create mode 100644 docs/configuration.md diff --git a/README.md b/README.md index 36af384..ffc5f88 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A lightweight, fluent Java library for building parameterized SQL queries and filtering in-memory data, no runtime dependencies required. + ## Features - Fluent, readable builder API for SELECT, INSERT, UPDATE, DELETE, and CREATE TABLE @@ -14,6 +15,7 @@ A lightweight, fluent Java library for building parameterized SQL queries and fi - Subquery support: `WHERE col IN (SELECT ...)`, `WHERE EXISTS (SELECT ...)`, `WHERE NOT EXISTS`, derived-table `FROM (SELECT ...) AS alias`, JOIN subqueries, and scalar `(SELECT ...) AS alias` in SELECT - Column selection, `DISTINCT`, `GROUP BY`, `ORDER BY`, `LIMIT`, and `OFFSET` - SQL dialect support: Standard, MySQL, SQLite +- **Global and per-query configuration of defaults (e.g., dialect, columns, limit, LIKE wrapping) via `QueryBuilderDefaults`** - In-memory filtering via `QueryableStorage` - Zero runtime dependencies, pure Java 21+ @@ -391,6 +393,48 @@ new SelectBuilder() .build(); // → SqlResult ``` + +## Global and Per-Query Configuration + +You can preset the default SQL dialect, default columns, limit, offset, and LIKE wrapping for all queries using `QueryBuilderDefaults`. This is useful for enforcing a project-wide dialect (e.g., always use SQLite) or customizing builder defaults. + +### Set SQLite as the default dialect for all queries + +```java +import com.github.ezframework.javaquerybuilder.query.QueryBuilderDefaults; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; + +// Set at application startup: +QueryBuilderDefaults.setGlobal( + QueryBuilderDefaults.builder() + .dialect(SqlDialect.SQLITE) + .build() +); + +// All new QueryBuilder, SelectBuilder, and DeleteBuilder instances will use SQLite quoting by default: +SqlResult result = new QueryBuilder() + .select("id", "name") + .from("users") + .buildSql(); +// → SELECT id, name FROM "users" +``` + +### Override per query + +You can override the defaults for a single query using `.withDefaults()`: + +```java +SqlResult result = new QueryBuilder() + .withDefaults(QueryBuilderDefaults.builder(QueryBuilderDefaults.global()) + .dialect(SqlDialect.MYSQL) + .build()) + .from("users") + .buildSql(); +// → SELECT * FROM `users` +``` + +See the Javadoc for [`QueryBuilderDefaults`](src/main/java/com/github/ezframework/javaquerybuilder/query/QueryBuilderDefaults.java) for all configurable options. + ## SQL Dialects By default, `buildSql(table)` uses `SqlDialect.STANDARD` (no identifier quoting). Pass a second argument to use a different dialect: diff --git a/docs/api-reference.md b/docs/api-reference.md index 3558521..85bfe6b 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,6 +1,6 @@ --- title: API Reference -nav_order: 10 +nav_order: 11 description: "Complete public method tables for every class and interface in JavaQueryBuilder" --- @@ -70,8 +70,10 @@ Main entry point for SELECT queries and static gateway to DML builders. | `offset(int n)` | `QueryBuilder` | Set `OFFSET` | | `build()` | `Query` | Build a `Query` object (no SQL rendered yet) | | `buildSql()` | `SqlResult` | Render SELECT using table set via `from()`, standard dialect | +| `buildSql(SqlDialect)` | `SqlResult` | Render SELECT using table set via `from()`, specified dialect | | `buildSql(String table)` | `SqlResult` | Render SELECT for explicit `table`, standard dialect | | `buildSql(String table, SqlDialect)` | `SqlResult` | Render SELECT for explicit `table` and dialect | +| `withDefaults(QueryBuilderDefaults)` | `QueryBuilder` | Set per-instance configuration defaults; throws `NullPointerException` if `null` | --- @@ -91,6 +93,8 @@ Lower-level SELECT builder that produces `SqlResult` directly (no `Query` interm | `orderBy(String col, boolean asc)` | `SelectBuilder` | Add `ORDER BY` | | `limit(int n)` | `SelectBuilder` | Set `LIMIT` | | `offset(int n)` | `SelectBuilder` | Set `OFFSET` | +| `withDefaults(QueryBuilderDefaults)` | `SelectBuilder` | Set per-instance configuration defaults; throws `NullPointerException` if `null` | +| `build()` | `SqlResult` | Render SELECT using defaults dialect | | `build(SqlDialect)` | `SqlResult` | Render SELECT with given dialect | --- @@ -134,6 +138,7 @@ Lower-level SELECT builder that produces `SqlResult` directly (no `Query` interm | `whereIn(col, List)` | `DeleteBuilder` | `WHERE col IN (...)` (AND); throws `IllegalArgumentException` if list is null/empty | | `whereNotIn(col, List)` | `DeleteBuilder` | `WHERE col NOT IN (...)` (AND); throws `IllegalArgumentException` if list is null/empty | | `whereBetween(col, from, to)` | `DeleteBuilder` | `WHERE col BETWEEN ? AND ?` (AND) | +| `withDefaults(QueryBuilderDefaults)` | `DeleteBuilder` | Set per-instance configuration defaults; throws `NullPointerException` if `null` | | `build()` | `SqlResult` | Render with standard dialect | | `build(SqlDialect)` | `SqlResult` | Render with specified dialect | @@ -242,7 +247,7 @@ getters and setters; setters are used exclusively by the builders. |--------|-------------| | `JoinClause(Type, String table, String on)` | Plain-table join | | `JoinClause(Type, Query subquery, String alias, String on)` | Subquery (derived-table) join | -| `getType()` | `JoinClause.Type` — `INNER`, `LEFT`, `RIGHT`, or `CROSS` | +| `getType()` | `JoinClause.Type`: `INNER`, `LEFT`, `RIGHT`, or `CROSS` | Join type | | `getTable()` | Table name for plain-table join; `null` for subquery join | | `getSubquery()` | Subquery for derived-table join; `null` for plain-table join | | `getAlias()` | Alias for derived-table join | @@ -279,9 +284,9 @@ public interface QueryableStorage { | Member | Description | |--------|-------------| -| `STANDARD` | ANSI SQL — no identifier quoting | -| `MYSQL` | MySQL — back-tick quoting; DELETE LIMIT supported | -| `SQLITE` | SQLite — double-quote quoting; DELETE LIMIT supported | +| `STANDARD` | ANSI SQL (no identifier quoting) | +| `MYSQL` | MySQL: back-tick quoting; DELETE LIMIT supported | +| `SQLITE` | SQLite: double-quote quoting; DELETE LIMIT supported | | `render(Query)` | Render a SELECT query to `SqlResult` | | `renderDelete(Query)` | Render a DELETE query to `SqlResult` | @@ -296,6 +301,49 @@ public interface QueryableStorage { --- +## configuration + +### `QueryBuilderDefaults` + +Immutable configuration object. See [Configuration](configuration) for a full +usage guide. + +**Static methods** + +| Method | Returns | Description | +|--------|---------|-------------| +| `global()` | `QueryBuilderDefaults` | Current JVM-wide defaults instance | +| `setGlobal(defaults)` | `void` | Replace the JVM-wide defaults; throws `NullPointerException` if `null` | +| `builder()` | `Builder` | New builder pre-filled with canonical defaults | +| `builder(source)` | `Builder` | New builder copied from `source`; throws `NullPointerException` if `null` | + +**Instance getters** + +| Method | Returns | Description | +|--------|---------|-------------| +| `getDialect()` | `SqlDialect` | Configured SQL dialect | +| `getDefaultColumns()` | `String` | Default SELECT column expression | +| `getDefaultLimit()` | `int` | Default LIMIT; `-1` means none | +| `getDefaultOffset()` | `int` | Default OFFSET; `-1` means none | +| `getLikePrefix()` | `String` | Prefix for LIKE values | +| `getLikeSuffix()` | `String` | Suffix for LIKE values | + +--- + +### `QueryBuilderDefaults.Builder` + +| Method | Returns | Description | +|--------|---------|-------------| +| `dialect(SqlDialect)` | `Builder` | Set dialect; throws `NullPointerException` if `null` | +| `defaultColumns(String)` | `Builder` | Set default SELECT columns; throws `NullPointerException` if `null` | +| `defaultLimit(int)` | `Builder` | Set default LIMIT; pass `-1` to disable | +| `defaultOffset(int)` | `Builder` | Set default OFFSET; pass `-1` to disable | +| `likePrefix(String)` | `Builder` | Set LIKE prefix; throws `NullPointerException` if `null` | +| `likeSuffix(String)` | `Builder` | Set LIKE suffix; throws `NullPointerException` if `null` | +| `build()` | `QueryBuilderDefaults` | Build the immutable configuration object | + +--- + ## exception ### `QueryBuilderException` diff --git a/docs/conditions.md b/docs/conditions.md index 2474755..b7fe103 100644 --- a/docs/conditions.md +++ b/docs/conditions.md @@ -17,7 +17,7 @@ description: "Operators, Condition, ConditionEntry, Connector AND/OR, and the or ## Overview -Every `where*` call on a builder creates a `ConditionEntry` — a triple of: +Every `where*` call on a builder creates a `ConditionEntry` with three properties: - a **column name** (or `null` for EXISTS subquery conditions) - a **`Condition`** (operator + value) @@ -84,7 +84,7 @@ constant and the SQL it generates. ## AND vs OR connector -### Default — AND +### Default behavior (AND) All `where*` methods use `AND`: @@ -96,7 +96,7 @@ new QueryBuilder() // → WHERE role = ? AND active = ? ``` -### OR — use the `orWhere*` variant +### OR conditions ```java new QueryBuilder() diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..8c36f23 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,205 @@ +--- +title: Configuration +nav_order: 8 +description: "QueryBuilderDefaults: global and per-query preset for dialect, columns, limit, offset, and LIKE wrapping" +--- + +# Configuration +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +`QueryBuilderDefaults` is an immutable configuration object that holds the +defaults applied by every builder (`QueryBuilder`, `SelectBuilder`, +`DeleteBuilder`) when no explicit value is provided. + +You configure it once at application startup and every builder created +afterward automatically honours those settings. You can also override the +defaults for a single builder instance using `.withDefaults()`. + +**Configurable settings:** + +| Setting | Default | Description | +|---------|---------|-------------| +| `dialect` | `SqlDialect.STANDARD` | SQL dialect used for identifier quoting | +| `defaultColumns` | `"*"` | Column expression used in `SELECT` when none are specified | +| `defaultLimit` | `-1` (no limit) | `LIMIT` applied when the builder has no `.limit()` call | +| `defaultOffset` | `-1` (no offset) | `OFFSET` applied when the builder has no `.offset()` call | +| `likePrefix` | `"%"` | Prefix wrapped around values in `LIKE` conditions | +| `likeSuffix` | `"%"` | Suffix wrapped around values in `LIKE` conditions | + +--- + +## Setting a global dialect + +Call `QueryBuilderDefaults.setGlobal()` once at startup. All builders created +after the call will use the new defaults. + +```java +import com.github.ezframework.javaquerybuilder.query.QueryBuilderDefaults; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; + +// Use SQLite for every query in this application +QueryBuilderDefaults.setGlobal( + QueryBuilderDefaults.builder() + .dialect(SqlDialect.SQLITE) + .build() +); +``` + +After this call you no longer need to pass a dialect to `buildSql()`: + +```java +SqlResult result = new QueryBuilder() + .from("users") + .whereEquals("id", 1) + .buildSql(); +// SELECT * FROM "users" WHERE "id" = ? +``` + +--- + +## Full configuration example + +```java +QueryBuilderDefaults.setGlobal( + QueryBuilderDefaults.builder() + .dialect(SqlDialect.MYSQL) + .defaultColumns("id, name, created_at") + .defaultLimit(100) + .defaultOffset(0) + .likePrefix("%") + .likeSuffix("%") + .build() +); +``` + +--- + +## Per-query override + +Use `.withDefaults()` on any builder to override the global configuration for +that one query. Any explicit value you set on the builder (such as `.limit()`) +always beats the defaults. + +```java +// Override to use SQLite for this query only +SqlResult result = new QueryBuilder() + .withDefaults( + QueryBuilderDefaults.builder(QueryBuilderDefaults.global()) + .dialect(SqlDialect.SQLITE) + .build() + ) + .from("users") + .buildSql(); +// SELECT * FROM "users" +``` + +{: .note } +> `QueryBuilderDefaults.builder(source)` copies all settings from an existing +> instance so you only need to override the fields you want to change. + +--- + +## Explicit dialect argument always wins + +Passing a dialect directly to `buildSql()` or `build(SqlDialect)` takes +precedence over both the global defaults and any `.withDefaults()` setting. + +```java +QueryBuilderDefaults.setGlobal( + QueryBuilderDefaults.builder().dialect(SqlDialect.MYSQL).build() +); + +// Explicit argument beats the global setting +SqlResult result = new QueryBuilder() + .from("users") + .buildSql("users", SqlDialect.SQLITE); +// Uses SQLite quoting regardless of the global setting +``` + +--- + +## Custom LIKE wrapping + +`whereLike` wraps the value with `%` on both sides by default. Change the +wrapping globally or per query: + +```java +// No wrapping (exact LIKE match) +QueryBuilderDefaults.builder() + .likePrefix("") + .likeSuffix("") + .build(); + +// Suffix-only (starts-with search) +QueryBuilderDefaults.builder() + .likePrefix("") + .likeSuffix("%") + .build(); +``` + +--- + +## Restoring defaults + +`QueryBuilderDefaults.builder()` always starts from the canonical +out-of-the-box values. To reset the global configuration: + +```java +QueryBuilderDefaults.setGlobal(QueryBuilderDefaults.builder().build()); +``` + +--- + +## API summary + +### `QueryBuilderDefaults` (static methods) + +| Method | Returns | Description | +|--------|---------|-------------| +| `global()` | `QueryBuilderDefaults` | The current JVM-wide defaults instance | +| `setGlobal(defaults)` | `void` | Replace the JVM-wide defaults; throws `NullPointerException` if `null` | +| `builder()` | `Builder` | New builder pre-filled with canonical defaults | +| `builder(source)` | `Builder` | New builder copied from `source`; throws `NullPointerException` if `null` | + +### `QueryBuilderDefaults` (instance getters) + +| Method | Returns | Description | +|--------|---------|-------------| +| `getDialect()` | `SqlDialect` | The configured SQL dialect | +| `getDefaultColumns()` | `String` | Default SELECT column expression | +| `getDefaultLimit()` | `int` | Default LIMIT value; `-1` means none | +| `getDefaultOffset()` | `int` | Default OFFSET value; `-1` means none | +| `getLikePrefix()` | `String` | Prefix for LIKE values | +| `getLikeSuffix()` | `String` | Suffix for LIKE values | + +### `QueryBuilderDefaults.Builder` + +| Method | Returns | Description | +|--------|---------|-------------| +| `dialect(SqlDialect)` | `Builder` | Set dialect; throws `NullPointerException` if `null` | +| `defaultColumns(String)` | `Builder` | Set default SELECT columns; throws `NullPointerException` if `null` | +| `defaultLimit(int)` | `Builder` | Set default LIMIT; pass `-1` to disable | +| `defaultOffset(int)` | `Builder` | Set default OFFSET; pass `-1` to disable | +| `likePrefix(String)` | `Builder` | Set LIKE prefix; throws `NullPointerException` if `null` | +| `likeSuffix(String)` | `Builder` | Set LIKE suffix; throws `NullPointerException` if `null` | +| `build()` | `QueryBuilderDefaults` | Build the immutable configuration object | + +### Builders that support `withDefaults()` + +All three builders throw `NullPointerException` if `null` is passed. + +| Builder | Method signature | +|---------|-----------------| +| `QueryBuilder` | `withDefaults(QueryBuilderDefaults defaults)` | +| `SelectBuilder` | `withDefaults(QueryBuilderDefaults defaults)` | +| `DeleteBuilder` | `withDefaults(QueryBuilderDefaults defaults)` | diff --git a/docs/exceptions.md b/docs/exceptions.md index de54e23..6503ab2 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -1,6 +1,6 @@ --- title: Exceptions -nav_order: 9 +nav_order: 10 description: "Exception hierarchy, when each exception is thrown, and handling patterns" --- @@ -19,13 +19,13 @@ description: "Exception hierarchy, when each exception is thrown, and handling p The library defines three standalone checked exceptions in the `com.github.ezframework.javaquerybuilder.query.exception` package. -None extends the others — each signals a distinct failure mode. +None extends the others; each signals a distinct failure mode. ```text Exception - ├── QueryBuilderException — general builder or configuration error - ├── QueryException — query-level runtime error - └── QueryRenderException — SQL rendering error + ├── QueryBuilderException : general builder or configuration error + ├── QueryException : query-level runtime error + └── QueryRenderException : SQL rendering error ``` All three share the same four constructor signatures. @@ -57,7 +57,7 @@ try { ## QueryException -Thrown for runtime errors at the query level — for example, when a +Thrown for runtime errors at the query level, for example when a `QueryableStorage` implementation encounters an error during in-memory evaluation. @@ -82,7 +82,7 @@ try { ## QueryRenderException -Thrown when a `Query` cannot be rendered to SQL — for example, if required +Thrown when a `Query` cannot be rendered to SQL, for example if required fields are missing or the query state is inconsistent at render time. ```java @@ -114,7 +114,7 @@ try { List ids = store.query(q); } catch (QueryRenderException e) { - // Rendering failed — log and return a safe error response + // Rendering failed, log and return a safe error response } catch (QueryException e) { // In-memory evaluation failed @@ -130,12 +130,12 @@ Exception messages may contain internal column names or values. Map exceptions to safe, generic responses before returning them to external clients. ```java -// CORRECT — map to a safe API response +// CORRECT: map to a safe API response catch (QueryRenderException e) { return Response.serverError().entity("Query rendering error").build(); } -// WRONG — leaks internal details +// WRONG: leaks internal details catch (QueryRenderException e) { return Response.serverError().entity(e.getMessage()).build(); } diff --git a/docs/in-memory.md b/docs/in-memory.md index ef48c7b..dde83ec 100644 --- a/docs/in-memory.md +++ b/docs/in-memory.md @@ -1,6 +1,6 @@ --- title: In-Memory Filtering -nav_order: 8 +nav_order: 9 description: "Filtering in-memory collections with QueryableStorage" --- @@ -38,7 +38,7 @@ caller controls how records are loaded by ID after filtering. Each `ConditionEntry` in the `Query` holds a `Condition` with an `Operator` and a value. `Condition.matches(Map, String key)` evaluates the -condition against an attribute map — no SQL dialect or database connection is +condition against an attribute map. No SQL dialect or database connection is required. The `QueryableStorage` implementation is responsible for: @@ -153,7 +153,7 @@ List ids = store.query(q); | `EQ` | `Objects.equals(stored, value)` | | `NEQ` | `!Objects.equals(stored, value)` | | `GT` / `GTE` / `LT` / `LTE` | Numeric comparison; coerces `Long`/`Integer`/`Double` as needed | -| `LIKE` | `stored.toString().contains(value)` — substring match | +| `LIKE` | `stored.toString().contains(value)` (substring match) | | `NOT_LIKE` | Negated `LIKE` | | `IS_NULL` | `!map.containsKey(key) \|\| map.get(key) == null` | | `IS_NOT_NULL` / `EXISTS` | `map.containsKey(key) && map.get(key) != null` | diff --git a/docs/index.md b/docs/index.md index 53ccc1a..02bb48d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,20 +19,22 @@ No runtime dependencies required. ## Features -- **Fluent SELECT builder** — `from`, `select`, `distinct`, `where*`, +- **Fluent SELECT builder**: `from`, `select`, `distinct`, `where*`, `orderBy`, `groupBy`, `havingRaw`, `limit`, `offset` -- **DML builders** — `InsertBuilder`, `UpdateBuilder`, `DeleteBuilder`, `CreateBuilder` -- **Parameterized-only** — user values always go through `?` bind parameters; SQL injection +- **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`, +- **16 operators**: equality, comparison, `LIKE`, `NULL` checks, `IN`, `BETWEEN`, `EXISTS`, and subquery operators -- **Subquery support** — `WHERE col IN (SELECT ...)`, `WHERE EXISTS (SELECT ...)`, +- **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) -- **In-memory filtering** — `QueryableStorage` functional interface applies the same `Query` +- **Three SQL dialects**: `STANDARD` (ANSI), `MYSQL` (back-tick quoting), `SQLITE` (double-quote) +- **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` to flat-map collections without touching a database -- **Zero runtime dependencies** — pure Java 25+, nothing to shade or exclude +- **Zero runtime dependencies**: pure Java 25+, nothing to shade or exclude --- @@ -100,11 +102,12 @@ 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` | +| [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` | | [Subqueries](subqueries) | All six subquery variants | | [SQL Dialects](sql-dialects) | `STANDARD`, `MYSQL`, `SQLITE`, `SqlResult`, dialect matrix | -| [In-Memory Filtering](in-memory) | `QueryableStorage` — filter collections without a database | +| [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 | | [API Reference](api-reference) | Full public-method tables for every class | diff --git a/docs/installation.md b/docs/installation.md index 899bf9a..37937f5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -110,7 +110,7 @@ authenticate with a personal access token that has `read:packages` scope. ## Verifying the installation -Add this snippet to a test class — it should compile and run without errors: +Add this snippet to a test class. It should compile and run without errors: ```java import com.github.ezframework.javaquerybuilder.query.builder.QueryBuilder; diff --git a/docs/query-builder.md b/docs/query-builder.md index d362577..6041098 100644 --- a/docs/query-builder.md +++ b/docs/query-builder.md @@ -40,7 +40,7 @@ List params = result.getParameters(); ``` `QueryBuilder` is also the gateway to all DML builders via its static factory -methods — see [DML Builders](dml-builders). +methods. See [DML Builders](dml-builders). --- @@ -55,7 +55,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 @@ -158,7 +158,7 @@ new QueryBuilder() ### HAVING -Pass a raw SQL fragment — no value interpolation; use static expressions only: +Pass a raw SQL fragment with no value interpolation. Use static expressions only: ```java .groupBy("category") @@ -185,7 +185,7 @@ Pass a raw SQL fragment — 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,7 +197,7 @@ 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: @@ -239,7 +239,7 @@ See [Subqueries](subqueries) for full examples. ## Security Every value passed to a `where*` method is placed in the `?` bind-parameter -list of the rendered `SqlResult` — it is never concatenated into the SQL string. +list of the rendered `SqlResult`. It is never concatenated into the SQL string. ```java // Safe even if userInput contains SQL metacharacters @@ -256,5 +256,5 @@ SqlResult r = new QueryBuilder() {: .warning } > Column names and table names are **not** parameterized. Always use static, -> known-safe strings for those arguments — never forward user input as a +> known-safe strings for those arguments. Never forward user input as a > column or table name. diff --git a/docs/sql-dialects.md b/docs/sql-dialects.md index 4986874..1b7d6fc 100644 --- a/docs/sql-dialects.md +++ b/docs/sql-dialects.md @@ -41,14 +41,14 @@ SqlResult r1 = new QueryBuilder() .buildSql(); // → SELECT * FROM users WHERE id = ? -// MySQL — identifiers back-tick quoted +// MySQL: back-tick quoted identifiers SqlResult r2 = new QueryBuilder() .from("users") .whereEquals("id", 1) .buildSql(SqlDialect.MYSQL); // → SELECT * FROM `users` WHERE `id` = ? -// SQLite — identifiers double-quoted +// SQLite: double-quoted identifiers SqlResult r3 = new QueryBuilder() .from("users") .whereEquals("id", 1) @@ -96,15 +96,15 @@ Query q = new QueryBuilder() .limit(500) .build(); -// Standard — LIMIT ignored +// Standard: LIMIT ignored SqlResult std = SqlDialect.STANDARD.renderDelete(q); // → DELETE FROM sessions WHERE expired = ? -// MySQL — LIMIT honored +// MySQL: LIMIT honored SqlResult my = SqlDialect.MYSQL.renderDelete(q); // → DELETE FROM `sessions` WHERE `expired` = ? LIMIT 500 -// SQLite — LIMIT honored +// SQLite: LIMIT honored SqlResult sq = SqlDialect.SQLITE.renderDelete(q); // → DELETE FROM "sessions" WHERE "expired" = ? LIMIT 500 ``` @@ -129,7 +129,7 @@ The same `Query` produces different SQL across dialects due to identifier quotin `AbstractSqlDialect` implements the shared rendering logic for SELECT and DELETE queries. It is the base class for both `MySqlDialect` and `SqliteDialect`. -**Subquery parameter ordering** — parameters are collected depth-first in this +**Subquery parameter ordering**: parameters are collected depth-first in this order: 1. SELECT-list scalar subquery parameters (left to right) diff --git a/docs/subqueries.md b/docs/subqueries.md index ea70ddd..e446027 100644 --- a/docs/subqueries.md +++ b/docs/subqueries.md @@ -1,7 +1,7 @@ --- title: Subqueries nav_order: 6 -description: "All six subquery variants — IN, EXISTS, NOT EXISTS, scalar, FROM-derived table, JOIN, and scalar SELECT" +description: "All six subquery variants: IN, EXISTS, NOT EXISTS, scalar, FROM-derived table, JOIN, and scalar SELECT" --- # Subqueries From 70cbbf9b3e09129b5f99156771e2eb49a094987f Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sat, 18 Apr 2026 19:57:56 +0200 Subject: [PATCH 11/11] docs: fix markdown format --- docs/api-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 85bfe6b..7eee228 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -247,7 +247,7 @@ getters and setters; setters are used exclusively by the builders. |--------|-------------| | `JoinClause(Type, String table, String on)` | Plain-table join | | `JoinClause(Type, Query subquery, String alias, String on)` | Subquery (derived-table) join | -| `getType()` | `JoinClause.Type`: `INNER`, `LEFT`, `RIGHT`, or `CROSS` | Join type | +| `getType()` | `JoinClause.Type`: `INNER`, `LEFT`, `RIGHT`, or `CROSS` | | `getTable()` | Table name for plain-table join; `null` for subquery join | | `getSubquery()` | Subquery for derived-table join; `null` for plain-table join | | `getAlias()` | Alias for derived-table join |