package rules import ( "fmt" "log" "strconv" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseService handles rule database operations // Converted from C++ WorldDatabase rule functions type DatabaseService struct { db *sqlite.Conn } // NewDatabaseService creates a new database service instance func NewDatabaseService(db *sqlite.Conn) *DatabaseService { return &DatabaseService{ db: db, } } // LoadGlobalRuleSet loads the global rule set from database // Converted from C++ WorldDatabase::LoadGlobalRuleSet() func (ds *DatabaseService) LoadGlobalRuleSet(ruleManager *RuleManager) error { if ds.db == nil { return fmt.Errorf("database not initialized") } ruleSetID := int32(0) // Get the default ruleset ID from variables table query := "SELECT variable_value FROM variables WHERE variable_name = ?" stmt := ds.db.Prep(query) stmt.BindText(1, DefaultRuleSetIDVar) hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("error querying default ruleset ID: %v", err) } if !hasRow { log.Printf("[Rules] Variables table is missing %s variable name, using code-default rules", DefaultRuleSetIDVar) return nil } variableValue := stmt.ColumnText(0) if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { ruleSetID = int32(id) log.Printf("[Rules] Loading Global Ruleset id %d", ruleSetID) } else { return fmt.Errorf("invalid ruleset ID format: %s", variableValue) } if ruleSetID > 0 { if !ruleManager.SetGlobalRuleSet(ruleSetID) { return fmt.Errorf("error loading global rule set - rule set with ID %d does not exist", ruleSetID) } } ruleManager.stats.IncrementDatabaseOperations() return nil } // LoadRuleSets loads all rule sets from database // Converted from C++ WorldDatabase::LoadRuleSets() func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if reload { ruleManager.Flush(true) } // First load the coded defaults into the global rule set ruleManager.LoadCodedDefaultsIntoRuleSet(ruleManager.GetGlobalRuleSet()) // Load active rule sets from database query := "SELECT ruleset_id, ruleset_name FROM rulesets WHERE ruleset_active > 0" loadedCount := 0 stmt := ds.db.Prep(query) defer stmt.Finalize() for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("error querying rule sets: %v", err) } if !hasRow { break } ruleSetID := int32(stmt.ColumnInt64(0)) ruleSetName := stmt.ColumnText(1) ruleSet := NewRuleSet() ruleSet.SetID(ruleSetID) ruleSet.SetName(ruleSetName) if ruleManager.AddRuleSet(ruleSet) { log.Printf("[Rules] Loading rule set '%s' (%d)", ruleSet.GetName(), ruleSet.GetID()) err := ds.LoadRuleSetDetails(ruleManager, ruleSet) if err != nil { log.Printf("[Rules] Error loading rule set details for '%s': %v", ruleSetName, err) continue // Continue with other rule sets } loadedCount++ } else { log.Printf("[Rules] Unable to add rule set '%s' - ID %d already exists", ruleSetName, ruleSetID) } } log.Printf("[Rules] Loaded %d Rule Sets", loadedCount) // Load global rule set err := ds.LoadGlobalRuleSet(ruleManager) if err != nil { return fmt.Errorf("error loading global rule set: %v", err) } ruleManager.stats.IncrementDatabaseOperations() return nil } // LoadRuleSetDetails loads the detailed rules for a specific rule set // Converted from C++ WorldDatabase::LoadRuleSetDetails() func (ds *DatabaseService) LoadRuleSetDetails(ruleManager *RuleManager, ruleSet *RuleSet) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if ruleSet == nil { return fmt.Errorf("rule set is nil") } // Copy rules from global rule set (coded defaults) first ruleSet.CopyRulesInto(ruleManager.GetGlobalRuleSet()) // Load rule overrides from database query := "SELECT rule_category, rule_type, rule_value FROM ruleset_details WHERE ruleset_id = ?" loadedRules := 0 stmt := ds.db.Prep(query) stmt.BindInt64(1, int64(ruleSet.GetID())) defer stmt.Finalize() for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("error querying rule set details: %v", err) } if !hasRow { break } categoryName := stmt.ColumnText(0) typeName := stmt.ColumnText(1) ruleValue := stmt.ColumnText(2) // Find the rule by name rule := ruleSet.GetRuleByName(categoryName, typeName) if rule == nil { log.Printf("[Rules] Unknown rule with category '%s' and type '%s'", categoryName, typeName) continue // Continue with other rules } log.Printf("[Rules] Setting rule category '%s', type '%s' to value: %s", categoryName, typeName, ruleValue) rule.SetValue(ruleValue) loadedRules++ } log.Printf("[Rules] Loaded %d rule overrides for rule set '%s'", loadedRules, ruleSet.GetName()) ruleManager.stats.IncrementDatabaseOperations() return nil } // SaveRuleSet saves a rule set to the database func (ds *DatabaseService) SaveRuleSet(ruleSet *RuleSet) error { if ds.db == nil { return fmt.Errorf("database not initialized") } if ruleSet == nil { return fmt.Errorf("rule set is nil") } // Use transaction for atomicity var err error defer sqlitex.Save(ds.db)(&err) // Insert or update rule set query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active) VALUES (?, ?, 1) ON CONFLICT(ruleset_id) DO UPDATE SET ruleset_name = excluded.ruleset_name, ruleset_active = excluded.ruleset_active` stmt := ds.db.Prep(query) stmt.BindInt64(1, int64(ruleSet.GetID())) stmt.BindText(2, ruleSet.GetName()) _, err = stmt.Step() if err != nil { return fmt.Errorf("error saving rule set: %v", err) } stmt.Finalize() // Delete existing rule details deleteQuery := "DELETE FROM ruleset_details WHERE ruleset_id = ?" deleteStmt := ds.db.Prep(deleteQuery) deleteStmt.BindInt64(1, int64(ruleSet.GetID())) _, err = deleteStmt.Step() if err != nil { return fmt.Errorf("error deleting existing rule details: %v", err) } deleteStmt.Finalize() // Insert rule details insertQuery := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)" rules := ruleSet.GetRules() for _, categoryMap := range rules { for _, rule := range categoryMap { if rule.IsValid() { combined := rule.GetCombined() parts := splitCombined(combined) if len(parts) == 2 { insertStmt := ds.db.Prep(insertQuery) insertStmt.BindInt64(1, int64(ruleSet.GetID())) insertStmt.BindText(2, parts[0]) insertStmt.BindText(3, parts[1]) insertStmt.BindText(4, rule.GetValue()) _, err = insertStmt.Step() insertStmt.Finalize() if err != nil { return fmt.Errorf("error saving rule detail: %v", err) } } } } } return nil } // DeleteRuleSet deletes a rule set from the database func (ds *DatabaseService) DeleteRuleSet(ruleSetID int32) error { if ds.db == nil { return fmt.Errorf("database not initialized") } // Use transaction for atomicity var err error defer sqlitex.Save(ds.db)(&err) // Delete rule details first (foreign key constraint) detailsStmt := ds.db.Prep("DELETE FROM ruleset_details WHERE ruleset_id = ?") detailsStmt.BindInt64(1, int64(ruleSetID)) _, err = detailsStmt.Step() detailsStmt.Finalize() if err != nil { return fmt.Errorf("error deleting rule details: %v", err) } // Delete rule set rulesetStmt := ds.db.Prep("DELETE FROM rulesets WHERE ruleset_id = ?") rulesetStmt.BindInt64(1, int64(ruleSetID)) _, err = rulesetStmt.Step() rulesetStmt.Finalize() if err != nil { return fmt.Errorf("error deleting rule set: %v", err) } return nil } // SetDefaultRuleSet sets the default rule set ID in the variables table func (ds *DatabaseService) SetDefaultRuleSet(ruleSetID int32) error { if ds.db == nil { return fmt.Errorf("database not initialized") } query := `INSERT INTO variables (variable_name, variable_value, comment) VALUES (?, ?, 'Default ruleset ID') ON CONFLICT(variable_name) DO UPDATE SET variable_value = excluded.variable_value` stmt := ds.db.Prep(query) stmt.BindText(1, DefaultRuleSetIDVar) stmt.BindText(2, strconv.Itoa(int(ruleSetID))) _, err := stmt.Step() stmt.Finalize() if err != nil { return fmt.Errorf("error setting default rule set: %v", err) } return nil } // GetDefaultRuleSetID gets the default rule set ID from the variables table func (ds *DatabaseService) GetDefaultRuleSetID() (int32, error) { if ds.db == nil { return 0, fmt.Errorf("database not initialized") } query := "SELECT variable_value FROM variables WHERE variable_name = ?" stmt := ds.db.Prep(query) stmt.BindText(1, DefaultRuleSetIDVar) hasRow, err := stmt.Step() if err != nil { return 0, fmt.Errorf("error querying default ruleset ID: %v", err) } if !hasRow { return 0, fmt.Errorf("default ruleset ID not found in variables table") } variableValue := stmt.ColumnText(0) if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { return int32(id), nil } return 0, fmt.Errorf("invalid ruleset ID format: %s", variableValue) } // GetRuleSetList returns a list of all rule sets func (ds *DatabaseService) GetRuleSetList() ([]RuleSetInfo, error) { if ds.db == nil { return nil, fmt.Errorf("database not initialized") } query := "SELECT ruleset_id, ruleset_name, ruleset_active FROM rulesets ORDER BY ruleset_id" var ruleSets []RuleSetInfo stmt := ds.db.Prep(query) defer stmt.Finalize() for { hasRow, err := stmt.Step() if err != nil { return nil, fmt.Errorf("error querying rule sets: %v", err) } if !hasRow { break } info := RuleSetInfo{ ID: int32(stmt.ColumnInt64(0)), Name: stmt.ColumnText(1), Active: stmt.ColumnInt64(2) > 0, // Convert int to bool } ruleSets = append(ruleSets, info) } return ruleSets, nil } // ValidateDatabase ensures the required tables exist func (ds *DatabaseService) ValidateDatabase() error { if ds.db == nil { return fmt.Errorf("database not initialized") } tables := []string{"rulesets", "ruleset_details", "variables"} query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?" for _, table := range tables { stmt := ds.db.Prep(query) stmt.BindText(1, table) hasRow, err := stmt.Step() if err != nil { stmt.Finalize() return fmt.Errorf("error checking %s table: %v", table, err) } count := stmt.ColumnInt64(0) stmt.Finalize() if !hasRow || count == 0 { return fmt.Errorf("%s table does not exist", table) } } return nil } // RuleSetInfo contains basic information about a rule set type RuleSetInfo struct { ID int32 `json:"id"` Name string `json:"name"` Active bool `json:"active"` } // Helper function to split combined rule string func splitCombined(combined string) []string { for i, char := range combined { if char == ':' { return []string{combined[:i], combined[i+1:]} } } return []string{combined} } // CreateRulesTables creates the necessary database tables for rules func (ds *DatabaseService) CreateRulesTables() error { if ds.db == nil { return fmt.Errorf("database not initialized") } // Create rulesets table createRuleSets := ` CREATE TABLE IF NOT EXISTS rulesets ( ruleset_id INTEGER PRIMARY KEY, ruleset_name TEXT NOT NULL UNIQUE, ruleset_active INTEGER NOT NULL DEFAULT 0 )` stmt := ds.db.Prep(createRuleSets) _, err := stmt.Step() stmt.Finalize() if err != nil { return fmt.Errorf("error creating rulesets table: %v", err) } // Create ruleset_details table createRuleSetDetails := ` CREATE TABLE IF NOT EXISTS ruleset_details ( id INTEGER PRIMARY KEY AUTOINCREMENT, ruleset_id INTEGER NOT NULL, rule_category TEXT NOT NULL, rule_type TEXT NOT NULL, rule_value TEXT NOT NULL, description TEXT, FOREIGN KEY (ruleset_id) REFERENCES rulesets(ruleset_id) ON DELETE CASCADE )` stmt = ds.db.Prep(createRuleSetDetails) _, err = stmt.Step() stmt.Finalize() if err != nil { return fmt.Errorf("error creating ruleset_details table: %v", err) } // Create variables table if it doesn't exist createVariables := ` CREATE TABLE IF NOT EXISTS variables ( variable_name TEXT PRIMARY KEY, variable_value TEXT NOT NULL, comment TEXT )` stmt = ds.db.Prep(createVariables) _, err = stmt.Step() stmt.Finalize() if err != nil { return fmt.Errorf("error creating variables table: %v", err) } // Create indexes for better performance indexes := []string{ "CREATE INDEX IF NOT EXISTS idx_ruleset_details_ruleset_id ON ruleset_details(ruleset_id)", "CREATE INDEX IF NOT EXISTS idx_ruleset_details_category_type ON ruleset_details(rule_category, rule_type)", "CREATE INDEX IF NOT EXISTS idx_rulesets_active ON rulesets(ruleset_active)", } for _, indexSQL := range indexes { stmt = ds.db.Prep(indexSQL) _, err = stmt.Step() stmt.Finalize() if err != nil { return fmt.Errorf("error creating index: %v", err) } } return nil }