CI / test (push) Successful in 1m25s
The catalog can change while the cluster runs; this is where a change goes so it is still there tomorrow. Models declared in configuration and models added at runtime are two sources on purpose: config-as-code stays declarative and under review, and the database holds what someone added to try it. Configuration wins every conflict, and the direction matters. A request for a reviewed production model must never be answered by something added from a laptop, so a stored model colliding with a declared name is refused when it is added. A declared model cannot be removed at runtime either — it would return on the next restart, leaving the cluster disagreeing with its own config file until then, which is a worse answer than refusing and saying where to edit. The whole catalog is built before anything is written. A model naming a missing fallback, or colliding, or malformed in any of the ways catalog.New refuses, is rejected rather than persisted and then skipped forever after — a model that looks added and never appears is the worst outcome available. Removal is checked the same way, because another model may fall back to the one being removed, and a fallback pointing at nothing is discovered during the outage it was meant to cover. At startup each stored model is validated on its own against what is already accepted, so one bad row is skipped and named rather than taking the cluster with it. The declared models are precisely the ones that must survive that. Catalog types now carry json tags mirroring their koanf ones, so a stored definition is the same shape as the YAML someone writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// StoredModel is a model added at runtime rather than declared in configuration.
|
|
type StoredModel struct {
|
|
Name string
|
|
// Definition is the catalog entry, as JSON. Stored whole because a profile
|
|
// is a nested and still-growing shape: as columns, every field added to it
|
|
// would be a migration, and the catalog already knows how to validate one.
|
|
Definition []byte
|
|
CreatedAt time.Time
|
|
CreatedBy string
|
|
}
|
|
|
|
// PutModel adds or replaces a runtime model.
|
|
func (s *SQLite) PutModel(ctx context.Context, m StoredModel) error {
|
|
if m.Name == "" {
|
|
return fmt.Errorf("a model needs a name")
|
|
}
|
|
if !json.Valid(m.Definition) {
|
|
return fmt.Errorf("model %q: the definition is not valid JSON", m.Name)
|
|
}
|
|
if m.CreatedAt.IsZero() {
|
|
m.CreatedAt = time.Now()
|
|
}
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO models (name, definition, created_at, created_by)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET definition = excluded.definition`,
|
|
m.Name, string(m.Definition), m.CreatedAt, m.CreatedBy)
|
|
return err
|
|
}
|
|
|
|
// DeleteModel removes a runtime model. Removing one that is not there is not an
|
|
// error: the caller wanted it gone, and it is.
|
|
func (s *SQLite) DeleteModel(ctx context.Context, name string) error {
|
|
_, err := s.db.ExecContext(ctx, `DELETE FROM models WHERE name = ?`, name)
|
|
return err
|
|
}
|
|
|
|
// Models lists the runtime models, oldest first so the catalog is built in a
|
|
// stable order and two starts produce the same result.
|
|
func (s *SQLite) Models(ctx context.Context) ([]StoredModel, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT name, definition, created_at, created_by FROM models ORDER BY created_at, name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []StoredModel
|
|
for rows.Next() {
|
|
var m StoredModel
|
|
var def string
|
|
if err := rows.Scan(&m.Name, &def, &m.CreatedAt, &m.CreatedBy); err != nil {
|
|
return nil, err
|
|
}
|
|
m.Definition = []byte(def)
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|