cc-codeconductor 0.2.1

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 (93) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +319 -0
  3. package/dist/index.js +12741 -0
  4. package/package.json +51 -0
  5. package/policy.yml +124 -0
  6. package/presets/claude/CLAUDE.md +598 -0
  7. package/presets/claude/commands/cc/feature.md +115 -0
  8. package/presets/claude/commands/cc/fix.md +121 -0
  9. package/presets/claude/commands/cc/refactor.md +148 -0
  10. package/presets/claude/commands/cc/review.md +126 -0
  11. package/presets/claude/commands/cc/tdd-cycle.md +226 -0
  12. package/presets/claude/commands/cc/test-plan.md +138 -0
  13. package/presets/claude/settings.json +37 -0
  14. package/presets/claude/skills/api-versioning/SKILL.md +389 -0
  15. package/presets/claude/skills/django-orm/SKILL.md +455 -0
  16. package/presets/claude/skills/django-testing/SKILL.md +409 -0
  17. package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
  18. package/presets/claude/skills/python/SKILL.md +606 -0
  19. package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
  20. package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
  21. package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
  22. package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
  23. package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
  24. package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
  25. package/presets/codex/AGENTS.md +883 -0
  26. package/presets/codex/README.md +102 -0
  27. package/presets/codex/skills/api-versioning/SKILL.md +389 -0
  28. package/presets/codex/skills/django-orm/SKILL.md +455 -0
  29. package/presets/codex/skills/django-testing/SKILL.md +409 -0
  30. package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
  31. package/presets/codex/skills/python/SKILL.md +606 -0
  32. package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
  33. package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
  34. package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
  35. package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
  36. package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
  37. package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
  38. package/presets/opencode/README.md +191 -0
  39. package/presets/opencode/agents/architect.md +133 -0
  40. package/presets/opencode/agents/docs.md +113 -0
  41. package/presets/opencode/agents/implementer.md +116 -0
  42. package/presets/opencode/agents/orchestrator.md +248 -0
  43. package/presets/opencode/agents/repo-explorer.md +119 -0
  44. package/presets/opencode/agents/reviewer.md +130 -0
  45. package/presets/opencode/agents/task-coach.md +103 -0
  46. package/presets/opencode/agents/tester.md +269 -0
  47. package/presets/opencode/commands/cc-feature.md +114 -0
  48. package/presets/opencode/commands/cc-fix.md +123 -0
  49. package/presets/opencode/commands/cc-refactor.md +148 -0
  50. package/presets/opencode/commands/cc-review.md +141 -0
  51. package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
  52. package/presets/opencode/commands/cc-test-plan.md +144 -0
  53. package/presets/opencode/opencode.jsonc +89 -0
  54. package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
  55. package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
  56. package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
  57. package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
  58. package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
  59. package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
  60. package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
  61. package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
  62. package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
  63. package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
  64. package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
  65. package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
  66. package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
  67. package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
  68. package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
  69. package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
  70. package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
  71. package/presets/opencode/skills/astro/SKILL.md +313 -0
  72. package/presets/opencode/skills/code-review/SKILL.md +202 -0
  73. package/presets/opencode/skills/django-orm/SKILL.md +455 -0
  74. package/presets/opencode/skills/django-testing/SKILL.md +408 -0
  75. package/presets/opencode/skills/django-uv/SKILL.md +400 -0
  76. package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
  77. package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
  78. package/presets/opencode/skills/python/SKILL.md +605 -0
  79. package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
  80. package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
  81. package/presets/opencode/skills/security/SKILL.md +376 -0
  82. package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
  83. package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
  84. package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
  85. package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
  86. package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
  87. package/src/presets/council/council.yml +59 -0
  88. package/src/presets/manifests/claude.yml +23 -0
  89. package/src/presets/manifests/codex.yml +12 -0
  90. package/src/presets/manifests/opencode.yml +18 -0
  91. package/src/presets/models/claude.yml +37 -0
  92. package/src/presets/models/codex.yml +38 -0
  93. package/src/presets/models/opencode.yml +37 -0
