happyDomain/internal/storage/postgresql/database.go
Pierre-Olivier Mercier f457071d5d chore: replace interface{} with any across the codebase
Use the modern Go type alias `any` instead of `interface{}` throughout
all packages for improved readability and consistency with current Go idioms.
2026-03-01 17:38:04 +07:00

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,
}
}