refactor(channel): defer lark_* table drop to a follow-up migration

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 <github@multica.ai>
This commit is contained in:
J
2026-06-23 16:54:18 +08:00
parent 4364a1dfe6
commit a873081746
3 changed files with 80 additions and 209 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"`