package sqlite // export.go — v0.34.0 CS0 // // ExportStore: batch-read queries for data export and GDPR operations. // SQLite dialect — uses ? placeholders, st() for time scanning, ScanArray for JSON arrays. import ( "context" "database/sql" "fmt" "strings" "time" "switchboard-core/models" ) // ExportStore implements store.ExportStore for SQLite. type ExportStore struct{} // NewExportStore creates a SQLite ExportStore. func NewExportStore() *ExportStore { return &ExportStore{} } // ── helpers ────────────────────────────────── // sqlitePlaceholders builds "?,?,...,?" and a matching []any slice for IN queries. func sqlitePlaceholders(ids []string) (string, []any) { ph := make([]string, len(ids)) args := make([]any, len(ids)) for i, id := range ids { ph[i] = "?" args[i] = id } return strings.Join(ph, ","), args } // ── User-scoped export ─────────────────────── func (s *ExportStore) UserChannels(ctx context.Context, userID string) ([]models.Channel, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, title, description, type, model, system_prompt, provider_config_id, is_archived, is_pinned, folder_id, team_id, project_id, workspace_id, settings, allow_anonymous, created_at, updated_at FROM channels WHERE user_id = ? ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user channels: %w", err) } defer rows.Close() var out []models.Channel for rows.Next() { var ch models.Channel if err := rows.Scan(&ch.ID, &ch.UserID, &ch.Title, &ch.Description, &ch.Type, &ch.Model, &ch.SystemPrompt, &ch.ProviderConfigID, &ch.IsArchived, &ch.IsPinned, &ch.FolderID, &ch.TeamID, &ch.ProjectID, &ch.WorkspaceID, &ch.Settings, &ch.AllowAnonymous, st(&ch.CreatedAt), st(&ch.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan channel: %w", err) } out = append(out, ch) } return out, rows.Err() } func (s *ExportStore) UserMessages(ctx context.Context, channelIDs []string) ([]models.Message, error) { if len(channelIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(channelIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, channel_id, role, content, model, tokens_used, tool_calls, metadata, parent_id, sibling_index, participant_type, participant_id, provider_config_id, created_at, updated_at FROM messages WHERE channel_id IN (%s) AND deleted_at IS NULL ORDER BY created_at ASC`, ph), args...) if err != nil { return nil, fmt.Errorf("export: user messages: %w", err) } defer rows.Close() var out []models.Message for rows.Next() { var m models.Message if err := rows.Scan(&m.ID, &m.ChannelID, &m.Role, &m.Content, &m.Model, &m.TokensUsed, &m.ToolCalls, &m.Metadata, &m.ParentID, &m.SiblingIndex, &m.ParticipantType, &m.ParticipantID, &m.ProviderConfigID, st(&m.CreatedAt), st(&m.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan message: %w", err) } out = append(out, m) } return out, rows.Err() } func (s *ExportStore) UserChannelParticipants(ctx context.Context, channelIDs []string) ([]models.ChannelParticipant, error) { if len(channelIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(channelIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, channel_id, participant_type, participant_id, role, display_name, avatar_url, joined_at, last_read_at FROM channel_participants WHERE channel_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: channel participants: %w", err) } defer rows.Close() var out []models.ChannelParticipant for rows.Next() { var cp models.ChannelParticipant if err := rows.Scan(&cp.ID, &cp.ChannelID, &cp.ParticipantType, &cp.ParticipantID, &cp.Role, &cp.DisplayName, &cp.AvatarURL, st(&cp.JoinedAt), st(&cp.LastReadAt)); err != nil { return nil, fmt.Errorf("export: scan participant: %w", err) } out = append(out, cp) } return out, rows.Err() } func (s *ExportStore) UserChannelModels(ctx context.Context, channelIDs []string) ([]models.ChannelModel, error) { if len(channelIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(channelIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, channel_id, model_id, provider_config_id, persona_id, handle, display_name, system_prompt, is_default FROM channel_models WHERE channel_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: channel models: %w", err) } defer rows.Close() var out []models.ChannelModel for rows.Next() { var cm models.ChannelModel if err := rows.Scan(&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID, &cm.PersonaID, &cm.Handle, &cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil { return nil, fmt.Errorf("export: scan channel model: %w", err) } out = append(out, cm) } return out, rows.Err() } func (s *ExportStore) UserChannelCursors(ctx context.Context, userID string, channelIDs []string) ([]models.ChannelCursor, error) { if len(channelIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(channelIDs) args = append(args, userID) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, channel_id, user_id, active_leaf_id, updated_at FROM channel_cursors WHERE channel_id IN (%s) AND user_id = ?`, ph), args...) if err != nil { return nil, fmt.Errorf("export: channel cursors: %w", err) } defer rows.Close() var out []models.ChannelCursor for rows.Next() { var cc models.ChannelCursor if err := rows.Scan(&cc.ID, &cc.ChannelID, &cc.UserID, &cc.ActiveLeafID, st(&cc.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan cursor: %w", err) } out = append(out, cc) } return out, rows.Err() } func (s *ExportStore) UserNotes(ctx context.Context, userID string) ([]models.Note, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, title, content, folder_path, tags, metadata, source_channel_id, source_message_id, team_id, created_at, updated_at FROM notes WHERE user_id = ? ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user notes: %w", err) } defer rows.Close() var out []models.Note for rows.Next() { var n models.Note var tagsJSON, metadataJSON string var sourceChannelID, sourceMessageID, teamID sql.NullString if err := rows.Scan(&n.ID, &n.UserID, &n.Title, &n.Content, &n.FolderPath, &tagsJSON, &metadataJSON, &sourceChannelID, &sourceMessageID, &teamID, st(&n.CreatedAt), st(&n.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan note: %w", err) } n.Tags = ScanArray(tagsJSON) n.SourceChannelID = NullableStringPtr(sourceChannelID) n.SourceMessageID = NullableStringPtr(sourceMessageID) n.TeamID = NullableStringPtr(teamID) out = append(out, n) } return out, rows.Err() } func (s *ExportStore) UserNoteLinks(ctx context.Context, noteIDs []string) ([]models.ExportNoteLink, error) { if len(noteIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(noteIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT source_note_id, target_note_id, target_title, COALESCE(display_text, ''), is_transclusion FROM note_links WHERE source_note_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: note links: %w", err) } defer rows.Close() var out []models.ExportNoteLink for rows.Next() { var nl models.ExportNoteLink if err := rows.Scan(&nl.SourceNoteID, &nl.TargetNoteID, &nl.TargetTitle, &nl.DisplayText, &nl.IsTransclusion); err != nil { return nil, fmt.Errorf("export: scan note link: %w", err) } out = append(out, nl) } return out, rows.Err() } func (s *ExportStore) UserMemories(ctx context.Context, userID string) ([]models.Memory, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, scope, owner_id, user_id, key, value, source_channel_id, confidence, status, created_at, updated_at FROM memories WHERE owner_id = ? AND scope = 'user' AND status = 'active' ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user memories: %w", err) } defer rows.Close() var out []models.Memory for rows.Next() { var m models.Memory if err := rows.Scan(&m.ID, &m.Scope, &m.OwnerID, &m.UserID, &m.Key, &m.Value, &m.SourceChannelID, &m.Confidence, &m.Status, st(&m.CreatedAt), st(&m.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan memory: %w", err) } out = append(out, m) } return out, rows.Err() } func (s *ExportStore) UserProjects(ctx context.Context, userID string) ([]models.Project, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, color, icon, scope, owner_id, team_id, is_archived, workspace_id, settings, created_at, updated_at FROM projects WHERE owner_id = ? AND scope = 'personal' ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user projects: %w", err) } defer rows.Close() var out []models.Project for rows.Next() { var p models.Project if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Color, &p.Icon, &p.Scope, &p.OwnerID, &p.TeamID, &p.IsArchived, &p.WorkspaceID, &p.Settings, st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan project: %w", err) } out = append(out, p) } return out, rows.Err() } func (s *ExportStore) UserProjectChannels(ctx context.Context, projectIDs []string) ([]models.ProjectChannel, error) { if len(projectIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(projectIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT project_id, channel_id, position, folder, added_at FROM project_channels WHERE project_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: project channels: %w", err) } defer rows.Close() var out []models.ProjectChannel for rows.Next() { var pc models.ProjectChannel if err := rows.Scan(&pc.ProjectID, &pc.ChannelID, &pc.Position, &pc.Folder, &pc.AddedAt); err != nil { return nil, fmt.Errorf("export: scan project channel: %w", err) } out = append(out, pc) } return out, rows.Err() } func (s *ExportStore) UserProjectKBs(ctx context.Context, projectIDs []string) ([]models.ProjectKB, error) { if len(projectIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(projectIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT project_id, kb_id, auto_search, added_at FROM project_knowledge_bases WHERE project_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: project KBs: %w", err) } defer rows.Close() var out []models.ProjectKB for rows.Next() { var pk models.ProjectKB if err := rows.Scan(&pk.ProjectID, &pk.KBID, &pk.AutoSearch, &pk.AddedAt); err != nil { return nil, fmt.Errorf("export: scan project KB: %w", err) } out = append(out, pk) } return out, rows.Err() } func (s *ExportStore) UserProjectNotes(ctx context.Context, projectIDs []string) ([]models.ProjectNote, error) { if len(projectIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(projectIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT project_id, note_id, added_at FROM project_notes WHERE project_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: project notes: %w", err) } defer rows.Close() var out []models.ProjectNote for rows.Next() { var pn models.ProjectNote if err := rows.Scan(&pn.ProjectID, &pn.NoteID, &pn.AddedAt); err != nil { return nil, fmt.Errorf("export: scan project note: %w", err) } out = append(out, pn) } return out, rows.Err() } func (s *ExportStore) UserWorkspaces(ctx context.Context, userID string) ([]models.Workspace, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, owner_type, owner_id, name, root_path, max_bytes, status, indexing_enabled, git_remote_url, git_branch, git_credential_id, git_last_sync, created_at, updated_at FROM workspaces WHERE owner_type = 'user' AND owner_id = ? ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user workspaces: %w", err) } defer rows.Close() var out []models.Workspace for rows.Next() { var w models.Workspace if err := rows.Scan(&w.ID, &w.OwnerType, &w.OwnerID, &w.Name, &w.RootPath, &w.MaxBytes, &w.Status, &w.IndexingEnabled, &w.GitRemoteURL, &w.GitBranch, &w.GitCredentialID, stN(&w.GitLastSync), st(&w.CreatedAt), st(&w.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan workspace: %w", err) } out = append(out, w) } return out, rows.Err() } func (s *ExportStore) UserWorkspaceFiles(ctx context.Context, workspaceIDs []string) ([]models.WorkspaceFile, error) { if len(workspaceIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(workspaceIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, workspace_id, path, is_directory, content_type, size_bytes, sha256, index_status, chunk_count, created_at, updated_at FROM workspace_files WHERE workspace_id IN (%s) ORDER BY path ASC`, ph), args...) if err != nil { return nil, fmt.Errorf("export: workspace files: %w", err) } defer rows.Close() var out []models.WorkspaceFile for rows.Next() { var wf models.WorkspaceFile if err := rows.Scan(&wf.ID, &wf.WorkspaceID, &wf.Path, &wf.IsDirectory, &wf.ContentType, &wf.SizeBytes, &wf.SHA256, &wf.IndexStatus, &wf.ChunkCount, st(&wf.CreatedAt), st(&wf.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan workspace file: %w", err) } out = append(out, wf) } return out, rows.Err() } func (s *ExportStore) UserFiles(ctx context.Context, userID string) ([]models.File, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, channel_id, message_id, user_id, project_id, origin, filename, content_type, size_bytes, storage_key, display_hint, extracted_text, metadata, created_at, updated_at FROM files WHERE user_id = ? ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user files: %w", err) } defer rows.Close() var out []models.File for rows.Next() { var f models.File if err := rows.Scan(&f.ID, &f.ChannelID, &f.MessageID, &f.UserID, &f.ProjectID, &f.Origin, &f.Filename, &f.ContentType, &f.SizeBytes, &f.StorageKey, &f.DisplayHint, &f.ExtractedText, &f.Metadata, st(&f.CreatedAt), st(&f.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan file: %w", err) } out = append(out, f) } return out, rows.Err() } func (s *ExportStore) UserFolders(ctx context.Context, userID string) ([]models.Folder, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, name, parent_id, sort_order, created_at, updated_at FROM folders WHERE user_id = ? ORDER BY sort_order ASC`, userID) if err != nil { return nil, fmt.Errorf("export: user folders: %w", err) } defer rows.Close() var out []models.Folder for rows.Next() { var f models.Folder if err := rows.Scan(&f.ID, &f.UserID, &f.Name, &f.ParentID, &f.SortOrder, st(&f.CreatedAt), st(&f.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan folder: %w", err) } out = append(out, f) } return out, rows.Err() } func (s *ExportStore) UserModelSettings(ctx context.Context, userID string) ([]models.UserModelSetting, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, model_id, provider_config_id, hidden, preferred_temperature, preferred_max_tokens, sort_order, created_at, updated_at FROM user_model_settings WHERE user_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: user model settings: %w", err) } defer rows.Close() var out []models.UserModelSetting for rows.Next() { var ums models.UserModelSetting if err := rows.Scan(&ums.ID, &ums.UserID, &ums.ModelID, &ums.ProviderConfigID, &ums.Hidden, &ums.PreferredTemperature, &ums.PreferredMaxTokens, &ums.SortOrder, st(&ums.CreatedAt), st(&ums.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan user model setting: %w", err) } out = append(out, ums) } return out, rows.Err() } func (s *ExportStore) UserNotifPrefs(ctx context.Context, userID string) ([]models.NotificationPreference, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, type, in_app, email FROM notification_preferences WHERE user_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: notification prefs: %w", err) } defer rows.Close() var out []models.NotificationPreference for rows.Next() { var np models.NotificationPreference if err := rows.Scan(&np.ID, &np.UserID, &np.Type, &np.InApp, &np.Email); err != nil { return nil, fmt.Errorf("export: scan notif pref: %w", err) } out = append(out, np) } return out, rows.Err() } func (s *ExportStore) UserUsageEntries(ctx context.Context, userID string) ([]models.UsageEntry, error) { cutoff := time.Now().AddDate(0, 0, -90).Format(timeFmt) rows, err := DB.QueryContext(ctx, ` SELECT id, channel_id, user_id, provider_config_id, provider_scope, model_id, role, prompt_tokens, completion_tokens, cache_creation_tokens, cache_read_tokens, cost_input, cost_output, routing_decision, created_at FROM usage_log WHERE user_id = ? AND created_at >= ? ORDER BY created_at ASC`, userID, cutoff) if err != nil { return nil, fmt.Errorf("export: usage entries: %w", err) } defer rows.Close() var out []models.UsageEntry for rows.Next() { var ue models.UsageEntry if err := rows.Scan(&ue.ID, &ue.ChannelID, &ue.UserID, &ue.ProviderConfigID, &ue.ProviderScope, &ue.ModelID, &ue.Role, &ue.PromptTokens, &ue.CompletionTokens, &ue.CacheCreationTokens, &ue.CacheReadTokens, &ue.CostInput, &ue.CostOutput, &ue.RoutingDecision, st(&ue.CreatedAt)); err != nil { return nil, fmt.Errorf("export: scan usage entry: %w", err) } out = append(out, ue) } return out, rows.Err() } func (s *ExportStore) UserPersonaGroups(ctx context.Context, userID string) ([]models.PersonaGroup, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, owner_id, scope, team_id, created_at, updated_at FROM persona_groups WHERE owner_id = ? ORDER BY created_at ASC`, userID) if err != nil { return nil, fmt.Errorf("export: persona groups: %w", err) } defer rows.Close() var out []models.PersonaGroup for rows.Next() { var pg models.PersonaGroup if err := rows.Scan(&pg.ID, &pg.Name, &pg.Description, &pg.OwnerID, &pg.Scope, &pg.TeamID, st(&pg.CreatedAt), st(&pg.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan persona group: %w", err) } out = append(out, pg) } return out, rows.Err() } func (s *ExportStore) UserPersonaGroupMembers(ctx context.Context, groupIDs []string) ([]models.PersonaGroupMember, error) { if len(groupIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(groupIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, group_id, persona_id, is_leader, sort_order FROM persona_group_members WHERE group_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: persona group members: %w", err) } defer rows.Close() var out []models.PersonaGroupMember for rows.Next() { var pgm models.PersonaGroupMember if err := rows.Scan(&pgm.ID, &pgm.GroupID, &pgm.PersonaID, &pgm.IsLeader, &pgm.SortOrder); err != nil { return nil, fmt.Errorf("export: scan persona group member: %w", err) } out = append(out, pgm) } return out, rows.Err() } // ── Team-scoped export ─────────────────────── func (s *ExportStore) TeamChannels(ctx context.Context, teamID string) ([]models.Channel, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, title, description, type, model, system_prompt, provider_config_id, is_archived, is_pinned, folder_id, team_id, project_id, workspace_id, settings, allow_anonymous, created_at, updated_at FROM channels WHERE team_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team channels: %w", err) } defer rows.Close() var out []models.Channel for rows.Next() { var ch models.Channel if err := rows.Scan(&ch.ID, &ch.UserID, &ch.Title, &ch.Description, &ch.Type, &ch.Model, &ch.SystemPrompt, &ch.ProviderConfigID, &ch.IsArchived, &ch.IsPinned, &ch.FolderID, &ch.TeamID, &ch.ProjectID, &ch.WorkspaceID, &ch.Settings, &ch.AllowAnonymous, st(&ch.CreatedAt), st(&ch.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan team channel: %w", err) } out = append(out, ch) } return out, rows.Err() } func (s *ExportStore) TeamMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) { rows, err := DB.QueryContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, u.email, u.display_name, u.username, u.role FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ?`, teamID) if err != nil { return nil, fmt.Errorf("export: team members: %w", err) } defer rows.Close() var out []models.TeamMember for rows.Next() { var tm models.TeamMember if err := rows.Scan(&tm.ID, &tm.TeamID, &tm.UserID, &tm.Role, &tm.JoinedAt, &tm.Email, &tm.DisplayName, &tm.Username, &tm.UserRole); err != nil { return nil, fmt.Errorf("export: scan team member: %w", err) } out = append(out, tm) } return out, rows.Err() } func (s *ExportStore) TeamPersonas(ctx context.Context, teamID string) ([]models.Persona, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, handle, description, icon, avatar, base_model_id, provider_config_id, system_prompt, temperature, max_tokens, thinking_budget, top_p, scope, owner_id, created_by, is_active, is_shared, memory_enabled, memory_extraction_prompt, created_at, updated_at FROM personas WHERE scope = 'team' AND owner_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team personas: %w", err) } defer rows.Close() var out []models.Persona for rows.Next() { var p models.Persona if err := rows.Scan(&p.ID, &p.Name, &p.Handle, &p.Description, &p.Icon, &p.Avatar, &p.BaseModelID, &p.ProviderConfigID, &p.SystemPrompt, &p.Temperature, &p.MaxTokens, &p.ThinkingBudget, &p.TopP, &p.Scope, &p.OwnerID, &p.CreatedBy, &p.IsActive, &p.IsShared, &p.MemoryEnabled, &p.MemoryExtractionPrompt, st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan persona: %w", err) } out = append(out, p) } return out, rows.Err() } func (s *ExportStore) TeamPersonaKBs(ctx context.Context, personaIDs []string) ([]models.PersonaKB, error) { if len(personaIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(personaIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT persona_id, kb_id, auto_search, added_at FROM persona_knowledge_bases WHERE persona_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: persona KBs: %w", err) } defer rows.Close() var out []models.PersonaKB for rows.Next() { var pk models.PersonaKB if err := rows.Scan(&pk.PersonaID, &pk.KBID, &pk.AutoSearch, st(&pk.AddedAt)); err != nil { return nil, fmt.Errorf("export: scan persona KB: %w", err) } out = append(out, pk) } return out, rows.Err() } func (s *ExportStore) TeamKnowledgeBases(ctx context.Context, teamID string) ([]models.KnowledgeBase, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, scope, owner_id, team_id, embedding_config, document_count, chunk_count, total_bytes, discoverable, status, created_at, updated_at FROM knowledge_bases WHERE team_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team KBs: %w", err) } defer rows.Close() var out []models.KnowledgeBase for rows.Next() { var kb models.KnowledgeBase if err := rows.Scan(&kb.ID, &kb.Name, &kb.Description, &kb.Scope, &kb.OwnerID, &kb.TeamID, &kb.EmbeddingConfig, &kb.DocumentCount, &kb.ChunkCount, &kb.TotalBytes, &kb.Discoverable, &kb.Status, st(&kb.CreatedAt), st(&kb.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan KB: %w", err) } out = append(out, kb) } return out, rows.Err() } func (s *ExportStore) TeamKBDocuments(ctx context.Context, kbIDs []string) ([]models.KBDocument, error) { if len(kbIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(kbIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, kb_id, filename, content_type, size_bytes, storage_key, chunk_count, status, error, uploaded_by, created_at, updated_at FROM kb_documents WHERE kb_id IN (%s) ORDER BY created_at ASC`, ph), args...) if err != nil { return nil, fmt.Errorf("export: KB documents: %w", err) } defer rows.Close() var out []models.KBDocument for rows.Next() { var d models.KBDocument if err := rows.Scan(&d.ID, &d.KBID, &d.Filename, &d.ContentType, &d.SizeBytes, &d.StorageKey, &d.ChunkCount, &d.Status, &d.Error, &d.UploadedBy, st(&d.CreatedAt), st(&d.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan KB document: %w", err) } out = append(out, d) } return out, rows.Err() } func (s *ExportStore) TeamWorkflows(ctx context.Context, teamID string) ([]models.Workflow, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, team_id, name, slug, description, branding, entry_mode, is_active, version, on_complete, retention, webhook_url, webhook_secret, created_by, created_at, updated_at FROM workflows WHERE team_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team workflows: %w", err) } defer rows.Close() var out []models.Workflow for rows.Next() { var w models.Workflow if err := rows.Scan(&w.ID, &w.TeamID, &w.Name, &w.Slug, &w.Description, &w.Branding, &w.EntryMode, &w.IsActive, &w.Version, &w.OnComplete, &w.Retention, &w.WebhookURL, &w.WebhookSecret, &w.CreatedBy, st(&w.CreatedAt), st(&w.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan workflow: %w", err) } out = append(out, w) } return out, rows.Err() } func (s *ExportStore) TeamWorkflowVersions(ctx context.Context, workflowIDs []string) ([]models.WorkflowVersion, error) { if len(workflowIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(workflowIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, workflow_id, version_number, snapshot, created_at FROM workflow_versions WHERE workflow_id IN (%s) ORDER BY version_number ASC`, ph), args...) if err != nil { return nil, fmt.Errorf("export: workflow versions: %w", err) } defer rows.Close() var out []models.WorkflowVersion for rows.Next() { var wv models.WorkflowVersion if err := rows.Scan(&wv.ID, &wv.WorkflowID, &wv.VersionNumber, &wv.Snapshot, st(&wv.CreatedAt)); err != nil { return nil, fmt.Errorf("export: scan workflow version: %w", err) } out = append(out, wv) } return out, rows.Err() } func (s *ExportStore) TeamWorkflowStages(ctx context.Context, workflowIDs []string) ([]models.WorkflowStage, error) { if len(workflowIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(workflowIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, workflow_id, ordinal, name, persona_id, assignment_team_id, form_template, stage_mode, history_mode, auto_transition, transition_rules, surface_pkg_id, created_at FROM workflow_stages WHERE workflow_id IN (%s) ORDER BY ordinal ASC`, ph), args...) if err != nil { return nil, fmt.Errorf("export: workflow stages: %w", err) } defer rows.Close() var out []models.WorkflowStage for rows.Next() { var ws models.WorkflowStage if err := rows.Scan(&ws.ID, &ws.WorkflowID, &ws.Ordinal, &ws.Name, &ws.PersonaID, &ws.AssignmentTeamID, &ws.FormTemplate, &ws.StageMode, &ws.HistoryMode, &ws.AutoTransition, &ws.TransitionRules, &ws.SurfacePkgID, st(&ws.CreatedAt)); err != nil { return nil, fmt.Errorf("export: scan workflow stage: %w", err) } out = append(out, ws) } return out, rows.Err() } func (s *ExportStore) TeamGroups(ctx context.Context, teamID string) ([]models.Group, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, scope, team_id, created_by, source, permissions, token_budget_daily, token_budget_monthly, allowed_models, created_at, updated_at FROM groups WHERE team_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team groups: %w", err) } defer rows.Close() var out []models.Group for rows.Next() { var g models.Group var permJSON, modelsJSON string if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Scope, &g.TeamID, &g.CreatedBy, &g.Source, &permJSON, &g.TokenBudgetDaily, &g.TokenBudgetMonthly, &modelsJSON, st(&g.CreatedAt), st(&g.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan group: %w", err) } g.Permissions = ScanArray(permJSON) g.AllowedModels = ScanArray(modelsJSON) out = append(out, g) } return out, rows.Err() } func (s *ExportStore) TeamGroupMembers(ctx context.Context, groupIDs []string) ([]models.GroupMember, error) { if len(groupIDs) == 0 { return nil, nil } ph, args := sqlitePlaceholders(groupIDs) rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT id, group_id, user_id, added_by, added_at FROM group_members WHERE group_id IN (%s)`, ph), args...) if err != nil { return nil, fmt.Errorf("export: group members: %w", err) } defer rows.Close() var out []models.GroupMember for rows.Next() { var gm models.GroupMember if err := rows.Scan(&gm.ID, &gm.GroupID, &gm.UserID, &gm.AddedBy, st(&gm.AddedAt)); err != nil { return nil, fmt.Errorf("export: scan group member: %w", err) } out = append(out, gm) } return out, rows.Err() } func (s *ExportStore) TeamResourceGrants(ctx context.Context, teamID string) ([]models.ResourceGrant, error) { rows, err := DB.QueryContext(ctx, ` SELECT rg.id, rg.resource_type, rg.resource_id, rg.grant_scope, rg.granted_groups, rg.created_by, rg.created_at, rg.updated_at FROM resource_grants rg WHERE rg.resource_id IN ( SELECT id FROM personas WHERE scope = 'team' AND owner_id = ? UNION ALL SELECT id FROM knowledge_bases WHERE team_id = ? )`, teamID, teamID) if err != nil { return nil, fmt.Errorf("export: team resource grants: %w", err) } defer rows.Close() var out []models.ResourceGrant for rows.Next() { var rg models.ResourceGrant var groupsJSON string if err := rows.Scan(&rg.ID, &rg.ResourceType, &rg.ResourceID, &rg.GrantScope, &groupsJSON, &rg.CreatedBy, st(&rg.CreatedAt), st(&rg.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan resource grant: %w", err) } rg.GrantedGroups = ScanArray(groupsJSON) out = append(out, rg) } return out, rows.Err() } func (s *ExportStore) TeamProjects(ctx context.Context, teamID string) ([]models.Project, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, name, description, color, icon, scope, owner_id, team_id, is_archived, workspace_id, settings, created_at, updated_at FROM projects WHERE team_id = ? ORDER BY created_at ASC`, teamID) if err != nil { return nil, fmt.Errorf("export: team projects: %w", err) } defer rows.Close() var out []models.Project for rows.Next() { var p models.Project if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Color, &p.Icon, &p.Scope, &p.OwnerID, &p.TeamID, &p.IsArchived, &p.WorkspaceID, &p.Settings, st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil { return nil, fmt.Errorf("export: scan project: %w", err) } out = append(out, p) } return out, rows.Err() } // ── GDPR ───────────────────────────────────── func (s *ExportStore) CountActiveAdmins(ctx context.Context) (int, error) { var count int err := DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin' AND is_active = 1`).Scan(&count) return count, err } func (s *ExportStore) AnonymizeUser(ctx context.Context, userID string, anonHash string) error { _, err := DB.ExecContext(ctx, ` UPDATE users SET username = ?, email = ?, display_name = 'Deleted User', avatar_url = '', handle = ?, settings = NULL, external_id = NULL, is_active = 0, password_hash = '', updated_at = datetime('now') WHERE id = ?`, "deleted-user-"+anonHash, "deleted-"+anonHash+"@deleted.local", "deleted-"+anonHash, userID, ) return err } func (s *ExportStore) SoftDeleteUserData(ctx context.Context, userID string) (map[string]int64, error) { counts := make(map[string]int64) tx, err := DB.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("export: begin tx: %w", err) } defer tx.Rollback() now := time.Now().UTC().Format(timeFmt) // Messages in user-owned channels r, err := tx.ExecContext(ctx, ` UPDATE messages SET deleted_at = ? WHERE channel_id IN (SELECT id FROM channels WHERE user_id = ?) AND deleted_at IS NULL`, now, userID) if err != nil { return nil, fmt.Errorf("export: soft-delete owned messages: %w", err) } n, _ := r.RowsAffected() counts["messages_owned"] = n // Messages sent by user in other channels r, err = tx.ExecContext(ctx, ` UPDATE messages SET deleted_at = ? WHERE participant_type = 'user' AND participant_id = ? AND deleted_at IS NULL`, now, userID) if err != nil { return nil, fmt.Errorf("export: soft-delete sent messages: %w", err) } n, _ = r.RowsAffected() counts["messages_sent"] = n // Archive channels r, err = tx.ExecContext(ctx, `UPDATE channels SET is_archived = 1 WHERE user_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: archive channels: %w", err) } n, _ = r.RowsAffected() counts["channels"] = n // Notes + links tx.ExecContext(ctx, `DELETE FROM note_links WHERE source_note_id IN (SELECT id FROM notes WHERE user_id = ?)`, userID) r, err = tx.ExecContext(ctx, `DELETE FROM notes WHERE user_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: delete notes: %w", err) } n, _ = r.RowsAffected() counts["notes"] = n // Memories r, err = tx.ExecContext(ctx, `DELETE FROM memories WHERE owner_id = ? AND scope = 'user'`, userID) if err != nil { return nil, fmt.Errorf("export: delete memories: %w", err) } n, _ = r.RowsAffected() counts["memories"] = n // Workspace cascade tx.ExecContext(ctx, `DELETE FROM workspace_chunks WHERE workspace_id IN (SELECT id FROM workspaces WHERE owner_type = 'user' AND owner_id = ?)`, userID) tx.ExecContext(ctx, `DELETE FROM workspace_files WHERE workspace_id IN (SELECT id FROM workspaces WHERE owner_type = 'user' AND owner_id = ?)`, userID) r, err = tx.ExecContext(ctx, `DELETE FROM workspaces WHERE owner_type = 'user' AND owner_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: delete workspaces: %w", err) } n, _ = r.RowsAffected() counts["workspaces"] = n // Files r, err = tx.ExecContext(ctx, `DELETE FROM files WHERE user_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: delete files: %w", err) } n, _ = r.RowsAffected() counts["files"] = n // Projects cascade tx.ExecContext(ctx, `DELETE FROM project_notes WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID) tx.ExecContext(ctx, `DELETE FROM project_knowledge_bases WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID) tx.ExecContext(ctx, `DELETE FROM project_channels WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID) r, err = tx.ExecContext(ctx, `DELETE FROM projects WHERE scope = 'personal' AND owner_id = ?`, userID) if err != nil { return nil, fmt.Errorf("export: delete projects: %w", err) } n, _ = r.RowsAffected() counts["projects"] = n // Folders, settings, prefs, notifications tx.ExecContext(ctx, `DELETE FROM folders WHERE user_id = ?`, userID) tx.ExecContext(ctx, `DELETE FROM user_model_settings WHERE user_id = ?`, userID) tx.ExecContext(ctx, `DELETE FROM notification_preferences WHERE user_id = ?`, userID) tx.ExecContext(ctx, `DELETE FROM notifications WHERE user_id = ?`, userID) // Persona groups tx.ExecContext(ctx, `DELETE FROM persona_group_members WHERE group_id IN (SELECT id FROM persona_groups WHERE owner_id = ?)`, userID) tx.ExecContext(ctx, `DELETE FROM persona_groups WHERE owner_id = ?`, userID) // Channel participation tx.ExecContext(ctx, `DELETE FROM channel_participants WHERE participant_type = 'user' AND participant_id = ?`, userID) tx.ExecContext(ctx, `DELETE FROM channel_cursors WHERE user_id = ?`, userID) // Memory extraction log, presence tx.ExecContext(ctx, `DELETE FROM memory_extraction_log WHERE user_id = ?`, userID) tx.ExecContext(ctx, `DELETE FROM user_presence WHERE user_id = ?`, userID) if err := tx.Commit(); err != nil { return nil, fmt.Errorf("export: commit: %w", err) } return counts, nil } func (s *ExportStore) DeleteUserTokens(ctx context.Context, userID string) error { _, err := DB.ExecContext(ctx, `DELETE FROM refresh_tokens WHERE user_id = ?`, userID) if err != nil { return fmt.Errorf("export: delete refresh tokens: %w", err) } _, err = DB.ExecContext(ctx, `DELETE FROM ws_tickets WHERE user_id = ?`, userID) if err != nil { return fmt.Errorf("export: delete ws tickets: %w", err) } return nil } // ── Import (CS1) ───────────────────────────── func (s *ExportStore) ImportFolders(ctx context.Context, folders []models.Folder) (int, int, error) { imported, skipped := 0, 0 for _, f := range folders { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO folders (id, user_id, name, parent_id, sort_order, created_at, updated_at) VALUES (?,?,?,?,?,?,?)`, f.ID, f.UserID, f.Name, f.ParentID, f.SortOrder, f.CreatedAt.Format(timeFmt), f.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import folder %s: %w", f.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportProjects(ctx context.Context, projects []models.Project) (int, int, error) { imported, skipped := 0, 0 for _, p := range projects { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO projects (id, name, description, color, icon, scope, owner_id, team_id, is_archived, workspace_id, settings, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, p.ID, p.Name, p.Description, p.Color, p.Icon, p.Scope, p.OwnerID, p.TeamID, p.IsArchived, p.WorkspaceID, p.Settings, p.CreatedAt.Format(timeFmt), p.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import project %s: %w", p.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportWorkspaces(ctx context.Context, workspaces []models.Workspace) (int, int, error) { imported, skipped := 0, 0 for _, w := range workspaces { var gitLastSync interface{} if w.GitLastSync != nil { t := w.GitLastSync.Format(timeFmt) gitLastSync = t } r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO workspaces (id, owner_type, owner_id, name, root_path, max_bytes, status, indexing_enabled, git_remote_url, git_branch, git_credential_id, git_last_sync, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, w.ID, w.OwnerType, w.OwnerID, w.Name, w.RootPath, w.MaxBytes, w.Status, w.IndexingEnabled, w.GitRemoteURL, w.GitBranch, w.GitCredentialID, gitLastSync, w.CreatedAt.Format(timeFmt), w.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import workspace %s: %w", w.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportChannels(ctx context.Context, channels []models.Channel) (int, int, error) { imported, skipped := 0, 0 for _, ch := range channels { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO channels (id, user_id, title, description, type, model, system_prompt, provider_config_id, is_archived, is_pinned, folder_id, team_id, project_id, workspace_id, settings, allow_anonymous, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, ch.ID, ch.UserID, ch.Title, ch.Description, ch.Type, ch.Model, ch.SystemPrompt, ch.ProviderConfigID, ch.IsArchived, ch.IsPinned, ch.FolderID, ch.TeamID, ch.ProjectID, ch.WorkspaceID, ch.Settings, ch.AllowAnonymous, ch.CreatedAt.Format(timeFmt), ch.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import channel %s: %w", ch.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportChannelParticipants(ctx context.Context, cps []models.ChannelParticipant) (int, int, error) { imported, skipped := 0, 0 for _, cp := range cps { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO channel_participants (id, channel_id, participant_type, participant_id, role, display_name, avatar_url, joined_at, last_read_at) VALUES (?,?,?,?,?,?,?,?,?)`, cp.ID, cp.ChannelID, cp.ParticipantType, cp.ParticipantID, cp.Role, cp.DisplayName, cp.AvatarURL, cp.JoinedAt.Format(timeFmt), cp.LastReadAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import participant %s: %w", cp.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportChannelModels(ctx context.Context, cms []models.ChannelModel) (int, int, error) { imported, skipped := 0, 0 for _, cm := range cms { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO channel_models (id, channel_id, model_id, provider_config_id, persona_id, handle, display_name, system_prompt, is_default) VALUES (?,?,?,?,?,?,?,?,?)`, cm.ID, cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.PersonaID, cm.Handle, cm.DisplayName, cm.SystemPrompt, cm.IsDefault) if err != nil { return imported, skipped, fmt.Errorf("import channel model %s: %w", cm.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportChannelCursors(ctx context.Context, cursors []models.ChannelCursor) (int, int, error) { imported, skipped := 0, 0 for _, cc := range cursors { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO channel_cursors (id, channel_id, user_id, active_leaf_id, updated_at) VALUES (?,?,?,?,?)`, cc.ID, cc.ChannelID, cc.UserID, cc.ActiveLeafID, cc.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import cursor %s: %w", cc.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportMessages(ctx context.Context, msgs []models.Message) (int, int, error) { imported, skipped := 0, 0 for _, m := range msgs { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO messages (id, channel_id, role, content, model, tokens_used, tool_calls, metadata, parent_id, sibling_index, participant_type, participant_id, provider_config_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, m.ID, m.ChannelID, m.Role, m.Content, m.Model, m.TokensUsed, ToJSON(m.ToolCalls), ToJSON(m.Metadata), m.ParentID, m.SiblingIndex, m.ParticipantType, m.ParticipantID, m.ProviderConfigID, m.CreatedAt.Format(timeFmt), m.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import message %s: %w", m.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportNotes(ctx context.Context, notes []models.Note) (int, int, error) { imported, skipped := 0, 0 for _, n := range notes { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO notes (id, user_id, title, content, folder_path, tags, metadata, source_channel_id, source_message_id, team_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, n.ID, n.UserID, n.Title, n.Content, n.FolderPath, ArrayToJSON(n.Tags), ToJSON(n.Metadata), models.NullString(n.SourceChannelID), models.NullString(n.SourceMessageID), models.NullString(n.TeamID), n.CreatedAt.Format(timeFmt), n.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import note %s: %w", n.ID, err) } nr, _ := r.RowsAffected() if nr > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportNoteLinks(ctx context.Context, links []models.ExportNoteLink) (int, int, error) { imported, skipped := 0, 0 for _, nl := range links { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO note_links (source_note_id, target_note_id, target_title, display_text, is_transclusion) VALUES (?,?,?,?,?)`, nl.SourceNoteID, nl.TargetNoteID, nl.TargetTitle, nl.DisplayText, nl.IsTransclusion) if err != nil { return imported, skipped, fmt.Errorf("import note link: %w", err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportMemories(ctx context.Context, mems []models.Memory) (int, int, error) { imported, skipped := 0, 0 for _, m := range mems { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO memories (id, scope, owner_id, user_id, key, value, source_channel_id, confidence, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, m.ID, m.Scope, m.OwnerID, m.UserID, m.Key, m.Value, m.SourceChannelID, m.Confidence, m.Status, m.CreatedAt.Format(timeFmt), m.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import memory %s: %w", m.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportProjectChannels(ctx context.Context, pcs []models.ProjectChannel) (int, int, error) { imported, skipped := 0, 0 for _, pc := range pcs { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO project_channels (project_id, channel_id, position, folder, added_at) VALUES (?,?,?,?,?)`, pc.ProjectID, pc.ChannelID, pc.Position, pc.Folder, pc.AddedAt) if err != nil { return imported, skipped, fmt.Errorf("import project channel: %w", err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportProjectKBs(ctx context.Context, pks []models.ProjectKB) (int, int, error) { imported, skipped := 0, 0 for _, pk := range pks { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO project_knowledge_bases (project_id, kb_id, auto_search, added_at) VALUES (?,?,?,?)`, pk.ProjectID, pk.KBID, pk.AutoSearch, pk.AddedAt) if err != nil { return imported, skipped, fmt.Errorf("import project KB: %w", err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportProjectNotes(ctx context.Context, pns []models.ProjectNote) (int, int, error) { imported, skipped := 0, 0 for _, pn := range pns { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO project_notes (project_id, note_id, added_at) VALUES (?,?,?)`, pn.ProjectID, pn.NoteID, pn.AddedAt) if err != nil { return imported, skipped, fmt.Errorf("import project note: %w", err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportWorkspaceFiles(ctx context.Context, wfs []models.WorkspaceFile) (int, int, error) { imported, skipped := 0, 0 for _, wf := range wfs { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO workspace_files (id, workspace_id, path, is_directory, content_type, size_bytes, sha256, index_status, chunk_count, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, wf.ID, wf.WorkspaceID, wf.Path, wf.IsDirectory, wf.ContentType, wf.SizeBytes, wf.SHA256, wf.IndexStatus, wf.ChunkCount, wf.CreatedAt.Format(timeFmt), wf.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import workspace file %s: %w", wf.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportFiles(ctx context.Context, files []models.File) (int, int, error) { imported, skipped := 0, 0 for _, f := range files { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO files (id, channel_id, message_id, user_id, project_id, origin, filename, content_type, size_bytes, storage_key, display_hint, extracted_text, metadata, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, f.ID, f.ChannelID, f.MessageID, f.UserID, f.ProjectID, f.Origin, f.Filename, f.ContentType, f.SizeBytes, f.StorageKey, f.DisplayHint, f.ExtractedText, f.Metadata, f.CreatedAt.Format(timeFmt), f.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import file %s: %w", f.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportUserModelSettings(ctx context.Context, ums []models.UserModelSetting) (int, int, error) { imported, skipped := 0, 0 for _, um := range ums { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO user_model_settings (id, user_id, model_id, provider_config_id, hidden, preferred_temperature, preferred_max_tokens, sort_order, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)`, um.ID, um.UserID, um.ModelID, um.ProviderConfigID, um.Hidden, um.PreferredTemperature, um.PreferredMaxTokens, um.SortOrder, um.CreatedAt.Format(timeFmt), um.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import user model setting %s: %w", um.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportNotifPrefs(ctx context.Context, nps []models.NotificationPreference) (int, int, error) { imported, skipped := 0, 0 for _, np := range nps { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO notification_preferences (id, user_id, type, in_app, email) VALUES (?,?,?,?,?)`, np.ID, np.UserID, np.Type, np.InApp, np.Email) if err != nil { return imported, skipped, fmt.Errorf("import notif pref %s: %w", np.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportPersonaGroups(ctx context.Context, pgs []models.PersonaGroup) (int, int, error) { imported, skipped := 0, 0 for _, pg := range pgs { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO persona_groups (id, name, description, owner_id, scope, team_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`, pg.ID, pg.Name, pg.Description, pg.OwnerID, pg.Scope, pg.TeamID, pg.CreatedAt.Format(timeFmt), pg.UpdatedAt.Format(timeFmt)) if err != nil { return imported, skipped, fmt.Errorf("import persona group %s: %w", pg.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil } func (s *ExportStore) ImportPersonaGroupMembers(ctx context.Context, pgms []models.PersonaGroupMember) (int, int, error) { imported, skipped := 0, 0 for _, pgm := range pgms { r, err := DB.ExecContext(ctx, ` INSERT OR IGNORE INTO persona_group_members (id, group_id, persona_id, is_leader, sort_order) VALUES (?,?,?,?,?)`, pgm.ID, pgm.GroupID, pgm.PersonaID, pgm.IsLeader, pgm.SortOrder) if err != nil { return imported, skipped, fmt.Errorf("import persona group member %s: %w", pgm.ID, err) } n, _ := r.RowsAffected() if n > 0 { imported++ } else { skipped++ } } return imported, skipped, nil }