Skip to content

Commit fb5cb3a

Browse files
committed
Release v1.1.0
Two major feature areas: • Object mapping — queryAs / queryAsList / executeAs / executeAsList / JsonValue.as(Class) map query results directly to Java records, POJOs, and scalars, for both single-source and multi-source JOIN queries. New MappingSettings + MissingFieldPolicy (IGNORE / FAIL), new SQL4JsonMappingException. Internal mapper/ package is JPMS-hidden so the implementation can evolve without API churn. • JDBC-style parameter binding — ? (positional) and :name (named) placeholders in WHERE / SELECT / GROUP BY / HAVING / JOIN / function args / LIMIT / OFFSET, bound via BoundParameters on PreparedQuery and SQL4JsonEngine. IN-list expansion (IN (?) + collection → N literals; empty collection → zero-row predicate), LIMIT/OFFSET validation, new SQL4JsonBindException, LimitsSettings.maxParameters (default 1024 placeholders per query). Parameterised queries bypass the engine's result cache. Notable fix: parameterised CASE WHEN (SearchedCaseWhen) now substitutes bound values in the embedded CriteriaNode. Previously any ? / :name inside a CASE WHEN condition crashed at execute time with "ParameterRef reached evaluator". Internal: ISO date/datetime/instant parsing consolidated into json.IsoTemporals; registry.DateCoercion delegates. Grammar additions: POSITIONAL_PARAM, NAMED_PARAM, limitValue nonterminal. Zero new runtime dependencies. 1753 tests passing. See CHANGELOG.md for full details.
1 parent 41274b7 commit fb5cb3a

76 files changed

Lines changed: 5149 additions & 179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@ All notable changes to SQL4Json are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2026-04-23
9+
10+
### Added — Object Mapping
11+
- `SQL4Json.queryAs` and `queryAsList` — map query results directly to Java records, POJOs, or basic types (both single-source and JOIN variants).
12+
- `PreparedQuery.executeAs` and `executeAsList` (`String` and `JsonValue` overloads).
13+
- `SQL4JsonEngine.queryAs` and `queryAsList`.
14+
- `JsonValue.as(Class)` and `JsonValue.as(Class, Sql4jsonSettings)` default methods on the sealed interface.
15+
- `MappingSettings` subsection of `Sql4jsonSettings` with `MissingFieldPolicy` enum (`IGNORE` / `FAIL`).
16+
- `SQL4JsonMappingException` (new sealed subclass of `SQL4JsonException`).
17+
18+
### Added — Parameter Binding
19+
- `PreparedQuery.execute(String json, BoundParameters params)` and `execute(JsonValue, BoundParameters)`.
20+
- `PreparedQuery.execute(String json, Object... positionalParams)` shortcut.
21+
- `PreparedQuery.execute(String json, Map<String, ?> namedParams)` shortcut.
22+
- `PreparedQuery.executeAs` / `executeAsList` with `BoundParameters` overloads.
23+
- `SQL4JsonEngine.query` / `queryAsJsonValue` / `queryAs` / `queryAsList` with `BoundParameters` overloads.
24+
- `BoundParameters` immutable carrier (named & positional modes, `named()` / `positional()` / `of(Object...)` / `of(Map)` factories, `bind` / `bindAll`, `EMPTY` singleton).
25+
- Grammar support for `?` (positional) and `:name` (named) placeholders throughout WHERE / SELECT / GROUP BY / HAVING / JOIN / function arguments.
26+
- Dynamic `LIMIT` / `OFFSET` binding (placeholders accepted in both positions).
27+
- IN-list expansion: `IN (?)` + collection bound → expanded to N literals; empty collection → zero-row predicate.
28+
- `LimitsSettings.maxParameters` (default `1024`) — DoS guard against placeholder flooding.
29+
- `SQL4JsonBindException` (new sealed subclass of `SQL4JsonException`) — surfaced at substitute time for missing / extra / type-mismatch bindings, IN-list overflow, and LIMIT/OFFSET validation failures.
30+
31+
### Changed
32+
- `Sql4jsonSettings` gains a `mapping` component (all existing construction paths preserved via builder).
33+
- Grammar now recognizes `?` and `:name` as placeholders in value positions.
34+
- `LimitsSettings` canonical-constructor signature extended (`maxParameters`) — public callers using `.builder()` unaffected.
35+
- `SQL4JsonEngine` bypasses `QueryResultCache` for parameterized queries.
36+
- Internal: ISO date/datetime/instant parsing consolidated into `json.IsoTemporals`; `registry.DateCoercion` delegates.
37+
38+
### Dependencies
39+
- No new runtime dependencies — zero-dep philosophy preserved.
40+
841
## [1.0.0] - 2026-04-10
942

