bmad-method-quarkus 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/removals.txt +10 -0
  3. package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
  4. package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +30 -10
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +163 -15
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +37 -4
  10. package/src/bmm-skills/module.yaml +0 -7
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  14. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  15. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  16. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  17. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  18. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -215
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -160
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -661
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -165
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -196
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -145
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -275
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
@@ -1,275 +0,0 @@
1
- ---
2
- name: quarkus-sql-jdbc-agroal
3
- description: Standard for executing SQL queries, updates, batches, and transactions in Quarkus WITHOUT Panache or any ORM — using the Agroal connection pool with plain JDBC (default) or the reactive Vert.x SQL client (justified cases only). Use this skill whenever the user writes or reviews ANY repository, DAO, SQL statement, SELECT/INSERT/UPDATE/DELETE, batch operation, transaction, pagination query, jsonb access, connection pool configuration, or mentions Agroal, JDBC, PreparedStatement, datasource, or "query the database" — all persistence code must follow these patterns. Data access lives in the slice `<Slice>Sql` class with `Connection` as the first parameter of every method (see quarkus-hexagonal-core skill); there are no repositories, DAOs or row-mapper classes.
4
- ---
5
-
6
- # SQL Execution Standard: Agroal + Plain JDBC (No ORM)
7
-
8
- Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults where they conflict.
9
-
10
- Persistence is explicit SQL through the Agroal pool. No Panache, no Hibernate, no reflection-based row mappers. Data access lives in the slice's `<Slice>Sql` class — the driven adapter of the vertical slice (see quarkus-hexagonal-core skill). There are no `*Repository` ports and no `Jdbc*` adapters: one `Sql` class per slice, called only by that slice's `Handler`.
11
-
12
- One clarification to keep teams from chasing ghosts: **Agroal is a JDBC (blocking) pool** — there is no "reactive Agroal". The reactive path in Quarkus is the Vert.x SQL client (`quarkus-reactive-pg-client`) with its own pool. Default choice here is **Agroal + JDBC** (simpler, dominant skill base, works perfectly in native); reactive client only for measured hot paths with extreme concurrency (see §7).
13
-
14
- ## 1. Datasource configuration (Agroal)
15
-
16
- ```properties
17
- quarkus.datasource.db-kind=postgresql
18
- quarkus.datasource.username=${DB_USER}
19
- quarkus.datasource.password=${DB_PASSWORD}
20
- quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
21
-
22
- # Pool sizing: start small; size = concurrent transactions, NOT concurrent users
23
- quarkus.datasource.jdbc.min-size=2
24
- quarkus.datasource.jdbc.max-size=16
25
- quarkus.datasource.jdbc.acquisition-timeout=5S
26
-
27
- # Hygiene
28
- quarkus.datasource.jdbc.validation-query-sql=SELECT 1
29
- quarkus.datasource.jdbc.leak-detection-interval=60S
30
- quarkus.datasource.jdbc.idle-removal-interval=5M
31
- quarkus.datasource.jdbc.max-lifetime=30M
32
-
33
- # Observability (pairs with quarkus-observability-otel skill)
34
- quarkus.datasource.jdbc.telemetry=true
35
- quarkus.datasource.metrics.enabled=true
36
- ```
37
-
38
- Rules:
39
- - `max-size` per pod must respect the DB's `max_connections` budget across ALL replicas of ALL services. Coordinate with DBAs; 16 is a ceiling, not a target.
40
- - Leak detection stays on in every environment — a logged leak is a bug, treat it as such.
41
- - Multiple datasources: named (`quarkus.datasource."audit".jdbc.url=...`) and injected with `@DataSource("audit")`.
42
-
43
- ## 2. Injection and the golden resource pattern
44
-
45
- Inject the pool, never raw drivers, never `DriverManager`. The `Sql` class holds the `DataSource` but **does not open connections** — the `Handler` opens exactly one per business operation and passes it in (see §3):
46
-
47
- ```java
48
- @ApplicationScoped
49
- public class CreatePartyIndividualSql {
50
-
51
- @Inject
52
- DataSource dataSource; // injected for symmetry/health checks; connections come from the Handler
53
- }
54
- ```
55
-
56
- **Every method's first parameter is `Connection conn`.** That signature is the contract the `Handler` must honour exactly — same name, order, arity and types. Statements use **try-with-resources on PreparedStatement AND ResultSet** (the `Connection` is owned and closed by the caller). A missed close under load exhausts the pool and takes the native pod down:
57
-
58
- ```java
59
- private static final String SELECT_PARTY_BY_ID = """
60
- SELECT id, tenant_id, party_type, status, created_at
61
- FROM customer.party
62
- WHERE id = ?
63
- AND tenant_id = ?
64
- """;
65
-
66
- public PartyDto findPartyById(Connection conn, UUID id, String tenantId) throws SQLException {
67
- try (PreparedStatement ps = conn.prepareStatement(SELECT_PARTY_BY_ID)) {
68
- ps.setObject(1, id);
69
- ps.setString(2, tenantId);
70
- try (ResultSet rs = ps.executeQuery()) {
71
- return rs.next() ? mapRow(rs) : null;
72
- }
73
- }
74
- }
75
- ```
76
-
77
- Absolute rules:
78
- - SQL as `private static final String` text blocks (comma-first concatenation is equally acceptable), schema-qualified. Never concatenate user input — `PreparedStatement` placeholders ALWAYS (SQL injection + plan cache).
79
- - Quote reserved-word schemas and tables: `"order".party`. Verify the real schema name before writing the query.
80
- - Dynamic WHERE clauses: build from a whitelist of column/operator constants, values still as placeholders.
81
- - Methods declare `throws SQLException` and never catch it — the `Handler` translates it (§8). No `commit`, `rollback` or `setAutoCommit` here, ever.
82
- - One private `mapRow(ResultSet) -> Dto` per `Sql` class. Rows map to the slice's own DTOs; there is no separate domain entity and no `*RowMapper` class. No reflection mappers: they break native and hide cost.
83
-
84
- Naming (canonical rules in the quarkus-hexagonal-core skill):
85
-
86
- | Artifact | Convention | Example |
87
- |---|---|---|
88
- | Data access class | `${SERVICE_CLASS_PREFIX}Sql`, in the slice folder | `CreatePartyIndividualSql` |
89
- | Method | imperative verb, matching the query | `insertParty`, `findPartyById`, `listPartiesByTenant` |
90
- | SQL constant | `UPPER_SNAKE_CASE` matching the method, `private static final String` | `SELECT_PARTY_BY_ID`, `INSERT_PARTY` |
91
- | Table / column | `snake_case`, singular table name, schema-qualified | `customer.party`, `created_at` |
92
- | Row target | the slice's `*Dto` | `PartyDto` |
93
- | Technical exception | `PersistenceException` + specific subtypes (§8) | `TransientPersistenceException` |
94
-
95
- Never name this class `*Repository`, `*Dao`, `*Manager` or `*Service` — those suffixes are banned by the core skill's ArchUnit rules. An `Sql` class that grows an `if` encoding a business rule has swallowed logic that belongs in the `Handler`.
96
-
97
- ## 3. Transactions and connections: one of each, on `Handler.process()`
98
-
99
- The unit of work is the business operation, which in a slice is `process()`. The `Handler` opens **one** `Connection` in `execution()` and passes it to every `Sql` call:
100
-
101
- ```java
102
- @ApplicationScoped
103
- public class CreatePartyIndividualHandler {
104
-
105
- @Inject DataSource dataSource;
106
- @Inject CreatePartyIndividualSql sql;
107
-
108
- @Transactional // the ONLY @Transactional in the slice
109
- public CreatePartyIndividualResponseDto process(CreatePartyIndividualRequestDto request) {
110
- validate(request);
111
- return getResult(execution(request));
112
- }
113
-
114
- private UUID execution(CreatePartyIndividualRequestDto request) {
115
- try (Connection conn = dataSource.getConnection()) { // exactly one, for the whole operation
116
- UUID partyId = sql.insertParty(conn, request.getTenantId(), "Individual",
117
- "Active", request.getCreatedBy());
118
- sql.insertIndividual(conn, partyId, request.getGivenName(), request.getFamilyName());
119
- sql.insertOutboxEvent(conn, partyId, "party", EVENT_TYPE, payloadJson(partyId, request), traceparent());
120
- return partyId;
121
- } catch (SQLException e) {
122
- throw SqlStateTranslator.translate("PTY-500-001", e); // unchecked -> container rolls back
123
- }
124
- }
125
- }
126
- ```
127
-
128
- **Why the explicit `Connection` parameter (performance):** inside an active JTA transaction Agroal returns the *same* enlisted connection for every `getConnection()` call, so both styles cost one physical connection. The difference shows on **read paths that carry no `@Transactional`** — there, a per-method `ds.getConnection()` takes *N* pool leases for *N* queries, each with its own acquisition and return. Passing `conn` makes every path, transactional or not, cost exactly one lease. It also keeps session state (`statement_timeout`, prepared-statement cache, temp tables) coherent across the operation.
129
-
130
- - `@Transactional` goes on `process()` only — never on a private method (CDI interceptors don't fire on self-invocation, so the transaction would silently never open), never on the `Sql` class.
131
- - **Read-only slices skip `@Transactional` entirely.** A single `SELECT`, or several reads that tolerate a non-repeatable view, are cheaper without a JTA transaction — still one `Connection`, opened and closed in `execution()`. Add the annotation the moment there is a write, or when several reads must see one snapshot.
132
- - Runtime exceptions roll back by default; `BusinessException` extends `RuntimeException`, so throwing it rolls back — correct by construction. A checked `SQLException` would **not** roll back, which is why §8 translation is mandatory.
133
- - Programmatic control when annotations don't fit (loops with per-item commit, batch jobs) — from a `common/` bean or a `*Job`, never from inside a slice `Handler`:
134
-
135
- ```java
136
- QuarkusTransaction.requiringNew().timeout(30).run(() -> { ...sql calls... });
137
- ```
138
-
139
- - Never call `con.commit()`/`rollback()`/`setAutoCommit()` anywhere. The core skill ships a CI `grep` gate for exactly these three tokens.
140
-
141
- ## 4. Updates, inserts, upserts, generated keys
142
-
143
- ```java
144
- private static final String INSERT_PARTY = """
145
- INSERT INTO customer.party (tenant_id, party_type, status, created_by, updated_by)
146
- VALUES (?, ?, ?, ?, ?)
147
- RETURNING id
148
- """;
149
-
150
- public UUID insertParty(Connection conn, String tenantId, String partyType,
151
- String status, String createdBy) throws SQLException {
152
- try (PreparedStatement ps = conn.prepareStatement(INSERT_PARTY)) {
153
- ps.setString(1, tenantId);
154
- ps.setString(2, partyType);
155
- ps.setString(3, status);
156
- ps.setString(4, createdBy);
157
- ps.setString(5, createdBy);
158
- try (ResultSet rs = ps.executeQuery()) {
159
- rs.next();
160
- return rs.getObject("id", UUID.class);
161
- }
162
- }
163
- }
164
- ```
165
-
166
- - **Generated ids**: when the PK has a database default (`id uuid DEFAULT customer.uuidv7() NOT NULL`), exclude `id` from the column list and never bind it. Read it back with `RETURNING id` + `executeQuery()` — preferred over `getGeneratedKeys()`.
167
- - Check `executeUpdate()` counts — 0 rows on an expected UPDATE is a bug or a concurrency signal, not a success. The `Handler` decides what that means; `Sql` just returns the count.
168
- - Optimistic locking: `version` column, `UPDATE ... WHERE id = ? AND version = ?`; 0 rows → the `Handler` throws `StaleVersionException` (code-carrying, in `common/exception`) with the slice's conflict code → 409 via its dedicated mapper (see quarkus-error-handling-i18n skill). It is not SQLState-derived — no `SQLException` occurs, so `SqlStateTranslator` never sees it; the `Handler` checks the update count. Do NOT name it `ConcurrentModificationException` — it shadows `java.util.ConcurrentModificationException` and an accidental import turns the 409 mapping into a 500.
169
- - Upserts: `ON CONFLICT ... DO UPDATE` explicitly; never SELECT-then-INSERT races. A unique-violation (`23505`) surfacing as a `SQLException` is translated by the `Handler` (§8), not swallowed here.
170
-
171
- ## 5. Batches, pagination, jsonb
172
-
173
- **Batch** (bulk inserts/updates — outbox relays, imports):
174
-
175
- ```java
176
- public int[] insertPartiesBatch(Connection conn, List<PartyDto> items) throws SQLException {
177
- try (PreparedStatement ps = conn.prepareStatement(INSERT_PARTY)) {
178
- for (PartyDto item : items) {
179
- bind(ps, item);
180
- ps.addBatch();
181
- }
182
- return ps.executeBatch(); // add ?reWriteBatchedInserts=true to the JDBC URL (Postgres)
183
- }
184
- }
185
- ```
186
-
187
- Chunk batches (500–1000) inside long jobs; combine with `QuarkusTransaction` per chunk, driven from a `*Job` in `common/`, not from a slice `Handler`.
188
-
189
- **Pagination**: keyset over OFFSET for anything user-facing/deep:
190
-
191
- ```sql
192
- SELECT ... FROM app_user WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT ?
193
- ```
194
-
195
- OFFSET/LIMIT acceptable for TMF `offset/limit` list operations with bounded depth; when aggregations are involved, paginate FIRST in a CTE, then join/aggregate over the page — never aggregate the full set and paginate last.
196
-
197
- **jsonb** (audit `context`, outbox `payload` — see observability/kafka skills):
198
-
199
- ```java
200
- var pgo = new org.postgresql.util.PGobject();
201
- pgo.setType("jsonb");
202
- pgo.setValue(json); // serialized with Jackson/JsonObject, never string-built
203
- ps.setObject(5, pgo);
204
- ```
205
-
206
- Read side: `rs.getString("context")` then parse. Index jsonb lookups you actually query (`(context->>'trace_id')`).
207
-
208
- ## 6. A tiny helper is allowed; a framework is not
209
-
210
- To kill boilerplate, ONE small internal helper class (~50 lines) per service or shared lib is the sanctioned maximum:
211
-
212
- ```java
213
- public final class Jdbc { // common/util — the one sanctioned exception to the banned-suffix rule
214
- @FunctionalInterface public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; }
215
-
216
- public static <T> Optional<T> queryOne(Connection conn, String sql, RowMapper<T> m, Object... params) throws SQLException { ... }
217
- public static <T> List<T> queryList(Connection conn, String sql, RowMapper<T> m, Object... params) throws SQLException { ... }
218
- public static int update(Connection conn, String sql, Object... params) throws SQLException { ... }
219
- }
220
- ```
221
-
222
- Note the signatures take `Connection`, not `DataSource` — the helper must not open connections either, or it defeats the one-lease-per-operation rule of §3.
223
-
224
- - Pure delegation to the try-with-resources pattern above; no reflection, no annotations, no SQL generation. If someone proposes adding criteria builders or entity mapping to it, that's an ORM growing back — reject.
225
- - The class is named `Jdbc` — a namespace, not `JdbcUtils`/`JdbcHelper`. The banned-suffix ArchUnit rule (see quarkus-hexagonal-core skill) exists precisely to stop this class from becoming a junk drawer.
226
- - jOOQ (code-gen, type-safe SQL) MAY be evaluated as an alternative via formal ADR; MyBatis/Hibernate remain excluded.
227
-
228
- ## 7. Blocking model: worker threads or virtual threads (and when reactive)
229
-
230
- JDBC blocks. Never run it on the event loop:
231
-
232
- - Quarkus REST resources are blocking by default when returning plain types — fine as-is.
233
- - Reactive signatures (`Uni`), Kafka consumers, gRPC: mark `@Blocking` or hop to a worker pool (see grpc/kafka skills).
234
- - **Virtual threads**: `@RunOnVirtualThread` on JDBC-heavy endpoints is the modern default for high-concurrency blocking work (Java 25 baseline; synchronized-block pinning is fixed since JDK 24) — cheap threads, same simple code. Caveat: keep pool `max-size` as the real ceiling; virtual threads make it easy to pile up on `acquisition-timeout`.
235
- - **Reactive SQL client** (`quarkus-reactive-pg-client`, Vert.x pool — NOT Agroal): only for measured hot paths (extreme fan-in, streaming thousands of rows). It's a different programming model and a second pool to size; adopting it in a service requires an ADR. Do not mix both models in the same repository class.
236
-
237
- ## 8. SQLException translation
238
-
239
- `Sql` methods propagate `SQLException`; the `Handler` catches it once, in `execution()`, and translates it through `SqlStateTranslator` (`common/error`) so nothing checked ever escapes `process()` — a checked exception would not trigger the container rollback:
240
-
241
- ```java
242
- } catch (SQLException e) {
243
- throw SqlStateTranslator.translate("PTY-500-001", e); // default code if the state is not mapped
244
- }
245
- ```
246
-
247
- | SQLState | Meaning | Resulting exception (all unchecked) |
248
- |---|---|---|
249
- | `23505` | unique violation | `BusinessException` with the slice's conflict code (→ 409) |
250
- | `23503` | FK violation | `BusinessException` with the slice's integrity code (→ 409/422) |
251
- | `40001` / `40P01` | serialization failure / deadlock | retryable `TransientPersistenceException` |
252
- | `57014` | statement timeout/cancel | `PersistenceTimeoutException` |
253
- | other | infrastructure failure | `PersistenceException(code, e)` (→ 500) |
254
-
255
- Every resulting code is registered in `ErrorCatalog` and present in all locale bundles (see quarkus-error-handling-i18n skill). The client sees a localized message; the `SQLException` stays in the logs.
256
-
257
- Set `statement_timeout` (session or per-datasource via `quarkus.datasource.jdbc.additional-jdbc-properties.options=-c statement_timeout=5000`) so runaway queries fail fast instead of holding pool connections.
258
-
259
- ## 9. Testing
260
-
261
- - `<Slice>Sql` tests: `@QuarkusTest` + Dev Services (Testcontainers Postgres starts automatically — no config). Real SQL against real Postgres; never H2 (dialect lies). Obtain a `Connection` in the test and pass it in, exactly as the `Handler` does.
262
- - `<Slice>Handler` tests are pure Mockito with a mocked `Sql` — no database (see quarkus-hexagonal-core skill).
263
- - Flyway migrations run at test start (`quarkus.flyway.migrate-at-start=true`) — tests validate DDL and queries together.
264
- - Native verification: `@QuarkusIntegrationTest` re-runs the same tests against the binary.
265
-
266
- ## Checklist for a new `Sql` method
267
-
268
- 0. Names follow the canonical table above (`<Slice>Sql` class, imperative method name, `UPPER_SNAKE` SQL constant).
269
- 1. First parameter is `Connection conn`; the method declares `throws SQLException` and opens no connection of its own.
270
- 2. SQL text block constant, schema-qualified, reserved words quoted, placeholders only; whitelist for any dynamic fragment.
271
- 3. try-with-resources on statement/resultset (the caller owns the connection).
272
- 4. No `commit`/`rollback`/`setAutoCommit`; the transaction boundary is `Handler.process()`.
273
- 5. `executeUpdate()` count returned or checked; `SQLException` propagated for the `Handler` to translate, never swallowed.
274
- 6. Generated ids via `RETURNING`; jsonb via `PGobject`; batch + chunking for bulk; keyset pagination if deep.
275
- 7. Rows mapped by hand into the slice's `*Dto`; Dev Services test written.