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.
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.
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.
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