🫙

SQLiteの型について

createdAt
2025-08-19
reading time
9 min read

お断り

初学者が曖昧な理解のままこの文章を記載しています。この文章には間違いがある可能性があるため、公式ドキュメントを確認することを推奨します。参考文献は文末にあります

はじめに

SQLxでSQLiteを利用する際に型周りで沼にはまった記録です。以下常態で記載

沼にはまった事象

以下のコードは単純に10.0を挿入してそれを再度取り出しているだけだが、エラーになる

#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val NUMERIC
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind(10.0)
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<f64, _>("numeric_val").unwrap();
assert_eq!(val, 10.0);
}
Terminal window
$ cargo run
Compiling type-mapping-sqlx-sqlite v0.1.0 (/workspace/examples/type-mapping/sqlx-sqlite)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.10s
Running `/workspace/target/debug/type-mapping-sqlx-sqlite`
thread 'main' panicked at examples/type-mapping/sqlx-sqlite/src/main.rs:27:52:
called `Result::unwrap()` on an `Err` value: ColumnDecode { index: "\"numeric_val\"", source: "mismatched types; Rust type `f64` (as SQL type `REAL`) is not compatible with SQL type `INTEGER`" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

取り出す型をi64に変更した次のコードはエラーなく実行できる

#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val NUMERIC
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind(10.0)
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<i64, _>("numeric_val").unwrap();
assert_eq!(val, 10);
}

SQLiteの型システム

先ほどのエラーの原因はSQLiteの型システムへの不理解による。

SQLiteの型システムは以下の特徴を有している

  • データは5種類のストレージクラスのどれかとして保存される
  • 列によって型が決定される静的型ではなく、格納される値によって決まる動的型を採用している

ストレージクラス

SQLiteは以下の5種類のストレージクラスのどれかとして、値が格納される

名前説明
NULLデータがないことを示す
INTEGER符号付整数が格納される。大きさによって0~8バイトになるが、取り出す場合は8バイトになる
REALIEEE 754 binary64 形式で浮動小数点が格納される
TEXT文字列が格納される。エンコード形式はUTF-8,UTF-16BE,UTF-16LEのいずれか。データベースごとに固定で変更不可
BLOB入力されたデータがそのまま格納される

DATETIMEのような値を保存する専用のストレージクラスはサポートされていない。そのような値はTEXT,REAL,INTEGERの値のどれかとして保存することで、組み込みの日付関数を利用できる

型のアフィニティ(親和性)

SQLiteは動的型付けを採用しているため、ある列に複数のストレージクラスの値が格納されうる。以下のコードはNUMERIC型を宣言した列に文字列を入れているがこれはエラーにならない

#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val NUMERIC
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind("abcde")
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<String, _>("numeric_val").unwrap();
assert_eq!(val, "abcde");
}

SQLiteの列の型はあくまで格納されるデータ型を推奨するだけで、制約を設けるわけではない。この推奨されるデータ型はアフィニティと呼ばれる。 アフィニティは以下の5種類であり、各列には必ずどれかが割り当てられる

名前説明
TEXTストレージクラスNULL,TEXT,BLOBのどれかを使ってデータが格納される。何らかの数値が挿入されたとき、テキスト値に変換されて格納される
NUMERIC5つのストレージクラスのうちどれかを使ってデータが格納される。テキスト値が挿入されたとき数値リテラルであればINTEGERもしくはREALストレージクラスで、そうでない場合ストレージクラスTEXTで格納される。正確に整数で表現できるREAL値が挿入されたとき、ストレージクラスINTEGERで格納される
INTEGERNUMERICアフィニティと同じ動作
REAL整数値を自動で浮動小数点に変換することを除いて、NUMERICと同じ動作
BLOBデータ値は変換されずにそのまま保存される

アフィニティは列に割り当てられた型名によって決定される

FYI: https://www.sqlite.org/datatype3.html#determination_of_column_affinity

エラーの原因

冒頭で示したコードがエラーになる理由がわかる

冒頭のコード(再掲)
#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val NUMERIC
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind(10.0)
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<f64, _>("numeric_val").unwrap();
assert_eq!(val, 10.0);
}

データ型NUMERICにはNUMERICアフィニティが割り当てられる。ここで挿入した10.0は正確に整数で表現可能なため内部的に10として保存されることになる。 結果として、SQLiteからは整数が返却されf64では値をデコードできず、エラーになっていた

対策

対策として以下の2つが考えられる

型指定をREALにする

現在の型名ではNUMERICアフィニティが割り当てられているため、10.0などが自動で整数になってしまう。 浮動小数点を保存する列なのであれば、正確にREAL型を指定することでREALアフィニティが割り当てられ、変換が行われなくなる

#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val REAL
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind(10.0)
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<f64, _>("numeric_val").unwrap();
assert_eq!(val, 10.0);
}

ただし、この場合でもREAL以外のものが挿入される可能性がある。この場合テーブルに対してSTRICTオプションを使用することで、REAL以外を挿入することは出来なくなる

サンプルコード
  • STRICTオプションなし
#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val REAL
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind("abcde")
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<String, _>("numeric_val").unwrap();
assert_eq!(val, "abcde");
}
  • STRICTオプションあり(挿入時にエラーになる)
#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val REAL
) STRICT;
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind("abcde")
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<String, _>("numeric_val").unwrap();
assert_eq!(val, "abcde");
}
Terminal window
cargo run
Compiling type-mapping-sqlx-sqlite v0.1.0 (/workspace/examples/type-mapping/sqlx-sqlite)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.08s
Running `/workspace/target/debug/type-mapping-sqlx-sqlite`
thread 'main' panicked at examples/type-mapping/sqlx-sqlite/src/main.rs:20:10:
called `Result::unwrap()` on an `Err` value: Database(SqliteError { code: 3091, message: "cannot store TEXT value in REAL column test.numeric_val" })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

CAST式を使う

SQLiteにはストレージクラスを変換するCAST式がある。これを利用することでストレージクラスINTEGERで格納されたデータをREALに変換できる。

#[tokio::main]
async fn main() {
use sqlx::Row as _;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::raw_sql(
"
CREATE TABLE test (
numeric_val REAL
);
",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO test (numeric_val) VALUES (?)")
.bind(10.0)
.execute(&pool)
.await
.unwrap();
let row = sqlx::query("SELECT CAST(numeric_val AS REAL) AS numeric_val FROM test")
.fetch_one(&pool)
.await
.unwrap();
let val = row.try_get::<f64, _>("numeric_val").unwrap();
assert_eq!(val, 10.0);
}

終わりに

雰囲気で使う前によくドキュメントを読めば沼らなかった

参考