package flags import ( "context" "database/sql" "errors" "fmt" "time" "go.uber.org/zap" ) // PgFlagStore implements FlagStore using PostgreSQL. type PgFlagStore struct { db *sql.DB logger *zap.Logger } func NewPgFlagStore(db *sql.DB, logger *zap.Logger) *PgFlagStore { return &PgFlagStore{db: db, logger: logger} } func (p *PgFlagStore) IsEnabled(ctx context.Context, tenantID, name string) (bool, error) { // Tenant-specific takes precedence over global (NULL tenant_id) const q = ` SELECT is_enabled FROM feature_flags WHERE name = $1 AND (tenant_id = $2 OR tenant_id IS NULL) ORDER BY tenant_id NULLS LAST LIMIT 1` var enabled bool err := p.db.QueryRowContext(ctx, q, name, tenantID).Scan(&enabled) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { return false, fmt.Errorf("feature_flags is_enabled: %w", err) } return enabled, nil } func (p *PgFlagStore) Get(ctx context.Context, tenantID, name string) (FeatureFlag, error) { const q = ` SELECT id, COALESCE(tenant_id::text,''), name, is_enabled, created_at, updated_at FROM feature_flags WHERE name = $1 AND (tenant_id = $2 OR ($2 = '' AND tenant_id IS NULL))` row := p.db.QueryRowContext(ctx, q, name, tenantID) f, err := scanFlag(row) if errors.Is(err, sql.ErrNoRows) { return FeatureFlag{}, ErrNotFound } return f, err } func (p *PgFlagStore) Set(ctx context.Context, tenantID, name string, enabled bool) (FeatureFlag, error) { // tenantID="" → NULL in DB (global flag) var tidArg interface{} if tenantID != "" { tidArg = tenantID } const q = ` INSERT INTO feature_flags (tenant_id, name, is_enabled) VALUES ($1, $2, $3) ON CONFLICT (tenant_id, name) DO UPDATE SET is_enabled = EXCLUDED.is_enabled, updated_at = NOW() RETURNING id, COALESCE(tenant_id::text,''), name, is_enabled, created_at, updated_at` row := p.db.QueryRowContext(ctx, q, tidArg, name, enabled) return scanFlag(row) } func (p *PgFlagStore) List(ctx context.Context, tenantID string) ([]FeatureFlag, error) { const q = ` SELECT id, COALESCE(tenant_id::text,''), name, is_enabled, created_at, updated_at FROM feature_flags WHERE tenant_id = $1 OR tenant_id IS NULL ORDER BY name` rows, err := p.db.QueryContext(ctx, q, tenantID) if err != nil { return nil, fmt.Errorf("feature_flags list: %w", err) } defer rows.Close() //nolint:errcheck var out []FeatureFlag for rows.Next() { f, err := scanFlag(rows) if err != nil { return nil, err } out = append(out, f) } return out, rows.Err() } func (p *PgFlagStore) Delete(ctx context.Context, tenantID, name string) error { var tidArg interface{} if tenantID != "" { tidArg = tenantID } const q = `DELETE FROM feature_flags WHERE name = $1 AND (tenant_id = $2 OR ($2 IS NULL AND tenant_id IS NULL))` res, err := p.db.ExecContext(ctx, q, name, tidArg) if err != nil { return fmt.Errorf("feature_flags delete: %w", err) } n, _ := res.RowsAffected() if n == 0 { return ErrNotFound } return nil } // ─── scanner ───────────────────────────────────────────────────────────────── type flagScanner interface { Scan(dest ...interface{}) error } func scanFlag(s flagScanner) (FeatureFlag, error) { var ( f FeatureFlag createdAt time.Time updatedAt time.Time ) err := s.Scan(&f.ID, &f.TenantID, &f.Name, &f.IsEnabled, &createdAt, &updatedAt) if err != nil { return FeatureFlag{}, fmt.Errorf("scanning feature_flag row: %w", err) } f.CreatedAt = createdAt f.UpdatedAt = updatedAt return f, nil }