package sqlite import ( "context" "fmt" "strings" ) const ( defaultLogicalGroupRoutePolicy = "priority" defaultLogicalGroupStickyMode = "conversation_preferred" defaultConversationTTLSeconds = 7200 defaultUserModelTTLSeconds = 1800 defaultFailoverThreshold = 2 defaultCooldownSeconds = 600 ) type LogicalGroup struct { ID int64 LogicalGroupID string DisplayName string Status string Description string RoutePolicy string StickyMode string ConversationTTLSeconds int UserModelTTLSeconds int FailoverThreshold int CooldownSeconds int CreatedAt string UpdatedAt string } type LogicalGroupsRepo struct { db execQuerier } func newLogicalGroupsRepo(db execQuerier) *LogicalGroupsRepo { return &LogicalGroupsRepo{db: db} } func (r *LogicalGroupsRepo) Create(ctx context.Context, group LogicalGroup) (int64, error) { group, err := normalizeLogicalGroup(group) if err != nil { return 0, err } result, err := r.db.ExecContext( ctx, `INSERT INTO logical_groups ( logical_group_id, display_name, status, description, route_policy, sticky_mode, conversation_ttl_seconds, user_model_ttl_seconds, failover_threshold, cooldown_seconds ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, group.LogicalGroupID, group.DisplayName, group.Status, group.Description, group.RoutePolicy, group.StickyMode, group.ConversationTTLSeconds, group.UserModelTTLSeconds, group.FailoverThreshold, group.CooldownSeconds, ) if err != nil { return 0, fmt.Errorf("insert logical group %q: %w", group.LogicalGroupID, err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("read inserted logical group id for %q: %w", group.LogicalGroupID, err) } return id, nil } func (r *LogicalGroupsRepo) GetByLogicalGroupID(ctx context.Context, logicalGroupID string) (LogicalGroup, error) { logicalGroupID = strings.TrimSpace(logicalGroupID) if logicalGroupID == "" { return LogicalGroup{}, fmt.Errorf("logical_group_id is required") } var group LogicalGroup if err := r.db.QueryRowContext( ctx, `SELECT id, logical_group_id, display_name, status, description, route_policy, sticky_mode, conversation_ttl_seconds, user_model_ttl_seconds, failover_threshold, cooldown_seconds, created_at, updated_at FROM logical_groups WHERE logical_group_id = ?`, logicalGroupID, ).Scan( &group.ID, &group.LogicalGroupID, &group.DisplayName, &group.Status, &group.Description, &group.RoutePolicy, &group.StickyMode, &group.ConversationTTLSeconds, &group.UserModelTTLSeconds, &group.FailoverThreshold, &group.CooldownSeconds, &group.CreatedAt, &group.UpdatedAt, ); err != nil { return LogicalGroup{}, err } return group, nil } func (r *LogicalGroupsRepo) List(ctx context.Context) ([]LogicalGroup, error) { rows, err := r.db.QueryContext( ctx, `SELECT id, logical_group_id, display_name, status, description, route_policy, sticky_mode, conversation_ttl_seconds, user_model_ttl_seconds, failover_threshold, cooldown_seconds, created_at, updated_at FROM logical_groups ORDER BY id ASC`, ) if err != nil { return nil, fmt.Errorf("list logical groups: %w", err) } defer rows.Close() groups := make([]LogicalGroup, 0) for rows.Next() { var group LogicalGroup if err := rows.Scan( &group.ID, &group.LogicalGroupID, &group.DisplayName, &group.Status, &group.Description, &group.RoutePolicy, &group.StickyMode, &group.ConversationTTLSeconds, &group.UserModelTTLSeconds, &group.FailoverThreshold, &group.CooldownSeconds, &group.CreatedAt, &group.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan logical group: %w", err) } groups = append(groups, group) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate logical groups: %w", err) } return groups, nil } func (r *LogicalGroupsRepo) UpdateByLogicalGroupID(ctx context.Context, group LogicalGroup) error { group, err := normalizeLogicalGroup(group) if err != nil { return err } result, err := r.db.ExecContext( ctx, `UPDATE logical_groups SET display_name = ?, status = ?, description = ?, route_policy = ?, sticky_mode = ?, conversation_ttl_seconds = ?, user_model_ttl_seconds = ?, failover_threshold = ?, cooldown_seconds = ?, updated_at = CURRENT_TIMESTAMP WHERE logical_group_id = ?`, group.DisplayName, group.Status, group.Description, group.RoutePolicy, group.StickyMode, group.ConversationTTLSeconds, group.UserModelTTLSeconds, group.FailoverThreshold, group.CooldownSeconds, group.LogicalGroupID, ) if err != nil { return fmt.Errorf("update logical group %q: %w", group.LogicalGroupID, err) } affected, err := result.RowsAffected() if err != nil { return fmt.Errorf("read updated logical group rows for %q: %w", group.LogicalGroupID, err) } if affected == 0 { return fmt.Errorf("logical group %q not found", group.LogicalGroupID) } return nil } func (r *LogicalGroupsRepo) DeleteByLogicalGroupID(ctx context.Context, logicalGroupID string) error { logicalGroupID = strings.TrimSpace(logicalGroupID) if logicalGroupID == "" { return fmt.Errorf("logical_group_id is required") } result, err := r.db.ExecContext(ctx, `DELETE FROM logical_groups WHERE logical_group_id = ?`, logicalGroupID) if err != nil { return fmt.Errorf("delete logical group %q: %w", logicalGroupID, err) } affected, err := result.RowsAffected() if err != nil { return fmt.Errorf("read deleted logical group rows for %q: %w", logicalGroupID, err) } if affected == 0 { return fmt.Errorf("logical group %q not found", logicalGroupID) } return nil } func normalizeLogicalGroup(group LogicalGroup) (LogicalGroup, error) { group.LogicalGroupID = strings.TrimSpace(group.LogicalGroupID) group.DisplayName = strings.TrimSpace(group.DisplayName) group.Status = strings.TrimSpace(group.Status) group.Description = strings.TrimSpace(group.Description) group.RoutePolicy = strings.TrimSpace(group.RoutePolicy) group.StickyMode = strings.TrimSpace(group.StickyMode) switch { case group.LogicalGroupID == "": return LogicalGroup{}, fmt.Errorf("logical_group_id is required") case group.DisplayName == "": return LogicalGroup{}, fmt.Errorf("display_name is required") case group.Status == "": return LogicalGroup{}, fmt.Errorf("status is required") } if group.RoutePolicy == "" { group.RoutePolicy = defaultLogicalGroupRoutePolicy } if group.StickyMode == "" { group.StickyMode = defaultLogicalGroupStickyMode } if group.ConversationTTLSeconds <= 0 { group.ConversationTTLSeconds = defaultConversationTTLSeconds } if group.UserModelTTLSeconds <= 0 { group.UserModelTTLSeconds = defaultUserModelTTLSeconds } if group.FailoverThreshold <= 0 { group.FailoverThreshold = defaultFailoverThreshold } if group.CooldownSeconds <= 0 { group.CooldownSeconds = defaultCooldownSeconds } return group, nil }