1043
Initial public release.
@@ -76,4 +109,5 @@ Initial public release.
76109
- OWASP dependency-check plugin
77110
- Dependabot for automated dependency updates
78111

112+
[1.1.0]: https://github.com/mnesimiyilmaz/sql4json/releases/tag/v1.1.0
79113
[1.0.0]: https://github.com/mnesimiyilmaz/sql4json/releases/tag/v1.0.0

CLAUDE.md

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,24 @@ SQL4Json is a Java library that enables SQL-like querying of JSON data. It parse
2323

2424
# Run a single test
2525
./mvnw test -Dtest="SQL4JsonQueryTests#when_select_asterisk_then_return_all"
26+
# Run multiple methods in one class
27+
./mvnw test -Dtest='ClassName#m1+m2+m3'
28+
29+
# Dry-run the full release profile without publishing or signing —
30+
# builds sources + javadoc jars and exercises the release pipeline so
31+
# release-only failures surface before tagging.
32+
./mvnw clean deploy -Prelease -DskipPublishing=true -Dgpg.skip=true
2633
```
2734

2835
Tests use JUnit 5 covering unit tests, integration tests, edge cases, and performance. **Gotcha:** Default `./mvnw test` excludes tests tagged `@Tag("large")` — run `./mvnw test -Plarge-tests` to execute only those. CI runs `spotless:check` then `clean verify` via GitHub Actions (`ci.yml`).
2936

37+
**Never run two `./mvnw` invocations in parallel against this repo** — any goal that touches `target/` (including `clean`) races with any other goal reading it and fails with "Failed to delete target". Run maven commands sequentially.
38+
39+
**Reliable way to run an isolated Java snippet:** write a throwaway `@Test` under `src/test/java` and run via `-Dtest=`. Ad-hoc `javac -cp ...` against the Maven local repo is brittle (classpath assembly, Windows separator issues) — use the maven harness.
40+
3041
## Public API
3142

32-
Six usage patterns (all via static methods on `SQL4Json`):
43+
Nine usage patterns (all via static methods on `SQL4Json`):
3344

3445
```java
3546
// 1. One-off query with defaults
@@ -75,9 +86,36 @@ SQL4JsonEngine engine = SQL4Json.engine()
7586
.data("orders", ordersJson)
7687
.build();
7788
String result = engine.query(joinSql);
89+
90+
// 7. Typed result — record / POJO / scalar (single row)
91+
record Person(String name, int age) {}
92+
Person p = SQL4Json.queryAs(sql, jsonString, Person.class);
93+
// or via JsonValue
94+
Person p2 = SQL4Json.queryAsJsonValue(sql, jsonString).as(Person.class);
95+
96+
// 8. Typed result — List<T>
97+
List<Person> people = SQL4Json.queryAsList(sql, jsonString, Person.class);
98+
// Custom missing-field policy:
99+
Sql4jsonSettings strict = Sql4jsonSettings.builder()
100+
.mapping(m -> m.missingFieldPolicy(MissingFieldPolicy.FAIL))
101+
.build();
102+
List<Person> strictPeople = SQL4Json.queryAsList(sql, jsonString, Person.class, strict);
103+
104+
// 9. Parameterized query (JDBC-style parameter binding — PreparedQuery / Engine only)
105+
PreparedQuery q = SQL4Json.prepare("SELECT * FROM $r WHERE age > :min AND dept = :d");
106+
String result = q.execute(jsonString, BoundParameters.named()
107+
.bind("min", 25).bind("d", "Engineering"));
108+
109+
// Positional + IN expansion:
110+
PreparedQuery q2 = SQL4Json.prepare("SELECT * FROM $r WHERE id IN (?)");
111+
String r2 = q2.execute(jsonString, BoundParameters.of(List.of(1, 2, 3)));
112+
113+
// Combined with typed result:
114+
List<Person> engineers = SQL4Json.prepare("SELECT * FROM $r WHERE dept = :d")
115+
.executeAsList(jsonString, Person.class, BoundParameters.named().bind("d", "Engineering"));
78116
```
79117

80-
Key public types: `SQL4Json`, `PreparedQuery`, `SQL4JsonEngine`, `SQL4JsonEngineBuilder`, `Sql4jsonSettings`, `SecuritySettings`, `LimitsSettings`, `CacheSettings`, `DefaultJsonCodecSettings`, `DuplicateKeyPolicy`, `JsonCodec`, `QueryResultCache` (cache SPI), `JsonValue` (sealed), `SQL4JsonException` (sealed).
118+
Key public types: `SQL4Json`, `PreparedQuery`, `SQL4JsonEngine`, `SQL4JsonEngineBuilder`, `Sql4jsonSettings`, `SecuritySettings`, `LimitsSettings`, `CacheSettings`, `MappingSettings`, `MissingFieldPolicy`, `DefaultJsonCodecSettings`, `DuplicateKeyPolicy`, `JsonCodec`, `QueryResultCache` (cache SPI), `JsonValue` (sealed), `SQL4JsonException` (sealed), `SQL4JsonMappingException`, `BoundParameters`, `SQL4JsonBindException`.
81119

82120
## Architecture & Data Flow
83121

@@ -108,21 +146,41 @@ Input JSON + SQL
108146
→ Returns String (or JsonValue via queryAsJsonValue overloads)
109147
```
110148

