Skip to content

Migrate database access from Magnum to parlance#2938

Open
adamw wants to merge 8 commits into
masterfrom
migrate-magnum-to-parlance
Open

Migrate database access from Magnum to parlance#2938
adamw wants to merge 8 commits into
masterfrom
migrate-magnum-to-parlance

Conversation

@adamw
Copy link
Copy Markdown
Member

@adamw adamw commented May 29, 2026

Summary

Migrates the backend's database access layer from Magnum to parlance, a Scala 3 ORM under active development that is inspired by Magnum but is a substantially redesigned (Active-Record-style) library — not a drop-in fork.

Dependency change

Group Artifact Version
Old com.augustnagro magnum 1.3.1
New ma.chinespirit parlance 0.1.0

Resolves from Maven Central as ma/chinespirit/parlance_3/0.1.0. parlance publishes a single Scala-3 artifact (parlance_3, built with 3.8.2); it is binary/TASTy-forward-compatible and compiles cleanly under this project's Scala 3.8.3.

API mapping applied

Magnum parlance Notes
import com.augustnagro.magnum.* import ma.chinespirit.parlance.* package rename (selective imports; avoid parlance's Id, which collides with util.Strings.Id)
DbCodec, .biMap, summon[DbCodec[OffsetDateTime]] same unchanged; DbCodec.StringCodec still available
custom given DbCodec[Instant] kept parlance ships its own InstantCodec, but the imported given takes precedence — no ambiguity
Transactor(dataSource =, sqlLogger =) Transactor[Postgres](Postgres, ds, SqlLogger.logSlowQueries(...)) DB type is a required, explicitly-pinned first arg
top-level transact(xa)(f) / connect(ds)(f) instance methods xa.transact(f) / xa.connect(f) connect needs a Transactor, not a raw DataSource
DbTx, DbTx ?=> DbTx[Postgres], DbTx[Postgres] ?=> context type is parameterized by DB type (DbCon/DbTx)
@Table(PostgresDbType, SqlNameMapper.CamelToSnakeCase) @Table(SqlNameMapper.CamelToSnakeCase) + derives EntityMeta DB type moved to the Transactor; PostgresDbTypePostgres
@SqlName, SqlNameMapper.CamelToSnakeCase, TableInfo[EC,E,ID] same column/table refs in sql"…" render identically
Repo[EC,E,ID] Repo[EC,E,ID]() open class; needs ()
repo.insert(e) repo.rawInsert(e) no insert method (export rawInsert as insert in UserModel)
findById / findAll / count / deleteById / deleteAllById same deleteAllById returns BatchUpdateResult (still .discarded)
Spec[E].where(sql"…") / .limit(n) + findAll(spec) QueryBuilder.from[E].where(sql"…".unsafeAsWhere).first() / .limit(n).run() Spec does not exist in parlance

Files touched

  • Build: build.sbt (dependency + JUL/logging comments)
  • Infra: infrastructure/DB.scala, infrastructure/Magnum.scala → renamed to infrastructure/Codecs.scala
  • Models: user/UserModel.scala, security/ApiKeyModel.scala, passwordreset/PasswordResetCodeModel.scala, email/EmailModel.scala
  • Services/auth: user/UserService.scala, email/EmailService.scala (incl. EmailScheduler trait), security/Auth.scala (incl. AuthTokenOps trait), security/ApiKeyService.scala, passwordreset/PasswordResetService.scala
  • App: Main.scala (logging comment)
  • Docs: docs/stack.md, docs/devtips.md; plus parlance-migration-notes.md (scratch mapping for reviewers)

Tests

  • sbt backend/compile: green.
  • sbt backend/test: the 9 non-DB tests pass. The DB-backed suites (UserApiTest, PasswordResetApiTest) require Docker (Testcontainers-based otj-pg-embedded), which was unavailable in the migration environment, so they could not run there.
  • To compensate, the full DB behaviour was verified end-to-end against a real PostgreSQL using the project's actual schema: codec round-trips (Id/Hashed/LowerCased/Instant), snake_case + @SqlName column mapping, transactEither rollback-on-Left / commit-on-Right, QueryBuilder.limit, findBy (incl. OR), raw UPDATE/DELETE, and the slow-query logging path — all pass, with SQL rendered identically to Magnum.

Behavioural notes & caveats

  • Logging backend differs. Magnum and parlance both log via java.lang.System.Logger (not JUL). parlance's slow-query logs are routed to SLF4J by the existing slf4j-jdk-platform-logging runtime dependency, not by the jul-to-slf4j SLF4JBridgeHandler (which remains, but only serves OTEL). Comments in Main.scala/build.sbt were corrected accordingly. SqlLogger.logSlowQueries(200.millis) behaviour is preserved.
  • Instant precision truncates to microseconds (Postgres TIMESTAMPTZ) — identical to the pre-migration Magnum behaviour.
  • API gaps in parlance vs Magnum. Spec and PostgresDbType do not exist; @Table takes only a name mapper; entities must derives EntityMeta; the context type is parameterized (DbTx[Postgres]). parlance 0.1.0 is an early release of a much larger, Active-Record-oriented library — pin the exact version.

@adamw adamw force-pushed the migrate-magnum-to-parlance branch from b52ff8e to b56ef2b Compare May 29, 2026 09:08

def migrate(): Unit = if config.migrateOnStart then flyway.migrate().discard
def testConnection(ds: DataSource): Unit = connect(ds)(sql"SELECT 1".query[Int].run()).discard
def testConnection(ds: DataSource): Unit = Transactor[Postgres](Postgres, ds).connect(sql"SELECT 1".query[Int].run()).discard
Copy link
Copy Markdown

@aikido-pr-checks aikido-pr-checks Bot May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transactor[Postgres](Postgres, ds).connect(sql"SELECT 1".query[Int].run()).discard combines transactor construction, nested query execution and an IO connect/discard on one line; extract parts (build transactor, prepare query, then connect) to improve readability.

Show fix
Suggested change
def testConnection(ds: DataSource): Unit = Transactor[Postgres](Postgres, ds).connect(sql"SELECT 1".query[Int].run()).discard
def testConnection(ds: DataSource): Unit =
val transactor = Transactor[Postgres](Postgres, ds)
val query = sql"SELECT 1".query[Int].run()
transactor.connect(query).discard
Details

✨ AI Reasoning
​The testConnection helper was changed to construct a Transactor and on a single line call connect with a nested query expression and then discard the result. This packs transactor construction, a nested sql.query.run call, and an IO-producing connect/discard sequence into one line, increasing cognitive load when reasoning about side effects and resource usage.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Introduce `infrastructure.Tx = DbTx[Postgres]` so call sites stop
repeating the database type parameter, and rewrite the UserModel
lookups to use parlance's typed column DSL (`_.col === value`) instead
of the `unsafeAsWhere` raw-SQL escape hatch, gaining compile-time
column checking.

Behaviour-preserving: column names and bound parameters are unchanged.
Backend compiles (incl. tests); DB-backed tests require Docker and were
not run in this environment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant