メインコンテンツまでスキップ

Turso 更新タイミング一覧

Turso(SQLite / libSQL)上のテーブルについて、「いつ、何がきっかけで、どのカラムが更新されるか」 をまとめる。

Meilisearch 側の更新タイミングは Meilisearch 更新タイミング を参照。トリガー番号(T1〜T12)は両ページ共通で、両系統にまたがるトリガーは両方から参照される。

詳細なカラム定義は DB スキーマ を参照。


1. 更新トリガー一覧

両系統を合わせた 19 トリガー。太字が Turso を書き換えるトリガー。

#トリガー実装実行頻度Turso への影響
T1Google OAuth サインアップBetter Authユーザー登録時 1 回users (id/name/email/image/email_verified/role/stripe_customer_id/credit_balance/referral_code/created_at)
T2認証済み API 初回リクエストcountryBackfillMiddleware各ユーザー最大 1 回users.country
T3Stripe Webhook(subscription.created 他)Better Auth Stripe pluginStripe イベント毎subscriptions.* + users.credit_balance + (紹介成立時) users.bonus_credit / users.referral_rewarded_at
T4クレジット消費(字幕閲覧)consumeCreditsTransactionユーザー操作毎users.credit_balance (優先消費) + users.bonus_credit (不足分を減算) + user_video_access (UPSERT)
T5月次バッチbatch-monthly-reset.ymlmonthly-reset.ts毎月 1 日 UTC 0:00user_video_access (expired 削除) + users.credit_balance (一括リセット、bonus_credit は対象外)
T6チャンネル初回取り込みstartChannelFirstFetchユーザーが新チャンネルを検索した時channels (INSERT) + videos (INSERT)(※ Meilisearch にも影響)
T7チャンネル 24h 差分取得(#305)fetchChannelDiff検索時に 24h 経過チェックchannels.* (UPSERT、メタデータ refresh) + 新規 videos (INSERT)(※ Meilisearch にも影響)
T8字幕取得ジョブ完了subtitle-fetch-orchestrator字幕キュー処理毎videos.subtitle_status / subtitle_type(※ Meilisearch にも影響)
T9お気に入りチャンネル操作favorite channel APIユーザー操作毎favorite_channels (INSERT / DELETE)
T10検索実行search route + search-history API検索毎search_histories (UPSERT、searched_at 更新)
T11ユーザーアカウント削除Better Authユーザー操作users 削除 → sessions / accounts / favorite_channels / search_histories を CASCADE 削除
T12DMCA テイクダウン管理者操作ad-hocvideos.subtitle_status = 'deleted'(※ Meilisearch からもドキュメント削除)
T13ボーナスクレジット付与grantBonusCredits紹介報酬成立時・キャンペーン等users.bonus_credit (加算)
T14紹介コード入力POST /api/referrals/claimPlus 加入前のフォーム入力users.referred_by (セット)
T14.5広告主プロフィール作成・更新出稿フォーム → createAdvertiserProfile / 編集画面初回出稿前、または事業者情報の変更時advertiser_profiles (INSERT / UPDATE)
T15ダイレクト広告の予約作成出稿フォーム → createReservation(T14.5 の profile を参照)広告主が出稿時ad_reservations (INSERT, status=pending_reviewadvertiser_profile_id + snapshot カラム)
T16admin による広告審査・停止admin ページ → updateReservationStatusadmin 操作毎ad_reservations.status / stopped_at / stopped_reason (UPDATE)
T17ダイレクト広告のクリックGET /api/ad-click/:idユーザー クリック毎ad_click_logs (INSERT)
T18ダイレクト広告の表示表示エンジン → incrementImpressionページ配信毎ad_impression_daily (UPSERT、impressions を INCREMENT)
T19ダイレクト広告の自動期限切れバッチ / transitionExpiredReservations月次実行想定ad_reservations.status = expired (UPDATE)
注記

広告料金の改定は Stripe Dashboard で管理するため Turso 側には書き込みが発生しない(ダイレクト広告仕様書 §9)。改定時は admin が Stripe で新 Price を作成し、Workers Secret STRIPE_AD_PRICE_JP_ID を切り替える運用。


2. テーブル別: どのトリガーで更新されるか

users

カラム初回設定更新トリガー
id / name / email / image / email_verifiedT1Better Auth updateUser、Google プロフィール同期
roleT1(member手動(admin が昇格)
stripe_customer_idT1 (createCustomerOnSignUp)基本変更なし
credit_balanceT1(Free プラン上限 50)T4(優先的に減算。不足分はその後 bonus_credit から)/ T3(プラン変更でリセット)/ T5(月次一括リセット)
bonus_creditT1(デフォルト 0)T4credit_balance を使い切った後の不足分を減算)/ T13(付与で加算)。T5 は対象外(温存される)
referral_codeT1databaseHooks.user.create.after で nanoid 8 文字を生成。UNIQUE 衝突時は最大 3 回リトライ)基本的に変更なし(既存ユーザーはバックフィルスクリプトで付与)
referred_by-T14(紹介コード入力フォームからの /api/referrals/claim で確定。以降は上書き不可)
referral_rewarded_at-T3 経由で onSubscriptionComplete 内に設定(Plus 以上の初回加入時)。IS NOT NULL の行は以降の Webhook でスキップ
country-T2(初回のみ、上書きしない)
created_at / updated_atT1updated_at のみ Drizzle $onUpdate が各 UPDATE で自動更新