111-
Key packages: `engine/` (QueryExecutor, QueryPipeline, Expression, ExpressionEvaluator, Row, FieldKey, WindowSpec, JoinExecutor, JoinKey, StreamMaterializer), `engine/stage/` (WhereStage, GroupByStage, HavingStage, WindowStage, OrderByStage, TopNOrderByStage, LimitStage, SelectStage, DistinctStage), `parser/` (QueryParser, QueryDefinition, SQL4JsonParserListener, JoinDef, JoinType, JoinEquality), `registry/` (FunctionRegistry, OperatorRegistry, ConditionHandlerRegistry, CriteriaNode), `json/` (JsonParser, JsonSerializer, DefaultJsonCodec, JsonFlattener, JsonUnflattener, StreamingJsonParser, StreamingSerializer, CompactStringMap, JsonToSqlConverter, JsonValue records), `settings/` (Sql4jsonSettings, SecuritySettings, LimitsSettings, CacheSettings, DefaultJsonCodecSettings, DuplicateKeyPolicy), `types/` (SqlValue hierarchy), `sorting/` (SqlValueComparator), `grouping/` (GroupAggregator, GroupKey), `exception/` (sealed exception hierarchy).
149+
Key packages: `engine/` (QueryExecutor, QueryPipeline, Expression, ExpressionEvaluator, Row, FieldKey, WindowSpec, JoinExecutor, JoinKey, StreamMaterializer), `engine/stage/` (WhereStage, GroupByStage, HavingStage, WindowStage, OrderByStage, TopNOrderByStage, LimitStage, SelectStage, DistinctStage), `parser/` (QueryParser, QueryDefinition, SQL4JsonParserListener, JoinDef, JoinType, JoinEquality), `registry/` (FunctionRegistry, OperatorRegistry, ConditionHandlerRegistry, CriteriaNode), `json/` (JsonParser, JsonSerializer, DefaultJsonCodec, JsonFlattener, JsonUnflattener, StreamingJsonParser, StreamingSerializer, CompactStringMap, JsonToSqlConverter, IsoTemporals, JsonValue records), `mapper/` (JsonValueMapper, TypeDescriptor, TypeIntrospection, MappingPath, VisitedStack — internal, not exported), `settings/` (Sql4jsonSettings, SecuritySettings, LimitsSettings, CacheSettings, MappingSettings, MissingFieldPolicy, DefaultJsonCodecSettings, DuplicateKeyPolicy), `types/` (SqlValue hierarchy), `sorting/` (SqlValueComparator), `grouping/` (GroupAggregator, GroupKey), `exception/` (sealed exception hierarchy).
112150

113151
## Code Conventions
114152

