Use the modern Go type alias `any` instead of `interface{}` throughout
all packages for improved readability and consistency with current Go idioms.
233 lines
5.7 KiB
Go
233 lines
5.7 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2025 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
_ "github.com/lib/pq"
|
|
|
|
"git.happydns.org/happyDomain/internal/storage"
|
|
"git.happydns.org/happyDomain/model"
|
|
)
|
|
|
|
type PostgreSQLStorage struct {
|
|
db *sql.DB
|
|
table string
|
|
}
|
|
|
|
// NewPostgreSQLStorage establishes the connection to the PostgreSQL database
|
|
func NewPostgreSQLStorage(cfg *PostgreSQLConfig) (s *PostgreSQLStorage, err error) {
|
|
// Build connection string
|
|
connStr := fmt.Sprintf(
|
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
|
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Database, cfg.SSLMode,
|
|
)
|
|
|
|
// Open database connection
|
|
db, err := sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open PostgreSQL connection: %w", err)
|
|
}
|
|
|
|
// Test the connection
|
|
if err = db.Ping(); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("failed to ping PostgreSQL server: %w", err)
|
|
}
|
|
|
|
// Configure connection pool
|
|
db.SetMaxOpenConns(25)
|
|
db.SetMaxIdleConns(5)
|
|
db.SetConnMaxLifetime(5 * time.Minute)
|
|
|
|
log.Printf("Connected to PostgreSQL database: %s@%s:%d/%s", cfg.User, cfg.Host, cfg.Port, cfg.Database)
|
|
|
|
s = &PostgreSQLStorage{
|
|
db: db,
|
|
table: cfg.Table,
|
|
}
|
|
|
|
// Initialize database schema
|
|
if err = s.initSchema(); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// initSchema creates the table and index if they don't exist
|
|
func (s *PostgreSQLStorage) initSchema() error {
|
|
// Create table with JSONB column
|
|
createTableSQL := fmt.Sprintf(`
|
|
CREATE TABLE IF NOT EXISTS %s (
|
|
key TEXT PRIMARY KEY,
|
|
data JSONB NOT NULL
|
|
)
|
|
`, s.table)
|
|
|
|
_, err := s.db.Exec(createTableSQL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create table: %w", err)
|
|
}
|
|
|
|
// Create index for prefix searches
|
|
createIndexSQL := fmt.Sprintf(`
|
|
CREATE INDEX IF NOT EXISTS idx_%s_key_prefix
|
|
ON %s (key text_pattern_ops)
|
|
`, s.table, s.table)
|
|
|
|
_, err = s.db.Exec(createIndexSQL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create index: %w", err)
|
|
}
|
|
|
|
log.Printf("PostgreSQL schema initialized successfully (table: %s)", s.table)
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Close() error {
|
|
if s.db != nil {
|
|
log.Println("Closing PostgreSQL connection...")
|
|
return s.db.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) DecodeData(data any, v any) error {
|
|
var bytes []byte
|
|
|
|
switch d := data.(type) {
|
|
case []byte:
|
|
bytes = d
|
|
case string:
|
|
bytes = []byte(d)
|
|
default:
|
|
return fmt.Errorf("data to decode is not in []byte or string format (%T)", data)
|
|
}
|
|
|
|
return json.Unmarshal(bytes, v)
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Has(key string) (bool, error) {
|
|
query := fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM %s WHERE key = $1)", s.table)
|
|
|
|
var exists bool
|
|
err := s.db.QueryRow(query, key).Scan(&exists)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check key existence: %w", err)
|
|
}
|
|
|
|
return exists, nil
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Get(key string, v any) error {
|
|
query := fmt.Sprintf("SELECT data FROM %s WHERE key = $1", s.table)
|
|
|
|
var jsonData []byte
|
|
err := s.db.QueryRow(query, key).Scan(&jsonData)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return happydns.ErrNotFound
|
|
}
|
|
return fmt.Errorf("failed to get key %q: %w", key, err)
|
|
}
|
|
|
|
return json.Unmarshal(jsonData, v)
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Put(key string, v any) error {
|
|
// Marshal value to JSON
|
|
jsonData, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal value: %w", err)
|
|
}
|
|
|
|
// Use UPSERT to handle both insert and update
|
|
query := fmt.Sprintf(`
|
|
INSERT INTO %s (key, data)
|
|
VALUES ($1, $2::jsonb)
|
|
ON CONFLICT (key)
|
|
DO UPDATE SET data = EXCLUDED.data
|
|
`, s.table)
|
|
|
|
_, err = s.db.Exec(query, key, jsonData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put key %q: %w", key, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) FindIdentifierKey(prefix string) (key string, id happydns.Identifier, err error) {
|
|
found := true
|
|
for found {
|
|
id, err = happydns.NewRandomIdentifier()
|
|
if err != nil {
|
|
return
|
|
}
|
|
key = fmt.Sprintf("%s%s", prefix, id.String())
|
|
|
|
found, err = s.Has(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Delete(key string) error {
|
|
query := fmt.Sprintf("DELETE FROM %s WHERE key = $1", s.table)
|
|
|
|
_, err := s.db.Exec(query, key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete key %q: %w", key, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgreSQLStorage) Search(prefix string) storage.Iterator {
|
|
query := fmt.Sprintf("SELECT key, data FROM %s WHERE key LIKE $1 || '%%' ORDER BY key", s.table)
|
|
|
|
rows, err := s.db.Query(query, prefix)
|
|
if err != nil {
|
|
log.Printf("PostgreSQL Search error: %v", err)
|
|
// Return an iterator with the error
|
|
return &PostgreSQLIterator{
|
|
rows: nil,
|
|
err: err,
|
|
valid: false,
|
|
}
|
|
}
|
|
|
|
return &PostgreSQLIterator{
|
|
rows: rows,
|
|
valid: false,
|
|
}
|
|
}
|