Dialect System

Saferis supports multiple databases with compile-time type safety. Each dialect provides database-specific SQL generation and type mappings.

Available Dialects

import saferis.*

// PostgreSQL (default) - full feature support
// No additional import needed - it's the default

// MySQL:    import saferis.mysql.given
// SQLite:   import saferis.sqlite.given
// Spark SQL: import saferis.spark.given

// Switching dialect is a one-line import change; your query code adapts.
val pg = summon[Dialect]
summon[Dialect].getClass.getSimpleName
PostgresDialect$

A Dialect takes typed names and returns SqlText (ColumnType for type spellings), so a dialect you write outside Saferis says what each string is. See Core Concepts. Spark's dropTableSql and truncateTableSql quote the table name with backticks, as every other dialect does; they used to splice it unquoted.

Feature Comparison

FeaturePostgreSQLMySQLSQLiteSpark
RETURNING clauseYesNoYesNo
JSON operationsYesYesNoNo
Array typesYesNoNoNo
UPSERTYesNoNoNo
IF NOT EXISTS (indexes)YesNoYesYes
Window functionsYesYesYesYes
CTEsYesYesYesYes
Schema.verifyYesYesYesNo
Adapter modulesaferis-postgres-jdbc, saferis-postgres-nodesaferis-mysql-jdbcsaferis-sqlite-jdbcNone: SQL only

H2Dialect ships in saferis-h2-jdbc rather than in core. It claims none of the optional capabilities. See Databases for what each adapter does.

Type Mappings

The DDL each dialect renders for a column. SQLite stores by affinity but keeps the declared name, and its adapter reads that name back, so SQLite declares a name for every type.

SqlTypePostgreSQLMySQLSQLiteH2
Boolbooleanbooleanbooleanboolean
SmallIntsmallintsmallintintegersmallint
Integerintegerintintegerinteger
BigIntbigintbigintintegerbigint
Realrealfloatrealreal
DoublePrecisiondouble precisiondoubledoubledouble precision
Numericnumericdecimal(65, 30)numericnumeric(100000, 50000)
VarCharvarchar(255)varchar(255)varchar(255)varchar(255)
Texttextlongtexttextcharacter large object
Binarybyteablobblobvarbinary
Datedatedatedatedate
Timetimetime(6)timetime(6)
TimeTztimetzvarchar(32)timetztime(6) with time zone
Timestamptimestampdatetime(6)timestamptimestamp(6)
TimestampTztimestamptztimestamp(6)timestamptztimestamp(6) with time zone
Jsonjsonbjsonjsonjson
Uuiduuidchar(36)uuiduuid
Array(element)element[]jsontextelement array

MySQL's bare decimal is decimal(10,0) and drops the fraction, so Numeric asks for the widest scale. H2 2.5 stores at most 100000 numeric digits (Constants.MAX_NUMERIC_PRECISION). numeric(100000, 50000) is H2's TYPE_NUMERIC_FLOATING_POINT: 50000 digits on each side of the decimal. A decimal outside that fails when H2 binds it, instead of being rounded. MySQL has no timetz, so TimeTz is ISO-8601 text in varchar(32). SQLite declares timetz and reads that text back as TimeTz. MySQL's datetime is a local timestamp and timestamp an instant, so the two Scala types get different columns. Every SQLite integer is integer, so an integer primary key stays SQLite's rowid and autoincrements.

Auto-Increment Syntax

DatabaseSyntax
PostgreSQLGENERATED ALWAYS AS IDENTITY
MySQLAUTO_INCREMENT
SQLiteAUTOINCREMENT
H2GENERATED BY DEFAULT AS IDENTITY
Sparkno identity or primary-key clause

Dialects spell a GeneratedKey. A compound key is Identity or Plain, never IdentityPrimaryKey: the primary key is a table constraint.

For a deeper look at how capabilities are enforced at compile time, see Type-Safe Capabilities.