store: P2R-10 schema for source-group + host-default hooks (migration 0010)

Adds pre_hook/post_hook BLOB columns to source_groups and
pre_hook_default/post_hook_default to hosts. Bytes stored verbatim
(AEAD encrypt/decrypt happens at the HTTP layer where the AEAD key
lives). Round-trip tests cover set/clear semantics on both tables.
This commit is contained in:
2026-05-04 10:52:16 +01:00
parent c9b49637d1
commit 18b0bf976d
5 changed files with 190 additions and 11 deletions
+22 -7
View File
@@ -45,13 +45,14 @@ func (st *Store) CreateSourceGroup(ctx context.Context, g *SourceGroup) error {
`INSERT INTO source_groups (
id, host_id, name, includes, excludes, retention_policy,
retry_max, retry_backoff_seconds, conflict_dimension,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
created_at, updated_at, pre_hook, post_hook
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
g.ID, g.HostID, g.Name,
string(includesJSON), string(excludesJSON), string(retentionJSON),
g.RetryMax, g.RetryBackoffSeconds,
nullableString(g.ConflictDimension),
now.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano),
nullableBytes(g.PreHook), nullableBytes(g.PostHook),
); err != nil {
return fmt.Errorf("store: create source group: %w", err)
}
@@ -88,13 +89,14 @@ func (st *Store) UpdateSourceGroup(ctx context.Context, g *SourceGroup) error {
`UPDATE source_groups SET
name = ?, includes = ?, excludes = ?, retention_policy = ?,
retry_max = ?, retry_backoff_seconds = ?, conflict_dimension = ?,
updated_at = ?
updated_at = ?, pre_hook = ?, post_hook = ?
WHERE id = ? AND host_id = ?`,
g.Name,
string(includesJSON), string(excludesJSON), string(retentionJSON),
g.RetryMax, g.RetryBackoffSeconds,
nullableString(g.ConflictDimension),
now.Format(time.RFC3339Nano),
nullableBytes(g.PreHook), nullableBytes(g.PostHook),
g.ID, g.HostID,
)
if err != nil {
@@ -143,7 +145,7 @@ func (st *Store) GetSourceGroup(ctx context.Context, hostID, groupID string) (*S
row := st.db.QueryRowContext(ctx,
`SELECT id, host_id, name, includes, excludes, retention_policy,
retry_max, retry_backoff_seconds, conflict_dimension,
created_at, updated_at
created_at, updated_at, pre_hook, post_hook
FROM source_groups WHERE id = ? AND host_id = ?`,
groupID, hostID)
g, err := scanSourceGroup(row)
@@ -159,7 +161,7 @@ func (st *Store) GetSourceGroupByName(ctx context.Context, hostID, name string)
row := st.db.QueryRowContext(ctx,
`SELECT id, host_id, name, includes, excludes, retention_policy,
retry_max, retry_backoff_seconds, conflict_dimension,
created_at, updated_at
created_at, updated_at, pre_hook, post_hook
FROM source_groups WHERE host_id = ? AND name = ?`,
hostID, name)
g, err := scanSourceGroup(row)
@@ -177,7 +179,7 @@ func (st *Store) ListSourceGroupsByHost(ctx context.Context, hostID string) ([]S
rows, err := st.db.QueryContext(ctx,
`SELECT id, host_id, name, includes, excludes, retention_policy,
retry_max, retry_backoff_seconds, conflict_dimension,
created_at, updated_at
created_at, updated_at, pre_hook, post_hook
FROM source_groups WHERE host_id = ? ORDER BY name`,
hostID)
if err != nil {
@@ -224,14 +226,17 @@ func scanSourceGroupRow(s sourceGroupScanner) (*SourceGroup, error) {
includes, excludes, retention string
conflict sql.NullString
createdAt, updatedAt string
preHook, postHook []byte
)
err := s.Scan(&out.ID, &out.HostID, &out.Name,
&includes, &excludes, &retention,
&out.RetryMax, &out.RetryBackoffSeconds, &conflict,
&createdAt, &updatedAt)
&createdAt, &updatedAt, &preHook, &postHook)
if err != nil {
return nil, err
}
out.PreHook = preHook
out.PostHook = postHook
if includes != "" {
_ = json.Unmarshal([]byte(includes), &out.Includes)
}
@@ -259,3 +264,13 @@ func nullableString(s string) any {
}
return s
}
// nullableBytes returns nil for an empty/nil slice so SQL stores it
// as NULL rather than an empty BLOB. The agent treats both the same
// (no hook), but NULL is the canonical "absent" form on disk.
func nullableBytes(b []byte) any {
if len(b) == 0 {
return nil
}
return b
}