Injection is a type error

A query parameter is not SQL text. Concatenating a value into the statement is rejected by the compiler, not caught by a sanitizer.

The schema

Getting Started creates getting_started_quick_users (id, name, email). Foreign Key Support creates fk_users and fk_orders: one user, and zero or more orders whose userId points at that user. DDL's ddl_customers is the same kind of row. The figure is the foreign-key pair those pages build.

placesfk_usersPK int idstring namefk_ordersPK int idFK int userIddecimal amount

RETURNING

There is no statement type that has RETURNING and a second type that lacks the method. UpdateReady.build returns a SqlFragment with no RETURNING clause. UpdateReady.returningAs returns a ReturningQuery. Both methods are on that type. returningAs asks for Dialect & ReturningSupport. PostgresDialect is a Dialect and a ReturningSupport. MySQLDialect is only a Dialect. Under import saferis.mysql.given, summon[Dialect] is MySQLDialect, and the compiler refuses to treat it as ReturningSupport. returningAs still compiles in that file: Dialect.defaultDialect is a PostgresDialect given, and the intersection search finds it.

DialectPostgresDialect
<<trait>>ReturningSupportPostgresDialect
DialectMySQLDialect
returningAsUpdateReady~A~+build() SqlFragment+returningAs() ReturningQueryReturningQuery~A~
import saferis.*
import saferis.mysql.given

val refused: Dialect & ReturningSupport = summon[Dialect]
Found:    (saferis.mysql.given_Dialect : saferis.Dialect)
Required: saferis.Dialect & saferis.ReturningSupport
  val refused: Dialect & ReturningSupport = summon[Dialect]

Install

The coordinate is the release this site advertises.

libraryDependencies ++= Seq(
  "rocks.earlyeffect" %% "saferis" % "0.20.0",
  "rocks.earlyeffect" %% "saferis-postgres-jdbc" % "0.20.0",
)

sql

sql is an inline extension on StringContext. Naming it in a cite expands the macro, and a call is not a definition, so the panel is SqlFragment.interpolate: the method that expansion calls, with the literal parts and the placeholders kept apart. The footer is that definition.

saferis.SqlFragment.interpolate
private[saferis] def interpolate(parts: Seq[String], holders: Seq[Placeholder]): SqlFragment =
    // `StringContext.parts` still contains Scala escapes. `StringContext.s` used to decode them.
    val decoded                   = parts.map(StringContext.processEscapes)
    val b                         = Chunk.newBuilder[SqlPiece]
    val issues                    = List.newBuilder[FragmentIssue]
    var timeout: Option[Duration] = None
    var i                         = 0
    while i < holders.length do
      if i < decoded.length then b += SqlPiece.Text(SqlText(decoded(i)))
      b ++= holders(i).rawPieces
      issues ++= holders(i).issues
      timeout = timeout.orElse(holders(i).timeout)
      i += 1
    if i < decoded.length then b += SqlPiece.Text(SqlText(decoded(i)))
    new SqlFragment(SqlPieces.merge(b.result()), issues.result(), timeout)
  end interpolate

View source