Why
runtime/src/main/kotlin/norm/TransactionalConnectionProvider.kt implements the void and
result-returning transaction entry points as four separate private methods (executeOutermostVoid,
executeOutermostWithResult, executeNestedVoid, executeNestedWithResult, lines 95-205). The void
pair differs from the result pair only in how RollbackException is terminated: the void variants
swallow it and return; the result variants rethrow it. Everything else (rollback, savepoint release,
poisoning the parent, finally cleanup) is duplicated line for line.
Equivalence argument (checked against the current code)
Outermost, RollbackException thrown by body:
- Void (lines 100-104):
connection.rollback(), return, finally cleanupOutermost.
- Result (lines 121-126): falls into
catch (expected: Throwable), connection.rollback(), rethrow,
finally cleanupOutermost. A wrapper that catches RollbackException and returns yields the same
observable sequence: one rollback, cleanup, normal return, parent not involved.
Nested, RollbackException thrown by body:
- Void (lines 168-172):
connection.rollback(savepoint), return, finally activeTransaction.set(parent).
Parent is not poisoned.
- Result (lines 189-194):
connection.rollback(savepoint), throw RollbackException(), finally activeTransaction.set(parent). Parent is not poisoned. Wrapper swallows: identical.
Any other Throwable: both variants roll back, poison the parent (nested only), rethrow. Identical.
Normal completion: both commit/release. Identical.
Target state
@Throws(SQLException::class, IllegalStateException::class)
public fun transaction(readOnly: Boolean = true, body: TransactionScope.() -> Unit) {
try {
transactionWithResult(readOnly, body)
} catch (_: RollbackException) {
// Explicit rollback already happened inside transactionWithResult; a void transaction has
// nothing to return, so the signal ends here.
}
}
Delete executeOutermostVoid and executeNestedVoid. Rename the surviving pair to
executeOutermost and executeNested (the WithResult suffix no longer distinguishes anything).
Test design
runtime/src/test/kotlin/norm/TransactionalConnectionProviderTest.kt (335 lines) already covers
commit, exception rollback, explicit rollback, nesting, poisoning, and the read-only nesting check
for both entry points. Before changing code, confirm each of these four cases is asserted for the
void transaction() entry point specifically, and add any that is missing:
- Outermost
rollback() inside transaction { }: connection rolled back once, autoCommit
restored, connection closed, no exception escapes.
- Nested
rollback() inside transaction { transaction { rollback() } }: inner savepoint rolled
back, outer commits, outer not poisoned.
- Nested throwing inside
transaction { transaction { throw } }: inner savepoint rolled back, outer
rolled back (poisoned), exception propagates.
transaction { } returning normally commits.
These are the assertions the wrapper must keep true.
Acceptance criteria
TransactionalConnectionProvider has exactly one outermost and one nested execution path.
- The four tests above exist and pass.
./gradlew :runtime:check passes. ./gradlew :e2e-tests:test passes (it exercises
RealTransactable end to end against a live database).
Files
runtime/src/main/kotlin/norm/TransactionalConnectionProvider.kt
runtime/src/test/kotlin/norm/TransactionalConnectionProviderTest.kt
Conventions every issue inherits
- Repo root:
/Volumes/Code/3rd-party/norm. Module under change is almost always generator/.
- Style: 2-space indent, 120 columns, full words in identifiers (
parameter, not param), no
section-separator comments, @Nested classes group tests. Format with ./gradlew spotlessApply.
- TDD: tests are written first or alongside. For a behavior-preserving refactor the existing suite is
the pin; each issue says which tests must also be added.
- Golden files under
test-scenarios*/ are never hand-edited. A refactor is behavior-preserving only
if ./gradlew :gradle-plugin:generateGoldenFiles leaves git status --porcelain test-scenarios test-scenarios-frameworks empty.
- Verification for a
generator/ change: ./gradlew :generator:check :gradle-plugin:test (Docker
required). Then the golden regeneration check above.
- Commit message explains the design decision, not the diff.
- Never run
./gradlew clean or disable the configuration/build cache to "fix" a build problem.
Why
runtime/src/main/kotlin/norm/TransactionalConnectionProvider.ktimplements the void andresult-returning transaction entry points as four separate private methods (
executeOutermostVoid,executeOutermostWithResult,executeNestedVoid,executeNestedWithResult, lines 95-205). The voidpair differs from the result pair only in how
RollbackExceptionis terminated: the void variantsswallow it and return; the result variants rethrow it. Everything else (rollback, savepoint release,
poisoning the parent,
finallycleanup) is duplicated line for line.Equivalence argument (checked against the current code)
Outermost,
RollbackExceptionthrown bybody:connection.rollback(),return,finally cleanupOutermost.catch (expected: Throwable),connection.rollback(), rethrow,finally cleanupOutermost. A wrapper that catchesRollbackExceptionand returns yields the sameobservable sequence: one rollback, cleanup, normal return, parent not involved.
Nested,
RollbackExceptionthrown bybody:connection.rollback(savepoint),return,finally activeTransaction.set(parent).Parent is not poisoned.
connection.rollback(savepoint),throw RollbackException(),finally activeTransaction.set(parent). Parent is not poisoned. Wrapper swallows: identical.Any other
Throwable: both variants roll back, poison the parent (nested only), rethrow. Identical.Normal completion: both commit/release. Identical.
Target state
Delete
executeOutermostVoidandexecuteNestedVoid. Rename the surviving pair toexecuteOutermostandexecuteNested(theWithResultsuffix no longer distinguishes anything).Test design
runtime/src/test/kotlin/norm/TransactionalConnectionProviderTest.kt(335 lines) already coverscommit, exception rollback, explicit rollback, nesting, poisoning, and the read-only nesting check
for both entry points. Before changing code, confirm each of these four cases is asserted for the
void
transaction()entry point specifically, and add any that is missing:rollback()insidetransaction { }: connection rolled back once,autoCommitrestored, connection closed, no exception escapes.
rollback()insidetransaction { transaction { rollback() } }: inner savepoint rolledback, outer commits, outer not poisoned.
transaction { transaction { throw } }: inner savepoint rolled back, outerrolled back (poisoned), exception propagates.
transaction { }returning normally commits.These are the assertions the wrapper must keep true.
Acceptance criteria
TransactionalConnectionProviderhas exactly one outermost and one nested execution path../gradlew :runtime:checkpasses../gradlew :e2e-tests:testpasses (it exercisesRealTransactableend to end against a live database).Files
runtime/src/main/kotlin/norm/TransactionalConnectionProvider.ktruntime/src/test/kotlin/norm/TransactionalConnectionProviderTest.ktConventions every issue inherits
/Volumes/Code/3rd-party/norm. Module under change is almost alwaysgenerator/.parameter, notparam), nosection-separator comments,
@Nestedclasses group tests. Format with./gradlew spotlessApply.the pin; each issue says which tests must also be added.
test-scenarios*/are never hand-edited. A refactor is behavior-preserving onlyif
./gradlew :gradle-plugin:generateGoldenFilesleavesgit status --porcelain test-scenarios test-scenarios-frameworksempty.generator/change:./gradlew :generator:check :gradle-plugin:test(Dockerrequired). Then the golden regeneration check above.
./gradlew cleanor disable the configuration/build cache to "fix" a build problem.