Files
2026-07-10 23:53:43 +02:00

124 lines
3.4 KiB
Go

// 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()
}