リサイクルボックスに「ペットボトル専用」と書かれたラベルが貼ってあるのに、誰かが紙くずを投げ入れても、一言の抗議もなく静かに受け入れている様子を想像してみてください。
これが、開発者が初めて SQLite の型システムに出会ったときの背筋の凍るような体験です。
型が正しくなければ入国を即座に拒否する PostgreSQL のような厳格な税関スタイルに慣れていると、SQLite の気まぐれさ(ルーズさ)に人生を疑いたくなるかもしれません。
さらに恐ろしいことに、テーブル構造を変更 しようとすると、こう告げられます。
「テーブルを直接変更することはできません。新しい家を建て、家具を移動させてから、古い家を爆破してください。」
SQLiteの内部には5つのストレージクラスしかない
CREATE TABLE でどれほど華やかな型名(VARCHAR(255)、BIGINT、DECIMAL)を宣言しても、SQLite の内部が認識するのは以下の 5つのストレージクラス だけです。
| ストレージクラス | 説明 |
|---|---|
| NULL | 空値 |
| INTEGER | 整数(数値の大きさに応じて自動的に1〜8バイトを占有) |
| REAL | 浮動小数点数(固定8バイト) |
| TEXT | 文字列(デフォルトはUTF-8エンコーディング) |
| BLOB | バイナリデータ(入力されたまま保存) |
カラムに設定する型は、SQLite にとって 単なる「推奨」であり、「強制ルール」ではありません。
これを 「型アフィニティ(Type Affinity)」 と呼びます。
SQLiteはデータを推奨される型に変換しようと試みますが、変換できない場合はエラーを吐かずに元のデータをそのまま挿入します。
カラムに設定する型は、SQLite にとって 単なる「推奨」であり、「強制ルール」ではありません。
最も陥りやすい3つの型の罠
罠1:ネイティブのBooleanがない
SQLite にには Boolean型がありません。True と False は整数 1 と 0 でしか表現できません。
Node.js を使って SQLite からデータを取得すると、true や false ではなく、数値の 1 または 0 が返ってきます。
if (user.is_admin === true) のようなチェックを直接行うと、永遠に真になりません。
罠2:Date/Time型がない
SQLite には 日付時刻型がありません。時間は以下のように保存するしかありません。
| 保存方法 | 例 | メリット・デメリット |
|---|---|---|
| TEXT(ISO-8601 文字列) | '2026-05-19T18:00:00Z' |
最も推奨。視認性が高く、将来 PostgreSQL に移行する際もスムーズに変換可能 |
| INTEGER(Unixタイムスタンプ) | 1747656000 |
フットプリントは小さいが、人間には読めない |
日付を 2026/5/19 午後6時 のような独自のカスタム形式で保存してはいけません。将来のデータマイグレーションで悲劇が起こります。
罠3:INTEGERカラムに文字列を挿入してもエラーにならない
PostgreSQL では INTEGER カラムに文字列を挿入すると即座にエラーになります。しかし SQLite は静かに変換を試み、失敗した場合はそのまま受け入れます。
これは、汚いデータが静かにデータベースに紛れ込む 可能性を意味します。ある日、プログラムが予期しない型を受け取ってクラッシュしたときに初めて、問題に気づくことになります。
防御的プログラミング:気まぐれなデータベースに厳格な態度で臨む
SQLite のルーズさに直面したとき、Node.js 開発では 厳格な防御メカニズム を構築しなければなりません。
| 防御レベル | ツール | 役割 |
|---|---|---|
| コンパイル時のガード | TypeScript | コード記述段階で誤った型をキャッチする |
| API入口での検証 | Zod | 受信するデータを厳格に検証する(ageが必ず数値であることを保証する) |
| 内部的な型キャスト | Prisma / Drizzle ORM | SQLiteとPostgreSQL間の型の違いを自動的に処理する |
「データ検証の門番」 をデータベース層から
Node.jsアプリケーション層に移動させることが、SQLite の開発スピードを活かしつつ、将来の拡張性を確保するための重要な戦略です。
ORM を使用する場合、コード内で type: 'boolean' と宣言しておけば、ORMが SQLite への保存時に自動的に 1/0 に変換し、読み込み時には true/false に自動変換してくれるため、底層の型の違いを完璧に隠蔽できます。
ALTER TABLEは不完全:何が変更できて、何ができないのか
SQLite がサポートするテーブル構造の変更は非常に限定的です。
| 操作 | サポート状況 |
|---|---|
カラムの追加(ADD COLUMN) |
はい |
カラム名の変更(RENAME COLUMN) |
はい |
カラムの削除(DROP COLUMN) |
はい (比較的新しいバージョンのみ) |
テーブル名の変更(RENAME TO) |
はい |
| カラム型の変更 | いいえ |
UNIQUE、NOT NULL 制約の追加/削除 |
いいえ |
| プライマリキーの変更 | いいえ |
| 外部キーの変更 | いいえ |
「サポートされていない」変更を行う必要がある場合、
SQLiteは 「再作成して移行する」 戦略を実行するよう要求します。
再作成&移行の4ステップ:SQLite流のテーブルアップグレード方法
直接変更できないため、公式ドキュメントが推奨する標準的なアプローチは、新しい家を建て、家具を移動し、古い家を爆破し、新しい表札を掲げる ことです。
| ステップ | アクション | 説明 |
|---|---|---|
| 1 | 新しいテーブルの作成 | CREATE TABLE users_new (...) を正しい構造で作成 |
| 2 | データのコピー | INSERT INTO users_new SELECT ... FROM users |
| 3 | 古いテーブルの削除 | DROP TABLE users |
| 4 | テーブル名の変更 | ALTER TABLE users_new RENAME TO users |
これら4つのステップは 一気に行う 必要があり、途中で停電やアプリケーションのクラッシュが発生するとデータ損失につながります。
アップグレードでデータを失わないための確保:2つの安全防衛線
防衛線1:物理的な防衛、ファイルを直接コピーする
SQLite は本質的に単なるファイルです。スキーマ変更を実行する前に、単純に .db ファイルをバックアップとしてコピーしておくだけで十分です。
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
万が一失敗した場合は、ファイルを元に戻すだけで一瞬で復元できます。これは他の巨大なデータベースにはない強みです。
防衛線2:トランザクションによるカプセル化、データベースのタイムマシン
すべての移行ステップを単一の Transaction 内でカプセル化します。いずれかのステップが失敗した場合、プロセス全体が 自動的にロールバック され、何事もなかったかのように復元されます。
const Database = require('better-sqlite3');
const db = new Database('my_project.db');
const migrateData = db.transaction(() => {
db.prepare(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL DEFAULT 18
)
`).run();
db.prepare(`
INSERT INTO users_new (id, name, age)
SELECT id, name, COALESCE(age, 18) FROM users
`).run();
db.prepare('DROP TABLE users').run();
db.prepare('ALTER TABLE users_new RENAME TO users').run();
});
try {
migrateData();
console.log('テーブルのアップグレードに成功しました');
} catch (error) {
console.error('アップグレードに失敗しました。データは安全に復元されました:', error.message);
}
「バックアップファイル + トランザクションバインド」 が、データベースマイグレーションのエアバッグとなります。
厳格な態度でルーズな SQLite を乗りこなす
気まぐれな
SQLiteを 厳格なアプリケーション層アーキテクチャ で制御することで、将来の技術的負債を回避しながら、その超高速な開発スピードを存分に享受できます。
SQLiteの型システムは非常にルーズであり、ALTER TABLE にも多くの制限があります。
しかし、Node.js 側で TypeScript の型チェック + Zod のバリデーション + ORM の抽象化 をしっかりと行い、物理バックアップ + トランザクション の安全戦略と組み合わせることで、SQLite がもたらす開発効率を安心して享受しつつ、将来 PostgreSQL に移行するための道筋を整えることができます。