くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Drizzle x SQLiteで作成日時や更新日時をCustom typesでいい感じにしたい

前回の続き。Drizzle ORMをちょっと触ってみたときに、
いろいろ調べてみたときの備忘録(*´ω`*)

SQLiteにはdate/time型がない

残念なことに、SQLiteにはDate/Time/DateTimeの形がなく、
以下の形式の文字列(text)か数値(integer)で保存する

いくつか便利な関数が用意されていて、それを利用する形になる

  • date() ... YYYY-MM-DD形式のtext
  • time() ... HH:MM:SSHH:MM:SS.SSS形式のtext
  • datetime() ... YYYY-MM-DD HH:MM:SSなどのtext
  • strftime() ... 指定したフォーマットのtext
  • unixepoch() ... unix timestampのinteger

また、基本、SQLite上では、UTCで扱われるので注意

Drizzleで用意されている型

Drizzleだとこんな感じで、定義できる

import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const datetimeTestTable = sqliteTable("datetime_test", {
  // 10桁(秒)のinteger
  timestamp: integer({ mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
  // 13桁(ミリ秒)のinteger
  timestamp_ms: integer({ mode: "timestamp_ms" }).notNull().default(sql`(unixepoch())`),

  // UTCのISO8601形式の文字列(2025-01-01T00:00:00.000Z)
  datetime_iso_str: text().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});

DatetimeTestTableの各型はこんな感じ

export type DatetimeTestTable = typeof datetimeTestTable.$inferInsert;

// type DatetimeTestTable = {
//     timestamp?: Date | undefined;
//     timestamp_ms?: Date | undefined;
//     datetime_iso_str?: string | undefined;
// }

DB上でのinteger/textどちらも使えるが、
integer({ mode: "timestamp" })など、modeを指定すると、
TypeScript上ではDateとして扱うことができる

integer版の作成日時など

共通的なカラムとして扱う形式だととこんな感じ

import { integer, sqliteTable } from "drizzle-orm/sqlite-core";

// 共通のカラム
const timestamps = {
  // 作成日時
  created_at: integer({ mode: "timestamp" }).notNull()
    .default(sql`(unixepoch())`),
  // 更新日時
  updated_at: integer({ mode: "timestamp" }).notNull()
    .default(sql`(unixepoch())`)
    .$onUpdate(() => sql`(unixepoch())`),
  // 削除日時
  deleted_at: integer({ mode: "timestamp" }),
}

ISO8601版の作成日時

ISO8601文字列で、DBに保存したい場合はこんな感じ

const iso8601Datetimes = {
  created_at: text().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
  updated_at: text().notNull()
    .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`)
    .$onUpdate(() => sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
  deleted_at: text(),
};

ISO8601版でDateで受け取りたい

とはいえ、DB上ではtextでも、
TypeScript上では、Dateで扱いたい。。。

ので、DrizzleのCustom Typesという機能を使って実現してみる

Custom Typesとは

text()integer()みたいなのを自分で作れる機能。
ドキュメントには、PostgresとMySqlしか書かれていないが、
SQLiteにも用意されているっぽい

Custom Typesを使ってみる

customTypeで型を自作してみるとこんな感じ

import { customType } from "drizzle-orm/sqlite-core";

// TypeScriptではDate、DB上ではISOのtextとして扱うCustom Types
const isoDateTime = customType<{
  data: Date; // TypeScript上の型
  driverData: string; // DB上の型
}>({
  // DB上の型
  dataType: (): string => "text",

  // TypeScript -> DBの変換
  toDriver: (value: Date): string => value.toISOString(),

  // DB -> TypeScriptの変換
  fromDriver: (value: string): Date => new Date(value),
});

テーブル定義はこんな感じになる

export const isoDatetimeTestTable = sqliteTable("iso_datetime_test", {
  // 作ったisoDateTimeを使ってカラムを定義
  created_at: isoDateTime().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
});

// 型推論でDate型になる
export type IsoDatetimeTest = typeof isoDatetimeTestTable.$inferInsert;
// type IsoDatetimeTest = {
//     created_at?: Date | undefined;
// }

他のオプションはこんな感じ

customType<{
  data: Date; // TypeScript上の型
  driverData: string; // DB上の型
  config: Record<string, any>, // 任意のconfig
  configRequired: false, // configの必須フラグ
  // カスタムタイプとして、notNullか 
  // trueの場合、常に.notNull()が付与された状態になる(DDLには含まれない)
  notNull: false,
  // カスタムタイプとして、デフォルト値があるか
  // trueの場合、常に.default()系が設定された状態になる(DDLには含まれない)
  default: false,
}>();

日付ライブラリでもOK

なので、「Dateの代わりにDayjsを使う」とかもOK

import { customType } from "drizzle-orm/sqlite-core";
import { Dayjs } from "dayjs";

// Dateの代わりにDayjsを使うver
const isoDateTimeDayjs = customType<{
  data: Dayjs; // TypeScript上の型
  driverData: string; // DB上の型
}>({
  // DB上の型
  dataType: (): string => "text",

  // TypeScript -> DBの変換
  toDriver: (value: Dayjs): string => value.toISOString(),

  // DB -> TypeScriptの変換
  fromDriver: (value: string): Dayjs => new Dayjs(value),
});

以上!! これでだいぶ捗るぞ。。。(*´ω`*)

参考にしたサイト様