SQLite type mapping
SQLite differs meaningfully from the other JDBC databases: it uses dynamic typing and has only 5 storage classes (NULL, INTEGER, REAL, TEXT, BLOB). The declared column type is used only as a hint via type affinity:
Declared type contains | Affinity |
|---|---|
|
|
|
|
|
|
|
|
anything else |
|
Unlike the other databases supported in Kotlin DataFrame, SQLite does NOT canonicalize declared types. sqlTypeName in the driver's metadata is byte-for-byte what you wrote in CREATE TABLE. So INT8, INTEGER, TINYINT, MEDIUMINT, UNSIGNED BIG INT all share INTEGER affinity, but each keeps its literal declared name. There is no separate alias table — every declared name is resolved directly via the affinity tables below.
Because SQLite is dynamically typed, the Xerial JDBC driver reports getColumnClassName based on the actual stored value in the current row, not on the declared column type. The raw values returned by rs.getObject(int) therefore fall into a fixed set — driver produces exactly one of Integer/Long/Double/String/byte[]/null, chosen per row from the storage class of that value.
DataFrame's SQLite handler resolves each column in the following order:
User-supplied custom converters (by column name, then by declared type name) always win.
DATE/DATETIME/TIME/TIMESTAMP— detected by a substring match on the declared type name. Column type is fixed to an idiomatic Kotlin date-time type (kotlinx.datetime.LocalDate/LocalDateTime/LocalTime/kotlin.time.Instant); each value is converted from its storage class during preprocessing (ISO text → parsed viaLocalDate/LocalDateTime/Instant.parse; Unix INTEGER → epoch seconds; Julian REAL → date via the Julian-day formula). This gives one stable Kotlin type per column even when values are stored in mixed forms.BOOLEAN/BIT— the driver reportsTypes.BOOLEANbutrs.getObjectreturns Integer (0/1). A preprocessor converts every raw value (Int, Long, Double, or"true"/"1"/"y"String) to an actual KotlinBoolean.DECIMAL/NUMERIC— no canonical numeric type; DataFrame trusts the driver-reported class of each column (Int/Long/Double/ByteArray/String).Everything else falls through to the base
DbTypeend-to-end mapping. Note that DataFrame may expect a special type for some SQL type names (for example,UUID), while the Xerial driver only provides primitives. Consider using custom converters to handle these cases.
Column nullability is determined from the metadata provided by the JDBC driver. If the driver does not explicitly report a column as non-nullable, it is mapped to a nullable Kotlin type (Int? instead of Int).
INTEGER affinity
Declared type contains INT.
Declared type | DataFrame column type | Notes |
|---|---|---|
|
| |
|
| Also used implicitly for |
|
| |
|
| |
|
| |
|
| Xerial reports |
REAL affinity
Declared type contains REAL, FLOA, or DOUB.
Declared type | DataFrame column type | Notes |
|---|---|---|
|
| Note: not |
|
|
TEXT affinity
Declared type contains CHAR, CLOB, or TEXT.
Declared type | DataFrame column type | Notes |
|---|---|---|
|
| |
|
| |
|
|
BLOB affinity
Declared type contains BLOB or the column has no declared type.
Declared type | DataFrame column type | Notes |
|---|---|---|
|
| |
(none) |
| Column with no declared type falls back to BLOB. |
NUMERIC affinity (fallback for everything else)
DATE / DATETIME / TIME / TIMESTAMP
SQLite stores date and time values as TEXT, INTEGER, or REAL. Kotlin DataFrame automatically converts columns with recognized date-time types into idiomatic Kotlin date-time types.
Each value is converted according to its SQLite storage class during preprocessing.
Declared type | DataFrame column type | Storage class → conversion |
|---|---|---|
|
| TEXT (ISO |
|
| TEXT ( |
|
| TEXT ( |
|
| TEXT (ISO) → |
Detection is by declared type name, not jdbcType. Xerial changes the reported jdbcType based on the actual stored value — e.g. a DATE column with a Julian-day REAL value is reported as Types.FLOAT, and a TIMESTAMP column with an INTEGER value is reported as Types.INTEGER. DataFrame's SQLite adapter looks at sqlTypeName (substring match: DATETIME, TIMESTAMP, DATE, TIME) to preserve the intended date-time semantics regardless.
If a value cannot be parsed automatically (e.g., a DATETIME column contains an unexpected format), reading throws with a clear error message referencing the column name and stored value. Opt out of conversion by supplying a custom converter, for example:
BOOLEAN — INTEGER 0/1 converted to Boolean
Declared type | Storage class | DataFrame column type | Notes |
|---|---|---|---|
| INTEGER (0/1) |
| The preprocessor treats non-zero as |
| REAL |
| Same convention (non-zero → |
| TEXT |
| Case-insensitively accepts |
BOOL/BIT substring in the declared name is also caught (e.g. columns declared IS_ACTIVE_BOOL).
DECIMAL / NUMERIC — follow the storage class
DECIMAL and NUMERIC columns have no canonical numeric type; DataFrame reads the raw stored value as-is (Int, Long, or Double depending on how each row was inserted).
Declared type | Storage class | DataFrame column type | Notes |
|---|---|---|---|
| INTEGER |
| Follows the actual value's class. May fail for columns mixed |
| REAL |
| |
| REAL |
| |
| INTEGER |
| |
unrecognised type | any | depends on stored value | E.g. a text value in a |
Sometimes, a driver may return mixed Int and Long values for the same column, which can break column type detection. Consider using a custom converter to specify the expected column type and, optionally, provide a converter.
Assume we have a nullable "mixed_values" column for which the Xerial driver returns both Int and Long values.
You can specify the expected column type (Number) explicitly:
Or you can provide a converter to convert the values to the desired type:
STRICT tables
SQLite supports STRICT tables which enforce a limited set of storage class names (ANY, INT, INTEGER, REAL, TEXT, BLOB) and reject values of the wrong storage class. In STRICT tables the declared type is guaranteed to match the storage class, so the affinity tables above still hold — just without the ambiguity of ordinary tables. There are no dedicated BOOLEAN/DATE/TIMESTAMP storage classes in STRICT tables either.
The ANY column type accepts any storage class and reports the class of the stored value in metadata — DataFrame maps this via the same storage-class rules (String/Int/Long/Double/ByteArray).
Custom converters
Use Sqlite.withCustomConverters { ... } to register per-column or per-declared-type overrides. Column-name overrides win over type-name overrides.
Two overloads are available for each side:
Converter form —
forType<T, R>(name) { raw -> ... }/forColumn<T, R>(name) { raw -> ... }. A lambda transforms each raw stored value; the DataFrame column's Kotlin type is derived from the reifiedRviatypeOf<R>().Identity form —
forType<T>(name)/forColumn<T>(name). No transformation — values pass through asT. Handy when SQLite's type affinity misclassifies your column and the built-in mapping picks the wrong Kotlin type. Note: nullability is part ofT— declare it explicitly (forType<Long?>("BIGINT")) if you want a nullable column type.
SQLite specifics
No canonicalization — the Xerial JDBC driver preserves the declared type verbatim in
sqlTypeName. Two columns declaredINTandTINYINTboth have INTEGER affinity but distinctsqlTypeNamevalues in metadata.rs.getObject(int)returns exactly one ofInteger/Long/Double/String/byte[]/null. NoTimestamp/LocalDate/Boolean/Blobever reaches DataFrame from a SQLite driver — hence the SQLite adapter needs to translate.DATE/DATETIME/TIME/TIMESTAMPare converted from storage class to an idiomatic Kotlin date-time type. ISO strings, Unix epoch integers, and Julian days are all normalized tokotlinx.datetime.LocalDate/LocalDateTime/LocalTime/kotlin.time.Instantin preprocessing. This keeps the schema stable across rows even when values are stored in different formats. Unsupported inputs throw with a message pointing at the column and stored value and suggestingSqlite.withCustomConverters { }as the escape hatch.BOOLEANandBITare converted to Boolean. SQLite has no boolean storage class — values are stored as INTEGER (0/1). The preprocessor converts every raw value (Int, Long, Double, or textualtrue/false/yes/no) back to a real KotlinBoolean.DECIMALandNUMERICfollow the actual value's type. A DECIMAL column with a stored double value becomesDouble; with a stored integer value it becomesInt/Long.Custom overrides are registered via
Sqlite.withCustomConverters { ... }. See the Custom converters section above.