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 +```