// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "database/sql" "fmt" ) // Store wraps DB access against the {TablePrefix}records table (schema per // docs/SEMANTICS.md §1). type Store struct { db *sql.DB tableName string } // NewStore builds a Store bound to {prefix}records. func NewStore(db *sql.DB, tablePrefix string) *Store { return &Store{ db: db, tableName: tablePrefix + "records", } } // Record is one row of {prefix}records. type Record struct { Name string TTL sql.NullInt64 Content string RecordType string } // ListZones returns distinct zones present in the table, ordered by name. func (st *Store) ListZones(ctx context.Context) ([]string, error) { query := fmt.Sprintf("SELECT DISTINCT zone FROM %s ORDER BY zone", st.tableName) rows, err := st.db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var zones []string for rows.Next() { var zone string if err := rows.Scan(&zone); err != nil { return nil, err } zones = append(zones, zone) } return zones, rows.Err() } // GetZoneRecords returns every row for the given zone. func (st *Store) GetZoneRecords(ctx context.Context, zone string) ([]Record, error) { query := fmt.Sprintf("SELECT name, ttl, content, record_type FROM %s WHERE zone = ?", st.tableName) rows, err := st.db.QueryContext(ctx, query, zone) if err != nil { return nil, err } defer rows.Close() var records []Record for rows.Next() { var rec Record if err := rows.Scan(&rec.Name, &rec.TTL, &rec.Content, &rec.RecordType); err != nil { return nil, err } records = append(records, rec) } return records, rows.Err() } // ZoneExists reports whether at least one row exists for the zone. func (st *Store) ZoneExists(ctx context.Context, zone string) (bool, error) { query := fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM %s WHERE zone = ?)", st.tableName) var exists bool if err := st.db.QueryRowContext(ctx, query, zone).Scan(&exists); err != nil { return false, err } return exists, nil } // RRsetChange is one decomposed rrset change ready to apply to the DB. type RRsetChange struct { Zone string Name string // relative name, per SEMANTICS §1 (empty string at apex) RecordType string ChangeType string // REPLACE or DELETE TTL int Contents []string // marshalled JSON content, one per record; unused for DELETE } // ApplyRRsets runs every rrset change of one PATCH inside a single // transaction. REPLACE deletes existing rows for (zone, name, record_type) // then inserts one row per record. DELETE just deletes. Any error rolls // back the entire transaction. func (st *Store) ApplyRRsets(ctx context.Context, changes []RRsetChange) error { tx, err := st.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() deleteQuery := fmt.Sprintf("DELETE FROM %s WHERE zone = ? AND name = ? AND record_type = ?", st.tableName) insertQuery := fmt.Sprintf("INSERT INTO %s (zone, name, ttl, content, record_type) VALUES (?, ?, ?, ?, ?)", st.tableName) for _, ch := range changes { if _, err := tx.ExecContext(ctx, deleteQuery, ch.Zone, ch.Name, ch.RecordType); err != nil { return err } if ch.ChangeType == "REPLACE" { for _, content := range ch.Contents { if _, err := tx.ExecContext(ctx, insertQuery, ch.Zone, ch.Name, ch.TTL, content, ch.RecordType); err != nil { return err } } } } return tx.Commit() }