115153
- **Sealed types:** Used extensively — sealed interfaces (`JsonValue`, `SqlValue`, `SQL4JsonException`, `Expression`) with record implementations for type-safe pattern matching.
116-
- **Expression AST:** Sealed `Expression` interface (`ColumnRef`, `ScalarFnCall`, `AggregateFnCall`, `LiteralVal`, `WindowFnCall`, `SimpleCaseWhen`, `SearchedCaseWhen`, `NowRef`) represents all column expressions. `ExpressionEvaluator` is the tree-walking interpreter — used by all pipeline stages, condition handlers, and aggregation. `WindowFnCall` is evaluated by `WindowStage`, not by `ExpressionEvaluator`. Functions nest arbitrarily: `ROUND(AVG(NULLIF(col, 0)), 2)`.
154+
- **Expression AST:** Sealed `Expression` interface (`ColumnRef`, `ScalarFnCall`, `AggregateFnCall`, `LiteralVal`, `WindowFnCall`, `SimpleCaseWhen`, `SearchedCaseWhen`, `NowRef`, `ParameterRef`) represents all column expressions. `ExpressionEvaluator` is the tree-walking interpreter — used by all pipeline stages, condition handlers, and aggregation. `WindowFnCall` is evaluated by `WindowStage`, not by `ExpressionEvaluator`. Functions nest arbitrarily: `ROUND(AVG(NULLIF(col, 0)), 2)`.
155+
- **AST cross-hierarchy trap:** `Expression` and `CriteriaNode` are two separate sealed hierarchies that intersect inside `SearchedCaseWhen.SearchWhen` (it holds both a `CriteriaNode` condition and an `Expression` result). Any transformation or analysis that walks `Expression` must explicitly descend into the embedded `CriteriaNode` too — recursing only through the expression branches silently misses WHEN conditions.
156+
- **Adding a new sealed variant is cross-cutting:** a new `Expression` / `CriteriaNode` / `SqlValue` / `JsonValue` subtype requires updating every exhaustive `switch` on the parent. Grep the sealed parent name before adding.
117157
- **JSON flattening:** Core mechanism. Nested JSON is flattened to `Map<FieldKey, Object>` for processing, then unflattened for output. `FieldKey` tracks the "family" (base path) for nested field grouping.
118158
- **ANTLR generated code:** Located in `target/generated-sources/antlr4/`. Never edit generated files — modify `SQL4Json.g4` in `src/main/antlr4/` instead. Grammar changes require a rebuild (`mvn clean compile`).
119-
- **Settings subsections:** `Sql4jsonSettings` is composed of four immutable record subsections (`security`, `limits`, `cache`, `codec`). Customize via `Sql4jsonSettings.builder().limits(l -> l.maxRowsPerQuery(N)).build()`. `Sql4jsonSettings.defaults()` is a JVM-wide shared singleton — all no-settings API calls land on it so the `BoundedPatternCache` and derived `ConditionHandlerRegistry` are shared.
159+
- **Settings subsections:** `Sql4jsonSettings` is composed of five immutable record subsections (`security`, `limits`, `cache`, `mapping`, `codec`). Customize via `Sql4jsonSettings.builder().limits(l -> l.maxRowsPerQuery(N)).build()`. `Sql4jsonSettings.defaults()` is a JVM-wide shared singleton — all no-settings API calls land on it so the `BoundedPatternCache` and derived `ConditionHandlerRegistry` are shared.
120160
- **JSON codec limits:** Built-in JSON parser limits live in `DefaultJsonCodecSettings` (in the `settings` package, not `json`). Customize via `Sql4jsonSettings.builder().codec(new DefaultJsonCodec(DefaultJsonCodecSettings.builder()...)).build()`.
121161
- **Registry lifetime:** `ConditionHandlerRegistry.forSettings(settings)` is the single entry point and internally caches one registry per distinct `cache.likePatternCacheSize` — in practice one per JVM, since nearly every caller uses defaults. It shares a `BoundedPatternCache` between `LikeConditionHandler` and `NotLikeConditionHandler` (both package-private inside the `registry` package).
122162
- **Functional style:** Heavy use of Java streams, `BiPredicate`, `Function`, `Supplier`, `Optional`
123163
- **Spotless:** Code formatter configured — removes unused imports, trims trailing whitespace, ensures final newline. Run `./mvnw spotless:apply` before committing.
124164
- **Surefire config:** Tests run with `--add-modules=java.management` and `--add-reads=io.github.mnesimiyilmaz.sql4json=java.management` JVM flags, plus `-Xmx8g -Xms1g`.
125165

