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