@@ -22,6 +22,7 @@ type adminRow struct {
|
||||
type DatabaseStatus struct {
|
||||
ActiveProvider string `json:"activeProvider"`
|
||||
ConfigProvider string `json:"configProvider"`
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
SQLiteReady bool `json:"sqliteReady"`
|
||||
RemoteReady bool `json:"remoteReady"`
|
||||
FailoverActive bool `json:"failoverActive"`
|
||||
|
||||
@@ -22,6 +22,9 @@ func (s *Store) migrate(conn *sql.DB, d dialect) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := createSchemaIndexes(conn, d); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.recordSchemaVersion(conn, d)
|
||||
}
|
||||
|
||||
@@ -281,21 +284,67 @@ func schemaStatements(d dialect) []string {
|
||||
created_at %s NOT NULL,
|
||||
finished_at %s NOT NULL DEFAULT ''
|
||||
)`, d.idType(), keyText, keyText, keyText, longText, shortText, shortText, shortText),
|
||||
`CREATE INDEX IF NOT EXISTS idx_feedback_tickets_activity ON feedback_tickets(last_activity_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_feedback_comments_code ON feedback_comments(feedback_code)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_feedback_attachments_code ON feedback_attachments(feedback_code)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_feedback_events_code ON feedback_events(feedback_code)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mail_records_code ON mail_records(feedback_code)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_endpoint_call_logs_source ON endpoint_call_logs(source_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_logs_type ON audit_logs(type)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_logs_target ON audit_logs(target)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_legacy_json_revisions_name ON legacy_json_revisions(name, id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_release_notices_version ON release_notices(version)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_release_notice_revisions_version ON release_notice_revisions(version, id)`,
|
||||
}
|
||||
}
|
||||
|
||||
type schemaIndex struct {
|
||||
name string
|
||||
table string
|
||||
columns string
|
||||
}
|
||||
|
||||
func schemaIndexes() []schemaIndex {
|
||||
return []schemaIndex{
|
||||
{name: "idx_feedback_tickets_activity", table: "feedback_tickets", columns: "last_activity_at"},
|
||||
{name: "idx_feedback_comments_code", table: "feedback_comments", columns: "feedback_code"},
|
||||
{name: "idx_feedback_attachments_code", table: "feedback_attachments", columns: "feedback_code"},
|
||||
{name: "idx_feedback_events_code", table: "feedback_events", columns: "feedback_code"},
|
||||
{name: "idx_mail_records_code", table: "mail_records", columns: "feedback_code"},
|
||||
{name: "idx_endpoint_call_logs_source", table: "endpoint_call_logs", columns: "source_id"},
|
||||
{name: "idx_audit_logs_created", table: "audit_logs", columns: "created_at"},
|
||||
{name: "idx_audit_logs_type", table: "audit_logs", columns: "type"},
|
||||
{name: "idx_audit_logs_target", table: "audit_logs", columns: "target"},
|
||||
{name: "idx_legacy_json_revisions_name", table: "legacy_json_revisions", columns: "name, id"},
|
||||
{name: "idx_release_notices_version", table: "release_notices", columns: "version"},
|
||||
{name: "idx_release_notice_revisions_version", table: "release_notice_revisions", columns: "version, id"},
|
||||
}
|
||||
}
|
||||
|
||||
func createSchemaIndexes(conn *sql.DB, d dialect) error {
|
||||
for _, index := range schemaIndexes() {
|
||||
if d.name == "mysql" {
|
||||
exists, err := mysqlIndexExists(conn, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, err := conn.Exec(d.rebind(createIndexStatement(d, index))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mysqlIndexExists(conn *sql.DB, index schemaIndex) (bool, error) {
|
||||
var count int
|
||||
err := conn.QueryRow(`SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = ?
|
||||
AND index_name = ?`, index.table, index.name).Scan(&count)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func createIndexStatement(d dialect, index schemaIndex) string {
|
||||
if d.name == "mysql" {
|
||||
return fmt.Sprintf("CREATE INDEX %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns)
|
||||
}
|
||||
return fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns)
|
||||
}
|
||||
|
||||
func (s *Store) recordSchemaVersion(conn *sql.DB, d dialect) error {
|
||||
columns := []string{"version", "applied_at", "description"}
|
||||
_, err := conn.Exec(d.rebind(d.upsert("schema_migrations", columns, []string{"version"})),
|
||||
|
||||
@@ -66,6 +66,7 @@ func Open(cfg *config.Config) (*Store, error) {
|
||||
status: DatabaseStatus{
|
||||
ActiveProvider: "sqlite",
|
||||
ConfigProvider: cfg.Database.Provider,
|
||||
SchemaVersion: CurrentSchemaVersion,
|
||||
SQLiteReady: true,
|
||||
LastRecoveredAt: Now(),
|
||||
},
|
||||
@@ -165,6 +166,7 @@ func (s *Store) ReconfigureDatabase(cfg *config.Config) error {
|
||||
s.remoteDB = remote
|
||||
s.remoteDialect = remoteDialect
|
||||
s.status.ConfigProvider = cfg.Database.Provider
|
||||
s.status.SchemaVersion = CurrentSchemaVersion
|
||||
s.status.SQLiteReady = true
|
||||
s.status.RemoteReady = remote != nil
|
||||
s.status.LastError = ""
|
||||
@@ -269,6 +271,7 @@ func (s *Store) openRemote() error {
|
||||
s.dialect = remoteDialect
|
||||
s.status.ActiveProvider = "mysql"
|
||||
s.status.ConfigProvider = "mysql"
|
||||
s.status.SchemaVersion = CurrentSchemaVersion
|
||||
s.status.RemoteReady = true
|
||||
s.status.FailoverActive = false
|
||||
s.status.LastError = ""
|
||||
@@ -304,6 +307,7 @@ func (s *Store) checkRemote() {
|
||||
}
|
||||
s.status.ActiveProvider = "mysql"
|
||||
s.status.RemoteReady = true
|
||||
s.status.SchemaVersion = CurrentSchemaVersion
|
||||
s.status.FailoverActive = false
|
||||
s.status.LastError = ""
|
||||
s.status.LastRecoveredAt = Now()
|
||||
@@ -320,6 +324,7 @@ func (s *Store) markFailover(err error) {
|
||||
s.dialect = s.localDialect
|
||||
s.status.ActiveProvider = "sqlite"
|
||||
s.status.ConfigProvider = s.cfg.Database.Provider
|
||||
s.status.SchemaVersion = CurrentSchemaVersion
|
||||
s.status.RemoteReady = false
|
||||
s.status.FailoverActive = !strings.EqualFold(s.cfg.Database.Provider, "sqlite")
|
||||
s.status.LastError = err.Error()
|
||||
|
||||
@@ -138,6 +138,51 @@ func TestOpenRecordsCurrentSchemaVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaIndexStatementsAreDialectAware(t *testing.T) {
|
||||
mysql := dialectFor("mysql")
|
||||
sqlite := dialectFor("sqlite")
|
||||
for _, statement := range schemaStatements(mysql) {
|
||||
if strings.Contains(strings.ToUpper(statement), "CREATE INDEX IF NOT EXISTS") {
|
||||
t.Fatalf("mysql schema statement contains unsupported index syntax: %s", statement)
|
||||
}
|
||||
}
|
||||
|
||||
index := schemaIndexes()[0]
|
||||
mysqlStatement := createIndexStatement(mysql, index)
|
||||
if strings.Contains(strings.ToUpper(mysqlStatement), "IF NOT EXISTS") {
|
||||
t.Fatalf("mysql index statement contains unsupported syntax: %s", mysqlStatement)
|
||||
}
|
||||
if !strings.Contains(createIndexStatement(sqlite, index), "IF NOT EXISTS") {
|
||||
t.Fatalf("sqlite index statement should remain idempotent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateCreatesSQLiteIndexesIdempotently(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
conn, err := sql.Open("sqlite", filepath.Join(root, "indexes.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
store := &Store{}
|
||||
d := dialectFor("sqlite")
|
||||
if err := store.migrate(conn, d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.migrate(conn, d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var name string
|
||||
if err := conn.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`, "idx_feedback_tickets_activity").Scan(&name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name != "idx_feedback_tickets_activity" {
|
||||
t.Fatalf("unexpected index name %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeAdminPasswordPersistsWhenRemoteSyncFails(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "unified.sqlite")
|
||||
|
||||
Reference in New Issue
Block a user