166+
## Code-Quality Guidelines (SonarQube-enforced)
167+
168+
The repo is scanned by SonarQube; write new code with these general principles in mind so findings don't accumulate.
169+
170+
- **Prefer record patterns over pattern variables.** When matching a record with `instanceof`, deconstruct its components directly rather than binding a variable and calling accessors on it. Java 21 record patterns are the default style here.
171+
- **Empty method bodies need a nested comment.** Javadoc above the signature doesn't satisfy the rule — put a short `//` inside the body stating why it's empty. Applies to constructors too.
172+
- **Keep cognitive complexity low.** Long `if/else` chains and deeply nested branches should be broken into small focused helpers. Reach for suppression only when extraction would genuinely hurt readability.
173+
- **Suppress rules sparingly, and only with a reason.** Use the narrowest scope (declaration or method, not whole class unless the rule is class-level) and add a one-line comment above the annotation stating why the suppression is correct.
174+
- **Use idiomatic Java 21 APIs.** Prefer modern sequenced-collection and convenience methods (`getFirst()`, `getLast()`, `reversed()`, `addFirst/Last`, etc.) over positional index arithmetic like `list.get(0)` or `list.get(list.size() - 1)`. Similarly, lean on switch expressions, pattern matching, `var`, and `List.of`/`Map.of` where they improve clarity.
175+
- **No dangling Javadoc.** Don't place `/** ... */` blocks where the Javadoc tool won't attach them — most commonly on individual record components inline in the header. Document components via `@param` tags on the record's top-level Javadoc instead. The same applies to local variables, statements, and any other position that isn't a declaration.
176+
177+
## Development Workflow
178+
179+
- **Javadoc is mandatory:** Every new development (public types, methods, fields) must include a Java API doc comment. Do not skip this step — write Javadoc as part of the implementation, not as a follow-up.
180+
- **Test placement:** Before creating a new test class, look for an existing test class where the new tests logically belong and add them there. Only create a new test file when no appropriate existing class exists — and state the reason explicitly in the response.
181+
- **`@since` on public API:** Any new public API addition must carry an `@since` Javadoc tag with the version number the change ships in. If the user has not specified a version number, ask before adding the tag. If the user declines to specify one, omit the `@since` tag rather than guessing.
182+
- **Tests must assert, not log:** a `try { ... } catch (Throwable t) { System.out.println(t); }` test passes silently even when the code throws. For "must not throw" / "must return X" behavior, assert on the success path so failure is a red CI; use `assertThrows` only when the throw is the contract.
183+
126184
## SQL Syntax
127185

128186
- `SELECT *`, specific columns, aliases (`AS`), aggregate functions (`COUNT`, `SUM`, `AVG`, `MIN`, `MAX`)
@@ -139,3 +197,4 @@ Key packages: `engine/` (QueryExecutor, QueryPipeline, Expression, ExpressionEva
139197
- **Window functions:** `ROW_NUMBER()`, `RANK()`, `DENSE_RANK()`, `NTILE(n)`, `LAG(col [, offset])`, `LEAD(col [, offset])` with `OVER (PARTITION BY ... ORDER BY ...)`. Aggregate functions (`SUM`, `AVG`, `COUNT`, `MIN`, `MAX`) also work as window functions with `OVER`. Window functions are only valid in SELECT and do not collapse rows (unlike GROUP BY). Evaluated by `WindowStage` (materializing, runs after HAVING, before ORDER BY).
140198
- `CASE` expressions: simple (`CASE expr WHEN val THEN result END`) and searched (`CASE WHEN condition THEN result END`), fully nestable in SELECT, WHERE, ORDER BY, GROUP BY, HAVING
141199
- **Functions:** String (LOWER, UPPER, CONCAT, SUBSTRING, TRIM, LENGTH, REPLACE, LEFT, RIGHT, LPAD, RPAD, REVERSE, POSITION), Math (ABS, ROUND, CEIL, FLOOR, MOD, POWER, SQRT, SIGN), Date (TO_DATE, NOW, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DATE_ADD, DATE_DIFF), Conversion (CAST, NULLIF, COALESCE), Aggregate (COUNT, SUM, AVG, MIN, MAX), Window (ROW_NUMBER, RANK, DENSE_RANK, NTILE, LAG, LEAD, plus aggregate functions with OVER)
200+
- **Parameters:** `?` (positional) and `:name` (named) placeholders in WHERE / SELECT / GROUP BY / HAVING / function args / LIMIT / OFFSET. Bound via `BoundParameters` on `PreparedQuery.execute(...)` or `SQL4JsonEngine.query(...)`. Cannot mix `?` and `:name` in the same query (parse-time error). IN-list expansion: `IN (?)` + collection bind expands to N literals; empty collection → zero-row predicate. Null bind produces SQL-standard `col = NULL` (always false) — use literal `IS NULL` / `IS NOT NULL` for nullability tests (cannot be parameterized). `SQL4JsonEngine` bypasses result cache for parameterized queries. `LimitsSettings.maxParameters` (default 1024) caps total placeholder count per query.

0 commit comments

Comments
 (0)