package db import ( "database/sql" "fmt" "strings" ) type SyncResult struct { Direction string `json:"direction"` Tables map[string]int `json:"tables"` FinishedAt string `json:"finishedAt"` } type tableSpec struct { Name string Columns []string Conflict []string } var syncTables = []tableSpec{ {"feedbacks", []string{"code", "received_at", "title", "type", "severity", "category", "priority", "contact", "body", "status", "status_detail", "note", "public_reply", "handled_by", "assignee", "due_at", "resolved_at", "archived_at", "sla_level", "source_channel", "risk_score", "resolution", "package_path", "encrypted_package_path", "package_sha256", "plain_package_sha256", "remote_addr", "summary_text", "included_files", "mail_sent", "updated_at", "last_activity_at"}, []string{"code"}}, {"mail_records", []string{"id", "feedback_code", "kind", "status", "to_address", "subject", "plain_body", "html_body", "attachment_path", "attachment_name", "error_message", "created_at", "sent_at"}, []string{"id"}}, {"feedback_events", []string{"id", "feedback_code", "event_type", "actor", "from_value", "to_value", "message", "created_at"}, []string{"id"}}, {"feedback_comments", []string{"id", "feedback_code", "author", "body", "internal", "created_at"}, []string{"id"}}, {"feedback_tags", []string{"feedback_code", "tag", "created_at"}, []string{"feedback_code", "tag"}}, {"audit_logs", []string{"id", "actor", "type", "target", "message", "ip", "user_agent", "created_at"}, []string{"id"}}, {"webhook_deliveries", []string{"id", "webhook_name", "event", "status", "attempts", "response_code", "error_message", "payload_sha256", "created_at", "finished_at"}, []string{"id"}}, } func (s *Store) ImportSQLiteToRemote() (SyncResult, error) { s.mu.RLock() remote := s.remoteDB remoteDialect := s.remoteDialect local := s.localDB localDialect := s.localDialect s.mu.RUnlock() if remote == nil { return SyncResult{}, fmt.Errorf("remote database is not configured") } result, err := copyAllTables(local, localDialect, remote, remoteDialect, "sqlite_to_remote") s.setSyncStatus(result, err) return result, err } func (s *Store) SyncNow() (SyncResult, error) { return s.syncRemoteToSQLite() } func (s *Store) syncRemoteToSQLite() (SyncResult, error) { s.mu.RLock() remote := s.remoteDB remoteDialect := s.remoteDialect local := s.localDB localDialect := s.localDialect s.mu.RUnlock() if remote == nil { return SyncResult{}, nil } result, err := copyAllTables(remote, remoteDialect, local, localDialect, "remote_to_sqlite") s.setSyncStatus(result, err) return result, err } func (s *Store) setSyncStatus(result SyncResult, err error) { s.mu.Lock() defer s.mu.Unlock() if err != nil { s.status.LastSyncError = err.Error() return } if result.FinishedAt != "" { s.status.LastSyncAt = result.FinishedAt } s.status.LastSyncError = "" } func copyAllTables(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, direction string) (SyncResult, error) { result := SyncResult{Direction: direction, Tables: map[string]int{}, FinishedAt: Now()} for _, table := range syncTables { copied, err := copyTable(src, srcDialect, dst, dstDialect, table) if err != nil { return result, err } result.Tables[table.Name] = copied } return result, nil } func copyTable(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, spec tableSpec) (int, error) { selectSQL := "SELECT " + strings.Join(spec.Columns, ", ") + " FROM " + spec.Name rows, err := src.Query(srcDialect.rebind(selectSQL)) if err != nil { return 0, err } defer rows.Close() insertSQL := dstDialect.rebind(dstDialect.upsert(spec.Name, spec.Columns, spec.Conflict)) count := 0 for rows.Next() { values := make([]any, len(spec.Columns)) ptrs := make([]any, len(spec.Columns)) for index := range values { ptrs[index] = &values[index] } if err := rows.Scan(ptrs...); err != nil { return count, err } for index, value := range values { if bytes, ok := value.([]byte); ok { values[index] = string(bytes) } } if _, err := dst.Exec(insertSQL, values...); err != nil { return count, err } count++ } return count, rows.Err() }