@@ -0,0 +1,617 @@
1
+ ---
2
+ id: jpa-postgres
3
+ version: 1.0.0
4
+ name: JPA + PostgreSQL
5
+ description:
6
+ Provides expert knowledge of JPA entity design, relationship mapping, Flyway
7
+ migrations, and PostgreSQL-specific optimizations.
8
+
9
+ compatibility:
10
+ tools: [claude, codex, opencode]
11
+ stacks:
12
+ languages: [kotlin, java]
13
+ frameworks: [spring-boot, spring-data-jpa, hibernate]
14
+ databases: [postgresql]
15
+
16
+ risk:
17
+ level: high
18
+ can_execute_shell: false
19
+ can_modify_files: true
20
+ requires_network: false
21
+
22
+ inputs:
23
+ - source_files
24
+ - migration scripts
25
+ - entity classes
26
+ - repository interfaces
27
+
28
+ outputs:
29
+ - JPA entity classes
30
+ - repository interfaces
31
+ - Flyway migration scripts
32
+ - JPQL and native queries
33
+ - integration test classes
34
+
35
+ quality:
36
+ reviewed_by: codeconductor-core
37
+ version: 0.1.0
38
+ ---
39
+ # JPA + PostgreSQL
40
+
41
+ ## Entity Design
42
+
43
+ ### Primary Keys
44
+
45
+ Use UUIDs. Do not use auto-increment integers as public-facing identifiers.
46
+
47
+ ```kotlin
48
+ @Entity
49
+ @Table(name = "users")
50
+ class User(
51
+ @Id
52
+ @GeneratedValue(strategy = GenerationType.UUID)
53
+ val id: UUID = UUID.randomUUID(),
54
+
55
+ @Column(name = "email", nullable = false, unique = true, length = 255)
56
+ var email: String,
57
+
58
+ @Column(name = "name", nullable = false, length = 100)
59
+ var name: String
60
+ )
61
+ ```
62
+
63
+ UUID generation strategy `GenerationType.UUID` is available in Hibernate 6+
64
+ (Spring Boot 3+). For earlier versions, use `@UuidGenerator` from Hibernate or
65
+ generate manually.
66
+
67
+ ### Auditing
68
+
69
+ Enable automatic timestamp management with Spring Data auditing.
70
+
71
+ ```kotlin
72
+ // Enable in main application class or a @Configuration class
73
+ @EnableJpaAuditing
74
+ @SpringBootApplication
75
+ class Application
76
+
77
+ // Base class for auditable entities
78
+ @MappedSuperclass
79
+ @EntityListeners(AuditingEntityListener::class)
80
+ abstract class AuditableEntity {
81
+
82
+ @Column(name = "created_at", nullable = false, updatable = false)
83
+ @CreatedDate
84
+ lateinit var createdAt: Instant
85
+
86
+ @Column(name = "updated_at", nullable = false)
87
+ @LastModifiedDate
88
+ lateinit var updatedAt: Instant
89
+ }
90
+
91
+ // Entity extends the base class
92
+ @Entity
93
+ @Table(name = "users")
94
+ class User(
95
+ @Id @GeneratedValue(strategy = GenerationType.UUID)
96
+ val id: UUID = UUID.randomUUID(),
97
+
98
+ @Column(name = "email", nullable = false, unique = true)
99
+ var email: String,
100
+
101
+ var name: String
102
+ ) : AuditableEntity()
103
+ ```
104
+
105
+ ### Soft Delete
106
+
107
+ Do not delete rows — mark them as deleted and filter them transparently.
108
+
109
+ ```kotlin
110
+ @Entity
111
+ @Table(name = "users")
112
+ @SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
113
+ @FilterDef(name = "deletedFilter", parameters = [ParamDef(name = "isDeleted", type = Boolean::class)])
114
+ @Filter(name = "deletedFilter", condition = "deleted_at IS NULL")
115
+ class User(
116
+ @Id @GeneratedValue(strategy = GenerationType.UUID)
117
+ val id: UUID = UUID.randomUUID(),
118
+
119
+ @Column(name = "email", nullable = false, unique = true)
120
+ var email: String,
121
+
122
+ @Column(name = "deleted_at")
123
+ var deletedAt: Instant? = null
124
+ )
125
+ ```
126
+
127
+ Alternative using `@Where` (simpler, but applies globally without ability to
128
+ disable):
129
+
130
+ ```kotlin
131
+ @Entity
132
+ @Table(name = "users")
133
+ @SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
134
+ @Where(clause = "deleted_at IS NULL")
135
+ class User(...)
136
+ ```
137
+
138
+ ### Column Annotations
139
+
140
+ Always be explicit. Never rely on Hibernate defaults.
141
+
142
+ ```kotlin
143
+ @Column(
144
+ name = "email", // explicit column name
145
+ nullable = false, // maps to NOT NULL constraint
146
+ unique = true, // maps to UNIQUE constraint
147
+ length = 255 // VARCHAR(255) — ignored for TEXT type
148
+ )
149
+ var email: String
150
+ ```
151
+
152
+ For PostgreSQL `TEXT` type (unbounded), use `columnDefinition`:
153
+
154
+ ```kotlin
155
+ @Column(name = "description", nullable = false, columnDefinition = "TEXT")
156
+ var description: String
157
+ ```
158
+
159
+ ### Indexes
160
+
161
+ Define indexes on the entity, not in migration scripts, for searchable fields.
162
+
163
+ ```kotlin
164
+ @Entity
165
+ @Table(
166
+ name = "users",
167
+ indexes = [
168
+ Index(name = "idx_users_email", columnList = "email", unique = true),
169
+ Index(name = "idx_users_created_at", columnList = "created_at")
170
+ ]
171
+ )
172
+ class User(...)
173
+ ```
174
+
175
+ For partial indexes (e.g., soft-delete), use a migration script — JPA cannot
176
+ express partial indexes.
177
+
178
+ ## Relationship Mapping
179
+
180
+ ### One-to-Many
181
+
182
+ ```kotlin
183
+ @Entity
184
+ @Table(name = "orders")
185
+ class Order(
186
+ @Id @GeneratedValue(strategy = GenerationType.UUID)
187
+ val id: UUID = UUID.randomUUID(),
188
+
189
+ @OneToMany(
190
+ mappedBy = "order",
191
+ cascade = [CascadeType.ALL],
192
+ fetch = FetchType.LAZY, // ALWAYS lazy by default
193
+ orphanRemoval = true
194
+ )
195
+ val items: MutableList<OrderItem> = mutableListOf()
196
+ )
197
+
198
+ @Entity
199
+ @Table(name = "order_items")
200
+ class OrderItem(
201
+ @Id @GeneratedValue(strategy = GenerationType.UUID)
202
+ val id: UUID = UUID.randomUUID(),
203
+
204
+ @ManyToOne(fetch = FetchType.LAZY)
205
+ @JoinColumn(name = "order_id", nullable = false)
206
+ val order: Order,
207
+
208
+ @Column(name = "quantity", nullable = false)
209
+ var quantity: Int
210
+ )
211
+ ```
212
+
213
+ Rules:
214
+
215
+ - `fetch = FetchType.LAZY` is the default for `@OneToMany` — make it explicit
216
+ anyway
217
+ - `CascadeType.ALL` only when the child lifecycle is completely owned by the
218
+ parent
219
+ - `orphanRemoval = true` when removing an item from the collection should delete
220
+ the row
221
+
222
+ ### Many-to-One
223
+
224
+ ```kotlin
225
+ @ManyToOne(fetch = FetchType.LAZY) // LAZY — never EAGER
226
+ @JoinColumn(name = "user_id", nullable = false)
227
+ val user: User
228
+ ```
229
+
230
+ `FetchType.EAGER` on `@ManyToOne` causes N+1 problems. Always use `LAZY` and
231
+ fetch eagerly with `JOIN FETCH` when needed.
232
+
233
+ ### Many-to-Many
234
+
235
+ Do not use `@ManyToMany` directly. Use a join entity with explicit fields.
236
+
237
+ ```kotlin
238
+ // bad — opaque join table, no room for additional fields
239
+ @ManyToMany
240
+ @JoinTable(name = "user_roles")
241
+ val roles: MutableSet<Role>
242
+
243
+ // good — explicit join entity
244
+ @Entity
245
+ @Table(name = "user_roles")
246
+ class UserRole(
247
+ @Id @GeneratedValue(strategy = GenerationType.UUID)
248
+ val id: UUID = UUID.randomUUID(),
249
+
250
+ @ManyToOne(fetch = FetchType.LAZY)
251
+ @JoinColumn(name = "user_id", nullable = false)
252
+ val user: User,
253
+
254
+ @ManyToOne(fetch = FetchType.LAZY)
255
+ @JoinColumn(name = "role_id", nullable = false)
256
+ val role: Role,
257
+
258
+ @Column(name = "assigned_at", nullable = false)
259
+ val assignedAt: Instant = Instant.now()
260
+ )
261
+ ```
262
+
263
+ The join entity approach is more flexible — you can add fields like
264
+ `assignedAt`, `assignedBy`, and query through a repository.
265
+
266
+ ## N+1 Problem
267
+
268
+ The N+1 problem occurs when loading a collection results in one query for the
269
+ parent and N additional queries for each child, where N is the number of
270
+ parents.
271
+
272
+ **Detection:**
273
+
274
+ Enable SQL logging in tests:
275
+
276
+ ```yaml
277
+ # application-test.yml
278
+ spring:
279
+ jpa:
280
+ show-sql: true
281
+ properties:
282
+ hibernate:
283
+ format_sql: true
284
+ logging:
285
+ level:
286
+ org.hibernate.SQL: DEBUG
287
+ org.hibernate.type.descriptor.sql.BasicBinder: TRACE
288
+ ```
289
+
290
+ Count the queries. If you see `SELECT * FROM order_items WHERE order_id = ?`
291
+ executed once per order, you have N+1.
292
+
293
+ **Fix with `@EntityGraph`:**
294
+
295
+ ```kotlin
296
+ @Repository
297
+ interface OrderRepository : JpaRepository<Order, UUID> {
298
+
299
+ @EntityGraph(attributePaths = ["items", "items.product"])
300
+ fun findWithItemsById(id: UUID): Optional<Order>
301
+
302
+ @EntityGraph(attributePaths = ["items"])
303
+ fun findAllWithItems(): List<Order>
304
+ }
305
+ ```
306
+
307
+ **Fix with JPQL JOIN FETCH:**
308
+
309
+ ```kotlin
310
+ @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
311
+ fun findByIdWithItems(@Param("id") id: UUID): Optional<Order>
312
+
313
+ @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
314
+ fun findByStatusWithItems(@Param("status") status: OrderStatus): List<Order>
315
+ ```
316
+
317
+ Use `DISTINCT` with `JOIN FETCH` on collections to avoid duplicate parent rows
318
+ in the result.
319
+
320
+ **Rule:** never access a lazy collection outside a transaction. This causes
321
+ `LazyInitializationException`. If a service method needs related data, fetch it
322
+ within the same transaction using `@EntityGraph` or `JOIN FETCH`.
323
+
324
+ ## Projections for Read Queries
325
+
326
+ When you only need a subset of fields, do not load the full entity. Use
327
+ projections.
328
+
329
+ **Interface projection:**
330
+
331
+ ```kotlin
332
+ interface UserSummary {
333
+ val id: UUID
334
+ val email: String
335
+ val name: String
336
+ }
337
+
338
+ @Repository
339
+ interface UserRepository : JpaRepository<User, UUID> {
340
+ fun findAllProjectedBy(): List<UserSummary>
341
+ fun findProjectedById(id: UUID): UserSummary?
342
+ }
343
+ ```
344
+
345
+ **DTO projection with constructor expression:**
346
+
347
+ ```kotlin
348
+ data class UserDto(val id: UUID, val email: String)
349
+
350
+ @Query("SELECT new com.example.user.dto.UserDto(u.id, u.email) FROM User u WHERE u.active = true")
351
+ fun findActiveUserDtos(): List<UserDto>
352
+ ```
353
+
354
+ Use projections for list endpoints and reports. Load full entities only when you
355
+ need to modify them.
356
+
357
+ ## Flyway Migrations
358
+
359
+ ### Naming Convention
360
+
361
+ ```
362
+ V{timestamp}__{description}.sql
363
+ ```
364
+
365
+ Use a timestamp, not a sequential number, to avoid conflicts in parallel
366
+ branches.
367
+
368
+ ```
369
+ V20260507120000__create_users_table.sql
370
+ V20260507120001__add_users_email_index.sql
371
+ V20260508090000__add_orders_table.sql
372
+ ```
373
+
374
+ ### Migration Rules
375
+
376
+ - Never modify a migration that has already been applied in any environment
377
+ - Each migration must be forward-only — Flyway does not support automatic
378
+ rollbacks
379
+ - DDL changes and data migrations must be in separate files
380
+ - Destructive operations (DROP COLUMN, DROP TABLE) require a multi-step process:
381
+ 1. Migration 1: deploy code that no longer uses the column
382
+ 2. Migration 2: drop the column (safe once all instances are updated)
383
+
384
+ ### Schema Migration Example
385
+
386
+ ```sql
387
+ -- V20260507120000__create_users_table.sql
388
+ CREATE TABLE users (
389
+ id UUID NOT NULL PRIMARY KEY,
390
+ email VARCHAR(255) NOT NULL,
391
+ name VARCHAR(100) NOT NULL,
392
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
393
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
394
+ deleted_at TIMESTAMPTZ
395
+ );
396
+
397
+ CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;
398
+ ```
399
+
400
+ ### Rollback Strategy
401
+
402
+ Document rollback SQL in comments at the top of each migration file:
403
+
404
+ ```sql
405
+ -- V20260507120001__add_user_role_column.sql
406
+ -- Rollback: ALTER TABLE users DROP COLUMN role;
407
+
408
+ ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'USER';
409
+ ```
410
+
411
+ Automated rollback is not used. Manual rollback is applied only in emergencies.
412
+
413
+ ## PostgreSQL Specifics
414
+
415
+ ### Column Types
416
+
417
+ | Use case | PostgreSQL type | JPA mapping |
418
+ | ------------------------------- | --------------- | ------------------------------------- |
419
+ | Short text, known max length | `VARCHAR(n)` | `@Column(length = n)` |
420
+ | Long text, no known max | `TEXT` | `@Column(columnDefinition = "TEXT")` |
421
+ | Structured semi-structured data | `JSONB` | `@Column(columnDefinition = "JSONB")` |
422
+ | Timestamps with timezone | `TIMESTAMPTZ` | `Instant` |
423
+ | UUIDs | `UUID` | `UUID` |
424
+ | Money/currency | `NUMERIC(19,4)` | `BigDecimal` |
425
+
426
+ Use `TIMESTAMPTZ` (with timezone), not `TIMESTAMP`. Store all timestamps in UTC.
427
+
428
+ ### JSONB
429
+
430
+ Use `JSONB` for semi-structured data that does not warrant its own table.
431
+
432
+ ```kotlin
433
+ @Column(name = "metadata", columnDefinition = "JSONB")
434
+ @Type(JsonType::class) // requires hypersistence-utils dependency
435
+ var metadata: Map<String, Any> = emptyMap()
436
+ ```
437
+
438
+ JSONB is indexable. JSON is not. Always use JSONB.
439
+
440
+ ### Partial Indexes
441
+
442
+ Partial indexes cannot be expressed through JPA annotations. Use a migration:
443
+
444
+ ```sql
445
+ -- For soft-deleted tables: only index active (non-deleted) rows
446
+ CREATE UNIQUE INDEX idx_users_email_active ON users (email) WHERE deleted_at IS NULL;
447
+
448
+ -- For status-filtered queries
449
+ CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'PENDING';
450
+ ```
451
+
452
+ ### Native Queries with RETURNING
453
+
454
+ Use `RETURNING` to avoid an extra `SELECT` after an `INSERT` or `UPDATE`:
455
+
456
+ ```kotlin
457
+ @Modifying
458
+ @Query(
459
+ value = "INSERT INTO audit_log (user_id, action, created_at) VALUES (:userId, :action, NOW()) RETURNING id",
460
+ nativeQuery = true
461
+ )
462
+ fun insertAndReturnId(
463
+ @Param("userId") userId: UUID,
464
+ @Param("action") action: String
465
+ ): UUID
466
+ ```
467
+
468
+ ### Complex Queries
469
+
470
+ Use native queries when JPQL cannot express what you need:
471
+
472
+ ```kotlin
473
+ @Query(
474
+ value = """
475
+ SELECT u.id, u.email, COUNT(o.id) as order_count
476
+ FROM users u
477
+ LEFT JOIN orders o ON o.user_id = u.id AND o.deleted_at IS NULL
478
+ WHERE u.deleted_at IS NULL
479
+ GROUP BY u.id, u.email
480
+ HAVING COUNT(o.id) > :minOrders
481
+ ORDER BY order_count DESC
482
+ LIMIT :limit
483
+ """,
484
+ nativeQuery = true
485
+ )
486
+ fun findActiveUsersWithMinOrders(
487
+ @Param("minOrders") minOrders: Int,
488
+ @Param("limit") limit: Int
489
+ ): List<Map<String, Any>>
490
+ ```
491
+
492
+ ## Query Performance
493
+
494
+ ### Pagination
495
+
496
+ Always paginate. Never call `findAll()` on a large table.
497
+
498
+ ```kotlin
499
+ @Repository
500
+ interface UserRepository : JpaRepository<User, UUID> {
501
+ fun findAll(pageable: Pageable): Page<UserSummary>
502
+ fun findByRole(role: UserRole, pageable: Pageable): Page<UserSummary>
503
+ }
504
+
505
+ // In service
506
+ fun list(page: Int, size: Int): Page<UserSummary> {
507
+ val pageable = PageRequest.of(page, size, Sort.by("createdAt").descending())
508
+ return userRepository.findAll(pageable)
509
+ }
510
+ ```
511
+
512
+ Use `Slice<T>` instead of `Page<T>` when you do not need the total count
513
+ (cheaper — no COUNT query).
514
+
515
+ ### QueryHints for Fetch Control
516
+
517
+ ```kotlin
518
+ @QueryHints(
519
+ QueryHint(name = HINT_FETCHGRAPH, value = "User.withOrders")
520
+ )
521
+ fun findById(id: UUID): Optional<User>
522
+ ```
523
+
524
+ Requires a named entity graph:
525
+
526
+ ```kotlin
527
+ @Entity
528
+ @NamedEntityGraph(
529
+ name = "User.withOrders",
530
+ attributeNodes = [NamedAttributeNode("orders")]
531
+ )
532
+ class User(...)
533
+ ```
534
+
535
+ ## Testing
536
+
537
+ ### Unit — @DataJpaTest with H2
538
+
539
+ Fast. No server required.
540
+
541
+ ```kotlin
542
+ @DataJpaTest
543
+ class UserRepositoryTest {
544
+
545
+ @Autowired
546
+ private lateinit var userRepository: UserRepository
547
+
548
+ @Test
549
+ fun `should find user by email`() {
550
+ val saved = userRepository.save(User(email = "test@example.com", name = "Test User"))
551
+ val found = userRepository.findByEmail("test@example.com")
552
+ assertThat(found).isNotNull
553
+ assertThat(found?.id).isEqualTo(saved.id)
554
+ }
555
+
556
+ @Test
557
+ fun `should not find deleted user by email`() {
558
+ val user = userRepository.save(User(email = "deleted@example.com", name = "Gone"))
559
+ userRepository.delete(user) // triggers @SQLDelete — sets deleted_at
560
+ val found = userRepository.findByEmail("deleted@example.com")
561
+ assertThat(found).isNull()
562
+ }
563
+ }
564
+ ```
565
+
566
+ ### Integration — Testcontainers with PostgreSQL
567
+
568
+ Real database. Use for migrations, JSONB, partial indexes, and native queries.
569
+
570
+ ```kotlin
571
+ @SpringBootTest
572
+ @Testcontainers
573
+ class UserRepositoryIntegrationTest {
574
+
575
+ companion object {
576
+ @Container
577
+ @JvmStatic
578
+ val postgres = PostgreSQLContainer<Nothing>("postgres:16").apply {
579
+ withDatabaseName("testdb")
580
+ withUsername("test")
581
+ withPassword("test")
582
+ }
583
+
584
+ @DynamicPropertySource
585
+ @JvmStatic
586
+ fun registerProperties(registry: DynamicPropertyRegistry) {
587
+ registry.add("spring.datasource.url", postgres::getJdbcUrl)
588
+ registry.add("spring.datasource.username", postgres::getUsername)
589
+ registry.add("spring.datasource.password", postgres::getPassword)
590
+ }
591
+ }
592
+
593
+ @Autowired
594
+ private lateinit var userRepository: UserRepository
595
+
596
+ @Test
597
+ fun `should enforce unique email constraint at db level`() {
598
+ userRepository.save(User(email = "dup@example.com", name = "First"))
599
+ assertThrows<DataIntegrityViolationException> {
600
+ userRepository.save(User(email = "dup@example.com", name = "Second"))
601
+ }
602
+ }
603
+ }
604
+ ```
605
+
606
+ Use Testcontainers for:
607
+
608
+ - Flyway migration validation
609
+ - PostgreSQL-specific features (JSONB, partial indexes, native queries)
610
+ - Constraint enforcement (unique, foreign key, check constraints)
611
+ - Query performance checks that differ between H2 and PostgreSQL
612
+
613
+ Use `@DataJpaTest` with H2 for:
614
+
615
+ - Derived query methods
616
+ - Simple CRUD operations
617
+ - JPQL queries with standard JPA behavior