From a873081746f6f1d64ff69989996aca32476c7cba Mon Sep 17 00:00:00 2001 From: J Date: Tue, 23 Jun 2026 16:54:18 +0800 Subject: [PATCH] refactor(channel): defer lark_* table drop to a follow-up migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preflight deploy review: dropping lark_* in the same release that cuts over (old migration 125) is not rollback/rolling-safe — the v0.3.27 release still reads lark_*, so a rolling deploy or a post-deploy code rollback would hit "relation does not exist". Remove the drop and keep the old tables for one release (standard expand/contract): migration 124 already backfilled lark_* -> channel_*, the new code reads/writes only channel_*, and the physical drop moves to a separate cleanup migration once this ships and is observed. The lark_* tables remain in the schema, so sqlc regenerates the (now unused) db.Lark* models; queries/lark.sql stays deleted (the new code uses channel_*). No code path reads lark_* — only the destructive drop is deferred, keeping the design's no-compat-layer / no-dual-write rule while being deploy-safe. MUL-3515 Co-authored-by: multica-agent --- .../migrations/125_drop_lark_tables.down.sql | 194 ------------------ server/migrations/125_drop_lark_tables.up.sql | 15 -- server/pkg/db/generated/models.go | 80 ++++++++ 3 files changed, 80 insertions(+), 209 deletions(-) delete mode 100644 server/migrations/125_drop_lark_tables.down.sql delete mode 100644 server/migrations/125_drop_lark_tables.up.sql diff --git a/server/migrations/125_drop_lark_tables.down.sql b/server/migrations/125_drop_lark_tables.down.sql deleted file mode 100644 index 219147c89c..0000000000 --- a/server/migrations/125_drop_lark_tables.down.sql +++ /dev/null @@ -1,194 +0,0 @@ --- Recreate the lark_* tables dropped by 125_drop_lark_tables.up.sql --- (structure only — the data lives in channel_* after migration 124). This is --- the authoritative pre-drop schema: lark_installation plus the deltas from --- migrations 112 (bot_union_id), 113 (per-installation dedup), 116 (region), --- and 122 (thread-reply columns). Foreign keys are added after all tables --- exist, so table order does not matter. - - -CREATE TABLE IF NOT EXISTS lark_binding_token ( - token_hash text NOT NULL, - workspace_id uuid NOT NULL, - installation_id uuid NOT NULL, - lark_open_id text NOT NULL, - expires_at timestamp with time zone NOT NULL, - consumed_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT lark_binding_token_ttl_cap CHECK ((expires_at <= (created_at + '00:15:00'::interval))) -); - -CREATE TABLE IF NOT EXISTS lark_chat_session_binding ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - chat_session_id uuid NOT NULL, - installation_id uuid NOT NULL, - lark_chat_id text NOT NULL, - lark_chat_type text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - last_lark_message_id text, - last_lark_thread_id text, - CONSTRAINT lark_chat_session_binding_lark_chat_type_check CHECK ((lark_chat_type = ANY (ARRAY['p2p'::text, 'group'::text]))) -); - -CREATE TABLE IF NOT EXISTS lark_inbound_audit ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - installation_id uuid, - lark_chat_id text, - event_type text NOT NULL, - lark_event_id text, - lark_message_id text, - drop_reason text NOT NULL, - received_at timestamp with time zone DEFAULT now() NOT NULL -); - -CREATE TABLE IF NOT EXISTS lark_inbound_message_dedup ( - installation_id uuid NOT NULL, - message_id text NOT NULL, - received_at timestamp with time zone DEFAULT now() NOT NULL, - processed_at timestamp with time zone, - claim_token uuid DEFAULT gen_random_uuid() NOT NULL -); - -CREATE TABLE IF NOT EXISTS lark_installation ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - workspace_id uuid NOT NULL, - agent_id uuid NOT NULL, - app_id text NOT NULL, - app_secret_encrypted bytea NOT NULL, - tenant_key text, - bot_open_id text NOT NULL, - installer_user_id uuid NOT NULL, - status text DEFAULT 'active'::text NOT NULL, - ws_lease_token text, - ws_lease_expires_at timestamp with time zone, - installed_at timestamp with time zone DEFAULT now() NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - bot_union_id text, - region text DEFAULT 'feishu'::text NOT NULL, - CONSTRAINT lark_installation_region_check CHECK ((region = ANY (ARRAY['feishu'::text, 'lark'::text]))), - CONSTRAINT lark_installation_status_check CHECK ((status = ANY (ARRAY['active'::text, 'revoked'::text]))) -); - -CREATE TABLE IF NOT EXISTS lark_outbound_card_message ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - chat_session_id uuid NOT NULL, - task_id uuid, - lark_chat_id text NOT NULL, - lark_card_message_id text NOT NULL, - status text DEFAULT 'pending'::text NOT NULL, - last_patched_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT lark_outbound_card_message_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'streaming'::text, 'final'::text, 'error'::text]))) -); - -CREATE TABLE IF NOT EXISTS lark_user_binding ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - workspace_id uuid NOT NULL, - multica_user_id uuid NOT NULL, - installation_id uuid NOT NULL, - lark_open_id text NOT NULL, - union_id text, - bound_at timestamp with time zone DEFAULT now() NOT NULL -); - -ALTER TABLE ONLY lark_binding_token - ADD CONSTRAINT lark_binding_token_pkey PRIMARY KEY (token_hash); - -ALTER TABLE ONLY lark_chat_session_binding - ADD CONSTRAINT lark_chat_session_binding_chat_session_id_key UNIQUE (chat_session_id); - -ALTER TABLE ONLY lark_chat_session_binding - ADD CONSTRAINT lark_chat_session_binding_installation_id_lark_chat_id_key UNIQUE (installation_id, lark_chat_id); - -ALTER TABLE ONLY lark_chat_session_binding - ADD CONSTRAINT lark_chat_session_binding_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY lark_inbound_audit - ADD CONSTRAINT lark_inbound_audit_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY lark_inbound_message_dedup - ADD CONSTRAINT lark_inbound_message_dedup_pkey PRIMARY KEY (installation_id, message_id); - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_app_id_key UNIQUE (app_id); - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_id_workspace_id_key UNIQUE (id, workspace_id); - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_workspace_id_agent_id_key UNIQUE (workspace_id, agent_id); - -ALTER TABLE ONLY lark_outbound_card_message - ADD CONSTRAINT lark_outbound_card_message_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY lark_user_binding - ADD CONSTRAINT lark_user_binding_installation_id_lark_open_id_key UNIQUE (installation_id, lark_open_id); - -ALTER TABLE ONLY lark_user_binding - ADD CONSTRAINT lark_user_binding_pkey PRIMARY KEY (id); - -CREATE INDEX IF NOT EXISTS idx_lark_binding_token_installation ON lark_binding_token USING btree (installation_id, expires_at); - -CREATE INDEX IF NOT EXISTS idx_lark_chat_session_binding_session ON lark_chat_session_binding USING btree (chat_session_id); - -CREATE INDEX IF NOT EXISTS idx_lark_inbound_audit_installation ON lark_inbound_audit USING btree (installation_id, received_at DESC); - -CREATE INDEX IF NOT EXISTS idx_lark_inbound_audit_reason ON lark_inbound_audit USING btree (drop_reason, received_at DESC); - -CREATE INDEX IF NOT EXISTS idx_lark_inbound_dedup_received ON lark_inbound_message_dedup USING btree (received_at); - -CREATE INDEX IF NOT EXISTS idx_lark_installation_agent ON lark_installation USING btree (agent_id); - -CREATE INDEX IF NOT EXISTS idx_lark_installation_lease ON lark_installation USING btree (ws_lease_expires_at) WHERE (status = 'active'::text); - -CREATE INDEX IF NOT EXISTS idx_lark_installation_workspace ON lark_installation USING btree (workspace_id); - -CREATE INDEX IF NOT EXISTS idx_lark_outbound_card_session ON lark_outbound_card_message USING btree (chat_session_id, created_at DESC); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_lark_outbound_card_task ON lark_outbound_card_message USING btree (task_id) WHERE (task_id IS NOT NULL); - -CREATE INDEX IF NOT EXISTS idx_lark_user_binding_user ON lark_user_binding USING btree (multica_user_id, workspace_id); - -CREATE INDEX IF NOT EXISTS idx_lark_user_binding_workspace_open ON lark_user_binding USING btree (workspace_id, lark_open_id); - -ALTER TABLE ONLY lark_binding_token - ADD CONSTRAINT lark_binding_token_installation_id_fkey FOREIGN KEY (installation_id) REFERENCES lark_installation(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_binding_token - ADD CONSTRAINT lark_binding_token_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_chat_session_binding - ADD CONSTRAINT lark_chat_session_binding_chat_session_id_fkey FOREIGN KEY (chat_session_id) REFERENCES chat_session(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_chat_session_binding - ADD CONSTRAINT lark_chat_session_binding_installation_id_fkey FOREIGN KEY (installation_id) REFERENCES lark_installation(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_inbound_audit - ADD CONSTRAINT lark_inbound_audit_installation_id_fkey FOREIGN KEY (installation_id) REFERENCES lark_installation(id) ON DELETE SET NULL; - -ALTER TABLE ONLY lark_inbound_message_dedup - ADD CONSTRAINT lark_inbound_message_dedup_installation_id_fkey FOREIGN KEY (installation_id) REFERENCES lark_installation(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES agent(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_installer_user_id_fkey FOREIGN KEY (installer_user_id) REFERENCES "user"(id) ON DELETE RESTRICT; - -ALTER TABLE ONLY lark_installation - ADD CONSTRAINT lark_installation_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_outbound_card_message - ADD CONSTRAINT lark_outbound_card_message_chat_session_id_fkey FOREIGN KEY (chat_session_id) REFERENCES chat_session(id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_outbound_card_message - ADD CONSTRAINT lark_outbound_card_message_task_id_fkey FOREIGN KEY (task_id) REFERENCES agent_task_queue(id) ON DELETE SET NULL; - -ALTER TABLE ONLY lark_user_binding - ADD CONSTRAINT lark_user_binding_installation_fk FOREIGN KEY (installation_id, workspace_id) REFERENCES lark_installation(id, workspace_id) ON DELETE CASCADE; - -ALTER TABLE ONLY lark_user_binding - ADD CONSTRAINT lark_user_binding_member_fk FOREIGN KEY (workspace_id, multica_user_id) REFERENCES member(workspace_id, user_id) ON DELETE CASCADE; diff --git a/server/migrations/125_drop_lark_tables.up.sql b/server/migrations/125_drop_lark_tables.up.sql deleted file mode 100644 index 340f6b8ac8..0000000000 --- a/server/migrations/125_drop_lark_tables.up.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Drop the lark_* tables. MUL-3515 generalized them into channel_* in --- migration 124 and the Go cutover has landed (the lark package now reads and --- writes only channel_*), so the old tables are dead. Per the design (§5: --- replace, do not keep both) the old path is removed rather than left in place. --- --- Dropped in dependency order — every other lark_* table has a foreign key to --- lark_installation, so it goes last. The down migration recreates the schema --- (structure only; the data already lives in channel_*). -DROP TABLE IF EXISTS lark_binding_token; -DROP TABLE IF EXISTS lark_outbound_card_message; -DROP TABLE IF EXISTS lark_inbound_audit; -DROP TABLE IF EXISTS lark_inbound_message_dedup; -DROP TABLE IF EXISTS lark_user_binding; -DROP TABLE IF EXISTS lark_chat_session_binding; -DROP TABLE IF EXISTS lark_installation; diff --git a/server/pkg/db/generated/models.go b/server/pkg/db/generated/models.go index ac60617b67..58c4ecef9b 100644 --- a/server/pkg/db/generated/models.go +++ b/server/pkg/db/generated/models.go @@ -523,6 +523,86 @@ type IssueToLabel struct { LabelID pgtype.UUID `json:"label_id"` } +type LarkBindingToken struct { + TokenHash string `json:"token_hash"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + InstallationID pgtype.UUID `json:"installation_id"` + LarkOpenID string `json:"lark_open_id"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` + ConsumedAt pgtype.Timestamptz `json:"consumed_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type LarkChatSessionBinding struct { + ID pgtype.UUID `json:"id"` + ChatSessionID pgtype.UUID `json:"chat_session_id"` + InstallationID pgtype.UUID `json:"installation_id"` + LarkChatID string `json:"lark_chat_id"` + LarkChatType string `json:"lark_chat_type"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + LastLarkMessageID pgtype.Text `json:"last_lark_message_id"` + LastLarkThreadID pgtype.Text `json:"last_lark_thread_id"` +} + +type LarkInboundAudit struct { + ID pgtype.UUID `json:"id"` + InstallationID pgtype.UUID `json:"installation_id"` + LarkChatID pgtype.Text `json:"lark_chat_id"` + EventType string `json:"event_type"` + LarkEventID pgtype.Text `json:"lark_event_id"` + LarkMessageID pgtype.Text `json:"lark_message_id"` + DropReason string `json:"drop_reason"` + ReceivedAt pgtype.Timestamptz `json:"received_at"` +} + +type LarkInboundMessageDedup struct { + InstallationID pgtype.UUID `json:"installation_id"` + MessageID string `json:"message_id"` + ReceivedAt pgtype.Timestamptz `json:"received_at"` + ProcessedAt pgtype.Timestamptz `json:"processed_at"` + ClaimToken pgtype.UUID `json:"claim_token"` +} + +type LarkInstallation struct { + ID pgtype.UUID `json:"id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + AgentID pgtype.UUID `json:"agent_id"` + AppID string `json:"app_id"` + AppSecretEncrypted []byte `json:"app_secret_encrypted"` + TenantKey pgtype.Text `json:"tenant_key"` + BotOpenID string `json:"bot_open_id"` + InstallerUserID pgtype.UUID `json:"installer_user_id"` + Status string `json:"status"` + WsLeaseToken pgtype.Text `json:"ws_lease_token"` + WsLeaseExpiresAt pgtype.Timestamptz `json:"ws_lease_expires_at"` + InstalledAt pgtype.Timestamptz `json:"installed_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + BotUnionID pgtype.Text `json:"bot_union_id"` + Region string `json:"region"` +} + +type LarkOutboundCardMessage struct { + ID pgtype.UUID `json:"id"` + ChatSessionID pgtype.UUID `json:"chat_session_id"` + TaskID pgtype.UUID `json:"task_id"` + LarkChatID string `json:"lark_chat_id"` + LarkCardMessageID string `json:"lark_card_message_id"` + Status string `json:"status"` + LastPatchedAt pgtype.Timestamptz `json:"last_patched_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type LarkUserBinding struct { + ID pgtype.UUID `json:"id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + MulticaUserID pgtype.UUID `json:"multica_user_id"` + InstallationID pgtype.UUID `json:"installation_id"` + LarkOpenID string `json:"lark_open_id"` + UnionID pgtype.Text `json:"union_id"` + BoundAt pgtype.Timestamptz `json:"bound_at"` +} + type Member struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"`