SQLite type support
Foundations JDBC supports SQLite via the xerial sqlite-jdbc driver. SQLite's type system is famously minimal — five storage classes (NULL, INTEGER, REAL, TEXT, BLOB) plus declared-type "affinity" hints — so this page covers a smaller surface than the other dialects.
By default, every column accepts every storage class regardless of the declared type. INSERT into an INTEGER column accepts a string, an INTEGER PRIMARY KEY accepts a float, and VARCHAR(10) accepts a 10MB string. The only exception is STRICT tables (CREATE TABLE … STRICT, since SQLite 3.37) which enforce the declared type. Use STRICT tables for any new schema you create with this library — it's the only way to make SQLite behave like every other database in the suite. See STRICT Tables.
The xerial driver writes DATE / DATETIME / TIMESTAMP columns as ISO-8601 TEXT (yyyy-MM-dd HH:mm:ss.SSS). Foundations JDBC follows that convention — SqliteTypes.date, .datetime, .instant all read and write text. If you set dateClass(INTEGER) or dateClass(REAL) on SqliteConfig, you must supply your own date/time bindings.
Storage classes & type affinity
Per SQLite docs §2–3, every value belongs to one of five storage classes (NULL, INTEGER, REAL, TEXT, BLOB) regardless of column declarations. The declared type only suggests an "affinity":
| Affinity | Triggered by declared text containing... |
|---|---|
| INTEGER | INT (matches INTEGER, BIGINT, INT8, MEDIUMINT, …) |
| TEXT | CHAR, CLOB, or TEXT (matches VARCHAR, NCHAR, CLOB, …) |
| BLOB | BLOB, or no declaration at all |
| REAL | REAL, FLOA, or DOUB |
| NUMERIC | anything else (default) |
Each SqliteTypes value below picks one canonical declared name and registers the common SQL aliases as vendor-type names so query analysis accepts equivalent declarations (e.g. BIGINT, INT2 → integer; VARCHAR(100), CLOB → text).
Integer types
| SQLite Type | Java Type | Aliases recognised by query analysis |
|---|---|---|
INTEGER | Long | int, int2, int4, int8, bigint, smallint, tinyint, mediumint, unsigned big int |
BIGINT | Long | (same set) |
INT | Integer | (same set) |
SMALLINT | Short | (same set) |
TINYINT | Byte | (same set) |
- Kotlin
- Java
- Scala
val integerType: SqliteType<Long> = SqliteTypes.integer
val bigintType: SqliteType<Long> = SqliteTypes.bigint
val intType: SqliteType<Int> = SqliteTypes.int_
val smallintType: SqliteType<Short> = SqliteTypes.smallint
val tinyintType: SqliteType<Byte> = SqliteTypes.tinyint
SqliteType<Long> integerType = SqliteTypes.integer; // INTEGER (canonical, INTEGER affinity)
SqliteType<Long> bigintType = SqliteTypes.bigint; // BIGINT alias
SqliteType<Integer> intType = SqliteTypes.int_; // INT (32-bit Java int)
SqliteType<Short> smallintType = SqliteTypes.smallint;
SqliteType<Byte> tinyintType = SqliteTypes.tinyint;
val integerType: SqliteType[Long] = SqliteTypes.integer
val bigintType: SqliteType[Long] = SqliteTypes.bigint
val intType: SqliteType[Int] = SqliteTypes.int_
val smallintType: SqliteType[Short] = SqliteTypes.smallint
val tinyintType: SqliteType[Byte] = SqliteTypes.tinyint
Boolean
| SQLite Type | Java Type | Notes |
|---|---|---|
BOOLEAN | Boolean | Stored as INTEGER 0/1; the TRUE/FALSE keywords work since SQLite 3.23.0 |
- Kotlin
- Java
- Scala
val boolType: SqliteType<Boolean> = SqliteTypes.boolean_
// SQLite has no BOOLEAN storage class — values land as INTEGER 0/1.
SqliteType<Boolean> boolType = SqliteTypes.boolean_;
val boolType: SqliteType[Boolean] = SqliteTypes.boolean_
Floating-point types
| SQLite Type | Java Type | Notes |
|---|---|---|
REAL | Double | 8-byte IEEE 754 (canonical) |
DOUBLE / DOUBLE PRECISION | Double | Aliases |
FLOAT | Float | Stored as REAL — SQLite has no 32-bit float storage class |
- Kotlin
- Java
- Scala
val realType: SqliteType<Double> = SqliteTypes.real
val doubleType: SqliteType<Double> = SqliteTypes.double_
val floatType: SqliteType<Float> = SqliteTypes.float_
SqliteType<Double> realType = SqliteTypes.real; // REAL (canonical, 8-byte IEEE 754)
SqliteType<Double> doubleType = SqliteTypes.double_;
SqliteType<Float> floatType = SqliteTypes.float_;
val realType: SqliteType[Double] = SqliteTypes.real
val doubleType: SqliteType[Double] = SqliteTypes.double_
val floatType: SqliteType[Float] = SqliteTypes.float_
Numeric / decimal
| SQLite Type | Java Type | Notes |
|---|---|---|
NUMERIC | BigDecimal | Arbitrary precision; SQLite does not enforce (p,s) |
DECIMAL(p,s) | BigDecimal | Same — (p,s) is a documentation label |
BigDecimal is bound and read as plain text — the xerial driver's setBigDecimal/getBigDecimal are unimplemented (they throw column -1 out of bounds), so the library falls back to setString + BigDecimal::toPlainString and reads via getString. Precision is preserved exactly.
- Kotlin
- Java
- Scala
val numericType: SqliteType<java.math.BigDecimal> = SqliteTypes.numeric
val decimalType: SqliteType<java.math.BigDecimal> = SqliteTypes.decimal
val precise: SqliteType<java.math.BigDecimal> = SqliteTypes.decimalOf(18, 6)
// SQLite stores BigDecimal as TEXT to preserve precision — declared (p,s) is a label only.
SqliteType<BigDecimal> numericType = SqliteTypes.numeric;
SqliteType<BigDecimal> decimalType = SqliteTypes.decimal;
SqliteType<BigDecimal> precise = SqliteTypes.decimalOf(18, 6);
val numericType: SqliteType[BigDecimal] = SqliteTypes.numeric
val decimalType: SqliteType[BigDecimal] = SqliteTypes.decimal
val precise: SqliteType[BigDecimal] = SqliteTypes.decimalOf(18, 6)
String types
| SQLite Type | Java Type | Notes |
|---|---|---|
TEXT | String | UTF-8 (canonical) |
VARCHAR(n) | String | n is a label; SQLite does not enforce length |
CHAR(n) | String | Same — label only |
CLOB | String | TEXT alias |
- Kotlin
- Java
- Scala
val textType: SqliteType<String> = SqliteTypes.text
val varcharType: SqliteType<String> = SqliteTypes.varcharOf(255)
val charType: SqliteType<String> = SqliteTypes.charOf(10)
val clobType: SqliteType<String> = SqliteTypes.clob
SqliteType<String> textType = SqliteTypes.text; // TEXT (canonical)
SqliteType<String> varcharType = SqliteTypes.varcharOf(255); // VARCHAR(255) — length is a label
SqliteType<String> charType = SqliteTypes.charOf(10);
SqliteType<String> clobType = SqliteTypes.clob;
val textType: SqliteType[String] = SqliteTypes.text
val varcharType: SqliteType[String] = SqliteTypes.varcharOf(255)
val charType: SqliteType[String] = SqliteTypes.charOf(10)
val clobType: SqliteType[String] = SqliteTypes.clob
Binary types
| SQLite Type | Java Type |
|---|---|
BLOB / BINARY / VARBINARY | byte[] |
- Kotlin
- Java
- Scala
val blobType: SqliteType<ByteArray> = SqliteTypes.blob
SqliteType<byte[]> blobType = SqliteTypes.blob;
val blobType: SqliteType[Array[Byte]] = SqliteTypes.blob
Date/time types
SQLite has no date/time storage class. Foundations JDBC writes everything as ISO-8601 TEXT, the xerial driver default. Reads parse the text back into the Java type.
| SQLite Type | Java Type | Wire format |
|---|---|---|
DATE | LocalDate | yyyy-MM-dd |
TIME | LocalTime | HH:mm[:ss[.fraction]] |
DATETIME / TIMESTAMP | LocalDateTime | yyyy-MM-dd HH:mm:ss.SSS (or T separator) |
TIMESTAMP (UTC) | Instant | yyyy-MM-ddTHH:mm:ss[.fraction]Z |
- Kotlin
- Java
- Scala
val dateType: SqliteType<java.time.LocalDate> = SqliteTypes.date
val timeType: SqliteType<java.time.LocalTime> = SqliteTypes.time
val datetimeType: SqliteType<java.time.LocalDateTime> = SqliteTypes.datetime
val timestampType: SqliteType<java.time.LocalDateTime> = SqliteTypes.timestamp
val instantType: SqliteType<java.time.Instant> = SqliteTypes.instant
// SQLite stores date/time as ISO-8601 TEXT (the xerial driver default).
SqliteType<LocalDate> dateType = SqliteTypes.date;
SqliteType<LocalTime> timeType = SqliteTypes.time;
SqliteType<LocalDateTime> datetimeType = SqliteTypes.datetime;
SqliteType<LocalDateTime> timestampType = SqliteTypes.timestamp;
SqliteType<Instant> instantType = SqliteTypes.instant; // ISO-8601 with `Z` suffix
val dateType: SqliteType[java.time.LocalDate] = SqliteTypes.date
val timeType: SqliteType[java.time.LocalTime] = SqliteTypes.time
val datetimeType: SqliteType[java.time.LocalDateTime] = SqliteTypes.datetime
val timestampType: SqliteType[java.time.LocalDateTime] = SqliteTypes.timestamp
val instantType: SqliteType[java.time.Instant] = SqliteTypes.instant
The default LocalDateTime writer formats with .SSS precision. Microseconds and nanoseconds are silently dropped on write. Instant round-trips at full nanosecond precision because Instant.toString() emits whatever precision the value carries.
UUID
| SQLite Type | Java Type | Storage |
|---|---|---|
UUID | java.util.UUID | Canonical 36-character TEXT |
- Kotlin
- Java
- Scala
val uuidType: SqliteType<java.util.UUID> = SqliteTypes.uuid
// SQLite has no UUID storage class — values are stored as canonical 36-char TEXT.
SqliteType<UUID> uuidType = SqliteTypes.uuid;
val uuidType: SqliteType[java.util.UUID] = SqliteTypes.uuid
JSON
| SQLite Type | Java Type | Notes |
|---|---|---|
JSON | Json | Stored as TEXT; use SQLite's built-in JSON1 functions (json(), json_extract(), ->, ->>) |
- Kotlin
- Java
- Scala
val jsonType: SqliteType<Json> = SqliteTypes.json
val data = Json("{\"name\": \"SQLite\"}")
// Backed by TEXT — use SQLite's built-in JSON1 functions (json(), json_extract(), ->, ->>).
SqliteType<Json> jsonType = SqliteTypes.json;
Json data = new Json("{\"name\": \"SQLite\"}");
val jsonType: SqliteType[Json] = SqliteTypes.json
val data: Json = new Json("{\"name\": \"SQLite\"}")
SQLite's JSON1 extension is built into the engine since 3.38 (Feb 2022). A binary JSONB form exists since 3.45 — it's stored as BLOB and accessed via jsonb_* functions; foundations-jdbc does not currently expose a separate type for it, so use SqliteTypes.blob if you need the raw binary form.
Enums
SQLite has no native enum type. The library encodes Java enums as TEXT — pair the column with a CHECK (col IN (...)) constraint in DDL for static enforcement.
- Kotlin
- Java
- Scala
enum class Status { PENDING, ACTIVE, COMPLETED }
// SQLite has no native enum — pair with `CHECK (col IN ('PENDING','ACTIVE','COMPLETED'))` in DDL.
val statusType: SqliteType<Status> = SqliteTypes.ofEnum<Status>()
// Define your Java enum
public enum Status {
PENDING,
ACTIVE,
COMPLETED
}
// SQLite has no native enum — pair with `CHECK (col IN ('PENDING','ACTIVE','COMPLETED'))` in DDL.
SqliteType<Status> statusType = SqliteTypes.ofEnum(Status.values());
// Plain Scala 3 enum
enum Status:
case PENDING, ACTIVE, COMPLETED
// SQLite has no native enum — pair with a CHECK (col IN (...)) constraint in DDL.
val statusType: SqliteType[Status] = SqliteTypes.ofEnum(Status.values)
Nullable types
Any type can be made nullable via .opt():
- Kotlin
- Java
- Scala
val notNull: SqliteType<Long> = SqliteTypes.integer
val nullable: SqliteType<Long?> = SqliteTypes.integer.opt()
SqliteType<Long> notNull = SqliteTypes.integer;
SqliteType<Optional<Long>> nullable = SqliteTypes.integer.opt();
val notNull: SqliteType[Long] = SqliteTypes.integer
val nullable: SqliteType[Option[Long]] = SqliteTypes.integer.opt
Custom domain types
Wrap base types with custom Java types using transform:
- Kotlin
- Java
- Scala
data class ProductId(val value: Long)
val productIdType: SqliteType<ProductId> =
SqliteTypes.integer.transform(::ProductId, ProductId::value)
// Wrapper type
public record ProductId(Long value) {}
// Build a SqliteType from `integer` via a bidirectional transform
SqliteType<ProductId> productIdType =
SqliteTypes.integer.transform(ProductId::new, ProductId::value);
case class ProductId(value: Long)
val productIdType: SqliteType[ProductId] =
SqliteTypes.integer.transform(ProductId.apply, _.value)
What SQLite doesn't have
For parity with the other dialect pages, here's what's deliberately absent:
| Feature | SQLite equivalent |
|---|---|
Arrays (int[], text[]) | None — use JSON arrays or junction tables |
| Composite/STRUCT/OBJECT types | None — use JSON or denormalised columns |
| Maps | None — use JSON objects |
| Ranges | None |
| Intervals | None — store as INTEGER seconds or TEXT ISO-8601 |
Network types (inet, cidr, macaddr) | None — use TEXT |
| Geometry/GIS | Requires the SpatiaLite extension |
Stored procedures / CallableStatement | None — SqliteType.outParam() always returns Optional.empty() |
COPY / streaming bulk insert | None — use prepared INSERT in a transaction with addBatch() |
| Unsigned integers | None — INTEGER is signed 64-bit |
Connection configuration
Build a config with SqliteConfig.builder("path/to/file.db"), SqliteConfig.inMemory() (one connection only — each getConnection(":memory:") opens an independent database), or SqliteConfig.sharedInMemory() for file::memory:?cache=shared.
Common options:
.foreignKeys(true)— SQLite has foreign-key enforcement off by default; turn it on per connection.journalMode("WAL")+.synchronous("NORMAL")— recommended for concurrent readers.busyTimeoutMs(5000)— wait this long when the database is locked instead of failing immediately.dateClass(DateClass.TEXT)— keep the default (the codecs assume it); change only if you supply your own bindings
Query analysis
Query analysis works on SQLite. Column types are matched against the affinity-aware vendor-name aliases registered on each SqliteType. The xerial driver's ResultSetMetaData.getColumnTypeName() reports the literal text from CREATE TABLE, which the analyzer normalises (strips precision, lowercases) and compares against the type's alias set.
For empty result sets the driver may report NULL (0) as the column type — the analyzer treats that as "unknown" and skips the type assertion rather than failing. Pre-create some sample rows in your test fixture if you want type checking on every column.