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.getSimpleNamePostgresDialect$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
| Feature | PostgreSQL | MySQL | SQLite | Spark |
|---|---|---|---|---|
| RETURNING clause | Yes | No | Yes | No |
| JSON operations | Yes | Yes | No | No |
| Array types | Yes | No | No | No |
| UPSERT | Yes | No | No | No |
| IF NOT EXISTS (indexes) | Yes | No | Yes | Yes |
| Window functions | Yes | Yes | Yes | Yes |
| CTEs | Yes | Yes | Yes | Yes |
Schema.verify | Yes | Yes | Yes | No |
| Adapter module | saferis-postgres-jdbc, saferis-postgres-node | saferis-mysql-jdbc | saferis-sqlite-jdbc | None: 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.
| SqlType | PostgreSQL | MySQL | SQLite | H2 |
|---|---|---|---|---|
| Bool | boolean | boolean | boolean | boolean |
| SmallInt | smallint | smallint | integer | smallint |
| Integer | integer | int | integer | integer |
| BigInt | bigint | bigint | integer | bigint |
| Real | real | float | real | real |
| DoublePrecision | double precision | double | double | double precision |
| Numeric | numeric | decimal(65, 30) | numeric | numeric(100000, 50000) |
| VarChar | varchar(255) | varchar(255) | varchar(255) | varchar(255) |
| Text | text | longtext | text | character large object |
| Binary | bytea | blob | blob | varbinary |
| Date | date | date | date | date |
| Time | time | time(6) | time | time(6) |
| TimeTz | timetz | varchar(32) | timetz | time(6) with time zone |
| Timestamp | timestamp | datetime(6) | timestamp | timestamp(6) |
| TimestampTz | timestamptz | timestamp(6) | timestamptz | timestamp(6) with time zone |
| Json | jsonb | json | json | json |
| Uuid | uuid | char(36) | uuid | uuid |
| Array(element) | element[] | json | text | element 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
| Database | Syntax |
|---|---|
| PostgreSQL | GENERATED ALWAYS AS IDENTITY |
| MySQL | AUTO_INCREMENT |
| SQLite | AUTOINCREMENT |
| H2 | GENERATED BY DEFAULT AS IDENTITY |
| Spark | no 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.