channels

カラム初回設定更新トリガー
idT6-
title / handle / thumbnail_url / video_count / country / subscriber_countT6T7 で再フェッチ値で UPSERT
subtitle_statusT6('pending'字幕取得フロー側で state machine 遷移(processing / completed / failed / deleted / opted_out
created_atT6-
updated_atT6T7 の fetch 成功時のみ now に更新。クォータ失敗時は触らず 24h ロックを解除

videos

カラム初回設定更新トリガー
id / channel_id / published_at / duration_secondsT6 or T7(新規動画 INSERT 時)以降変更なし
title / thumbnail_url / category_id同上動画単位の再フェッチは未実装。変動が起きても追従しない
view_count / like_count同上(取得値 or null)動画単位の再フェッチは未実装(課題: ソートすると「1 年前の値」で並ぶ)
subtitle_status'pending'T8completed / failed / unavailable に遷移。T12deleted
subtitle_typenullT8 完了時に "manual" / "auto" を設定
updated_at-Drizzle $onUpdate が各 UPDATE で自動更新

user_video_access

カラム初回設定更新トリガー
user_id / video_idT4 INSERTUPSERT の ON CONFLICT キー
creditsT4T4 再消費で上書き
expires_atT4(now + 4週間T4 再消費で延長
created_atT4変更なし(UPSERT でも触らない)
updated_atT4Drizzle $onUpdate
行全体-T5expires_at < now を物理削除

subscriptions

全カラム T3 経由でのみ書き込まれる(Better Auth Stripe plugin 管理)。アプリコードから直接書かない。

favorite_channels

全カラム T9(INSERT / DELETE)。UPDATE なし。T11 で CASCADE 削除。

search_histories

カラム初回設定更新トリガー
idT10 INSERT 時自動採番-
user_id / query / channels / target / sort / semantic / srT10UNIQUE 制約でヒットすると UPSERT に切替
searched_atT10T10 再検索で現在時刻に更新
created_atT10変更なし
updated_atT10Drizzle $onUpdate
行全体-T11 で CASCADE 削除

ad_slots

全カラム pnpm seed 実行時に INSERT(onConflictDoNothing で冪等)。運用中の UPDATE / DELETE なし。

advertiser_profiles

カラム初回設定更新トリガー
id / user_idT14.5変更なし(独立 PK、所有者は固定)
business_legal_name / business_address / contact_name / contact_email / business_category / invoice_registration_numberT14.5T14.5 UPDATE(マイページからの編集)。既存予約の snapshot は不変
created_atT14.5変更なし
updated_atT14.5Drizzle $onUpdate が UPDATE 時に自動更新

ad_reservations

カラム初回設定更新トリガー
id / user_id / slot_id / position / country / start_month / end_monthT15変更なし
advertiser_name / image_url / redirect_urlT15変更なし(再入稿は別予約扱い)
advertiser_profile_idT15(T14.5 で作成/選択された profile の ID を参照)変更なし
business_legal_name / business_address / contact_name / contact_email / business_category / invoice_registration_numberT15(profile から contract snapshot としてコピー)変更しない(contract immutable。profile 側の T14.5 UPDATE は既存予約に波及しない)
statusT15(pending_reviewT16(admin 審査で active / 強制停止で stopped)/ T19(期間終了で expired
stopped_at / stopped_reason-T16stopped に変更時のみ設定)
stripe_payment_intent_id-Stripe Checkout 成功時 UPDATE(Webhook 未実装、予約作成 route 実装時に対応)
created_at / updated_atT15updated_at は各 UPDATE で Drizzle $onUpdate が更新

ad_click_logs

全カラム T17 で INSERT。UPDATE / DELETE なし(予約削除後もログは残す設計)。

ad_impression_daily

カラム初回設定更新トリガー
id / ad_reservation_id / date / countryT18 初回 INSERT変更なし
impressionsT18(初回 1T18 UPSERT で impressions += 1 に INCREMENT
created_atT18変更なし
updated_atT18Drizzle $onUpdate(INCREMENT 時に更新)

3. DB 内で完結する同期チェーン

複数テーブルにまたがる更新の連鎖(Turso 内)。

チェーン B: プラン変更(T3)

T3 Stripe Webhook
├─ subscriptions.* UPSERT
└─ users.credit_balance = 新プラン上限 (via resetCredits)

チェーン C: クレジット消費(T4)

T4 consumeCreditsTransaction
├─ user_video_access UPSERT (credits / expires_at / updated_at)
├─ users.credit_balance -= 消費分(優先消費、残高上限)
└─ users.bonus_credit -= 残りの不足分

チェーン D: 月次リセット(T5)

T5 monthly-reset.ts
├─ user_video_access DELETE (expires_at < now)
└─ users.credit_balance 一括 UPDATE
├─ 全ユーザーを 50 (Free) にベースライン
├─ subscriptions.plan='plus' AND status IN ('active','trialing') → 500
└─ subscriptions.plan='pro' AND status IN ('active','trialing') → 2000

注: users.bonus_credit はこのリセットの対象外で温存される。resetAllCreditscredit_balance のみを UPDATE するため、追加の除外ロジックは不要。

チェーン E: ボーナスクレジット付与(T13)

T13 grantBonusCredits
└─ users.bonus_credit += amount(amount >= 0 の整数、対象ユーザー不在時は throw)

チェーン F: 紹介報酬成立フロー(T14 → T3)

紹介コードの入力から報酬付与までのフロー。T14 と T3 にまたがる。

T14 /api/referrals/claim(Plus 加入前のフォーム入力)
└─ users[B].referred_by = A.id(紹介者のユーザーID)

(B が Stripe チェックアウトへ遷移して Plus に加入)

T3 onSubscriptionComplete(Stripe Webhook)
└─ if B.referred_by IS NOT NULL AND B.referral_rewarded_at IS NULL:
├─ grantBonusCredits(A, 100) → A.bonus_credit += 100 [T13]
├─ grantBonusCredits(B, 100) → B.bonus_credit += 100 [T13]
└─ B.referral_rewarded_at = now()

冪等性:

  • Webhook が再送された場合も referral_rewarded_at IS NULL 条件で弾かれるため二重付与は発生しない
  • Plus → Pro のアップグレード等で onSubscriptionComplete が再発火する場合も、同じ条件で既付与の紹介に対して追加付与は行わない

チェーン G: ダイレクト広告の予約から期限切れまで(T15 → T16 → T19)

T15 出稿フォーム送信
├─ fetchAdPrice(country) で現行 Stripe Price ID を取得
├─ ad_reservations INSERT (status = 'pending_review')
└─ Stripe Checkout Session 作成(line_items.price + quantity = months)
Stripe 側の PaymentIntent が契約時金額を immutable に記録する

T16 admin 審査・停止
└─ ad_reservations UPDATE
- 承認: status = 'active'
- 強制停止: status = 'stopped', stopped_at = now, stopped_reason = '...'

(期間中は T17 でクリックログ、T18 でインプレッション集計が発生)

T19 月次バッチ or 自動チェック
└─ ad_reservations UPDATE (end_month < 現在月 → status = 'expired')

冪等性:

  • 同一 (slot_id, position, country, start_month) は UNIQUE 制約で 2 重 INSERT 不可
  • status = 'stopped' に変更された行は T19 対象から除外される(すでに終了状態のため)

Turso → Meilisearch への伝播(概要)

Turso の書き込みが Meilisearch に波及するケースは Meilisearch 更新タイミング # Turso からの伝播 に詳細を記載。代表例:

  • T6 / T7channels.country / channels.subscriber_count が変動 → 対象チャンネルの全 subtitle document を updateDocuments(partial update)で追従
  • T8 で字幕取得完了 → subtitle document を新規投入
  • T12videos.subtitle_status = 'deleted' → Meilisearch の該当ドキュメントを削除

4. 「更新されないが、更新すべきか議論中」の項目

項目現状懸念対応状況
videos.view_count / like_count初回取得時のみソートすると古い値で並ぶ#84 のスコープ外。別 Issue で更新戦略(videos.list クォータ試算込み)を議論予定
videos.title / thumbnail_url初回取得時のみオーナーがタイトル変更しても追従しない必要性が顕在化したら別 Issue
videos.subtitle_status = 'deleted' のクリーンアップ月次バッチ設計あり(ER 図コメント)が未実装deleted レコードが蓄積するbatch-monthly-reset.yml に追加余地あり

関連資料