Skip to content

transaction() delegates to transactionWithResult() #277

Description

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:

  1. Outermost rollback() inside transaction { }: connection rolled back once, autoCommit
    restored, connection closed, no exception escapes.
  2. Nested rollback() inside transaction { transaction { rollback() } }: inner savepoint rolled
    back, outer commits, outer not poisoned.
  3. Nested throwing inside transaction { transaction { throw } }: inner savepoint rolled back, outer
    rolled back (poisoned), exception propagates.
  4. transaction { } returning normally commits.

These are the assertions the wrapper must keep true.

Acceptance criteria

  1. TransactionalConnectionProvider has exactly one outermost and one nested execution path.
  2. The four tests above exist and pass.
  3. ./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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions