package store import ( "context" "database/sql" "fmt" "time" ) // schemaSQL — схема таблицы устройств. // PRIMARY KEY (namespace, name) — как в REST-маршруте /devices/{name}. // Уникальность device_id в рамках namespace — для MQTT username. const schemaSQL = ` CREATE TABLE IF NOT EXISTS iot_devices ( namespace TEXT NOT NULL, name TEXT NOT NULL, device_id TEXT NOT NULL, enabled BOOLEAN NOT NULL DEFAULT true, mqtt_password TEXT NOT NULL, metadata JSONB, phase TEXT NOT NULL DEFAULT 'Active', last_connected TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (namespace, name) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_iot_devices_ns_device ON iot_devices (namespace, device_id); ` // DeviceStore — доступ к таблице iot_devices (собственный пул соединений). type DeviceStore struct { db *sql.DB } // Open подключается к PostgreSQL по DSN и инициализирует схему. func Open(ctx context.Context, dsn string) (*DeviceStore, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, fmt.Errorf("store: open DB: %w", err) } db.SetMaxOpenConns(5) db.SetMaxIdleConns(2) db.SetConnMaxLifetime(5 * time.Minute) pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := db.PingContext(pingCtx); err != nil { db.Close() return nil, fmt.Errorf("store: ping DB: %w", err) } if _, err := db.ExecContext(ctx, schemaSQL); err != nil { db.Close() return nil, fmt.Errorf("store: init schema: %w", err) } return &DeviceStore{db: db}, nil } // Close закрывает пул соединений. func (s *DeviceStore) Close() error { return s.db.Close() }