mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 18:19:19 +00:00
506 lines
17 KiB
Go
506 lines
17 KiB
Go
//go:build sqlite || sqliteonly
|
|
|
|
package sqlitestore
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
type SQLiteSkillEvolutionStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewSQLiteSkillEvolutionStore(db *sql.DB) *SQLiteSkillEvolutionStore {
|
|
return &SQLiteSkillEvolutionStore{db: db}
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) resolveSkill(ctx context.Context, skillID uuid.UUID) (string, int, error) {
|
|
tenantID := tenantIDForInsert(ctx)
|
|
var slug, skillTenant string
|
|
var version int
|
|
var isSystem bool
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT slug, version, tenant_id, is_system FROM skills WHERE id = ? AND status != 'deleted'`,
|
|
skillID.String(),
|
|
).Scan(&slug, &version, &skillTenant, &isSystem)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", 0, fmt.Errorf("skill not found")
|
|
}
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
if !store.IsCrossTenant(ctx) && !isSystem && skillTenant != tenantID.String() {
|
|
return "", 0, fmt.Errorf("skill not found")
|
|
}
|
|
return slug, version, nil
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) GetSettings(ctx context.Context, skillID uuid.UUID) (*store.SkillEvolutionSettings, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
var out store.SkillEvolutionSettings
|
|
var tenantStr, skillStr string
|
|
var last nullSqliteTime
|
|
var created, updated sqliteTime
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT tenant_id, skill_id, enabled, mode, last_analyzed_at, created_at, updated_at
|
|
FROM skill_evolution_settings WHERE tenant_id = ? AND skill_id = ?`,
|
|
tenantID.String(), skillID.String(),
|
|
).Scan(&tenantStr, &skillStr, &out.Enabled, &out.Mode, &last, &created, &updated)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return &store.SkillEvolutionSettings{
|
|
TenantID: tenantID,
|
|
SkillID: skillID,
|
|
Enabled: false,
|
|
Mode: store.SkillEvolutionModeSuggestOnly,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.TenantID, _ = uuid.Parse(tenantStr)
|
|
out.SkillID, _ = uuid.Parse(skillStr)
|
|
if last.Valid {
|
|
t := last.Time
|
|
out.LastAnalyzedAt = &t
|
|
}
|
|
out.CreatedAt = created.Time
|
|
out.UpdatedAt = updated.Time
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) UpsertSettings(ctx context.Context, settings store.SkillEvolutionSettings) (*store.SkillEvolutionSettings, error) {
|
|
if _, _, err := s.resolveSkill(ctx, settings.SkillID); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
mode := strings.TrimSpace(settings.Mode)
|
|
if mode == "" {
|
|
mode = store.SkillEvolutionModeSuggestOnly
|
|
}
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
var last any
|
|
if settings.LastAnalyzedAt != nil {
|
|
last = settings.LastAnalyzedAt.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO skill_evolution_settings (tenant_id, skill_id, enabled, mode, last_analyzed_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(tenant_id, skill_id) DO UPDATE SET
|
|
enabled = excluded.enabled,
|
|
mode = excluded.mode,
|
|
last_analyzed_at = excluded.last_analyzed_at,
|
|
updated_at = excluded.updated_at`,
|
|
tenantID.String(), settings.SkillID.String(), settings.Enabled, mode, last, now, now,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.GetSettings(ctx, settings.SkillID)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) RecordUsage(ctx context.Context, metric store.SkillUsageMetric) error {
|
|
slug, version, err := s.resolveSkill(ctx, metric.SkillID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
if metric.ID == uuid.Nil {
|
|
metric.ID = uuid.New()
|
|
}
|
|
if metric.SkillSlug == "" {
|
|
metric.SkillSlug = slug
|
|
}
|
|
if metric.SkillVersion == 0 {
|
|
metric.SkillVersion = version
|
|
}
|
|
if metric.Status == "" {
|
|
metric.Status = store.SkillUsageStatusStarted
|
|
}
|
|
agentID := ""
|
|
if metric.AgentID != uuid.Nil {
|
|
agentID = metric.AgentID.String()
|
|
}
|
|
_, err = s.db.ExecContext(ctx,
|
|
`INSERT INTO skill_usage_metrics
|
|
(id, tenant_id, skill_id, skill_slug, skill_version, agent_id, user_id, session_key,
|
|
trace_id, invocation_id, invocation_source, status, failure_reason, tool_calls_count, duration_ms)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
metric.ID.String(), tenantID.String(), metric.SkillID.String(), metric.SkillSlug, metric.SkillVersion,
|
|
agentID, metric.UserID, metric.SessionKey, metric.TraceID, metric.InvocationID, metric.InvocationSource,
|
|
metric.Status, metric.FailureReason, metric.ToolCallsCount, metric.DurationMs,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) AggregateUsage(ctx context.Context, skillID uuid.UUID, since *time.Time) (*store.SkillUsageStats, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
where := "tenant_id = ? AND skill_id = ?"
|
|
args := []any{tenantID.String(), skillID.String()}
|
|
if since != nil {
|
|
where += " AND created_at >= ?"
|
|
args = append(args, since.UTC().Format(time.RFC3339Nano))
|
|
}
|
|
query := fmt.Sprintf(
|
|
`SELECT COUNT(*),
|
|
SUM(CASE WHEN status = 'started' THEN 1 ELSE 0 END),
|
|
SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END),
|
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END),
|
|
SUM(CASE WHEN status = 'abandoned' THEN 1 ELSE 0 END),
|
|
MAX(created_at)
|
|
FROM skill_usage_metrics WHERE %s`,
|
|
where,
|
|
)
|
|
var out store.SkillUsageStats
|
|
out.SkillID = skillID
|
|
var last nullSqliteTime
|
|
if err := s.db.QueryRowContext(ctx, query, args...).Scan(&out.TotalCalls, &out.Started, &out.Succeeded, &out.Failed, &out.Abandoned, &last); err != nil {
|
|
return nil, err
|
|
}
|
|
if last.Valid {
|
|
t := last.Time
|
|
out.LastUsedAt = &t
|
|
}
|
|
if out.TotalCalls > 0 {
|
|
out.SuccessRate = float64(out.Succeeded) / float64(out.TotalCalls)
|
|
out.FailureRate = float64(out.Failed) / float64(out.TotalCalls)
|
|
}
|
|
reasonQuery := fmt.Sprintf(
|
|
`SELECT failure_reason, COUNT(*), MAX(created_at)
|
|
FROM skill_usage_metrics
|
|
WHERE %s AND status = 'failed' AND COALESCE(failure_reason, '') != ''
|
|
GROUP BY failure_reason ORDER BY COUNT(*) DESC, MAX(created_at) DESC LIMIT 5`,
|
|
where,
|
|
)
|
|
rows, err := s.db.QueryContext(ctx, reasonQuery, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var r store.SkillFailureReason
|
|
var lastSeen sqliteTime
|
|
if err := rows.Scan(&r.Reason, &r.Count, &lastSeen); err != nil {
|
|
return nil, err
|
|
}
|
|
r.LastSeen = lastSeen.Time
|
|
out.TopFailureReasons = append(out.TopFailureReasons, r)
|
|
}
|
|
return &out, rows.Err()
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) ListUsage(ctx context.Context, skillID uuid.UUID, limit int) ([]store.SkillUsageMetric, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, tenant_id, skill_id, skill_slug, skill_version, COALESCE(agent_id, ''),
|
|
COALESCE(user_id,''), COALESCE(session_key,''), COALESCE(trace_id,''), COALESCE(invocation_id,''),
|
|
invocation_source, status, COALESCE(failure_reason,''), tool_calls_count, duration_ms, created_at
|
|
FROM skill_usage_metrics
|
|
WHERE tenant_id = ? AND skill_id = ?
|
|
ORDER BY created_at DESC LIMIT ?`,
|
|
tenantID.String(), skillID.String(), limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []store.SkillUsageMetric
|
|
for rows.Next() {
|
|
var m store.SkillUsageMetric
|
|
var idStr, tenantStr, skillStr, agentStr string
|
|
var created sqliteTime
|
|
if err := rows.Scan(&idStr, &tenantStr, &skillStr, &m.SkillSlug, &m.SkillVersion, &agentStr,
|
|
&m.UserID, &m.SessionKey, &m.TraceID, &m.InvocationID, &m.InvocationSource, &m.Status,
|
|
&m.FailureReason, &m.ToolCallsCount, &m.DurationMs, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
m.ID, _ = uuid.Parse(idStr)
|
|
m.TenantID, _ = uuid.Parse(tenantStr)
|
|
m.SkillID, _ = uuid.Parse(skillStr)
|
|
if agentStr != "" {
|
|
m.AgentID, _ = uuid.Parse(agentStr)
|
|
}
|
|
m.CreatedAt = created.Time
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) CreateSuggestion(ctx context.Context, sg store.SkillImprovementSuggestion) (*store.SkillImprovementSuggestion, error) {
|
|
slug, _, err := s.resolveSkill(ctx, sg.SkillID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sg.ID == uuid.Nil {
|
|
sg.ID = uuid.New()
|
|
}
|
|
if sg.SkillSlug == "" {
|
|
sg.SkillSlug = slug
|
|
}
|
|
if sg.Status == "" {
|
|
sg.Status = store.SkillSuggestionStatusPending
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
_, err = s.db.ExecContext(ctx,
|
|
`INSERT INTO skill_improvement_suggestions
|
|
(id, tenant_id, skill_id, skill_slug, suggestion_type, status, reason, evidence,
|
|
draft_patch, target_file, created_by_actor_type, created_by_actor_id, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
sg.ID.String(), tenantID.String(), sg.SkillID.String(), sg.SkillSlug, sg.SuggestionType,
|
|
sg.Status, sg.Reason, string(jsonOrDefault(sg.Evidence, "{}")), string(jsonOrDefault(sg.DraftPatch, "{}")),
|
|
sg.TargetFile, sg.CreatedByActorType, sg.CreatedByActorID, now, now,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.GetSuggestion(ctx, sg.ID)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) ListSuggestions(ctx context.Context, skillID uuid.UUID, status string, limit int) ([]store.SkillImprovementSuggestion, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
q := `SELECT id, tenant_id, skill_id, skill_slug, suggestion_type, status, reason, evidence, draft_patch,
|
|
COALESCE(target_file,''), COALESCE(created_by_actor_type,''), COALESCE(created_by_actor_id,''),
|
|
COALESCE(reviewed_by_actor_type,''), COALESCE(reviewed_by_actor_id,''), reviewed_at, applied_version,
|
|
created_at, updated_at
|
|
FROM skill_improvement_suggestions WHERE tenant_id = ? AND skill_id = ?`
|
|
args := []any{tenantID.String(), skillID.String()}
|
|
if status != "" {
|
|
q += " AND status = ?"
|
|
args = append(args, status)
|
|
}
|
|
q += " ORDER BY created_at DESC LIMIT ?"
|
|
args = append(args, limit)
|
|
rows, err := s.db.QueryContext(ctx, q, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanSQLiteSuggestions(rows)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) GetSuggestion(ctx context.Context, id uuid.UUID) (*store.SkillImprovementSuggestion, error) {
|
|
tenantID := tenantIDForInsert(ctx)
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, tenant_id, skill_id, skill_slug, suggestion_type, status, reason, evidence, draft_patch,
|
|
COALESCE(target_file,''), COALESCE(created_by_actor_type,''), COALESCE(created_by_actor_id,''),
|
|
COALESCE(reviewed_by_actor_type,''), COALESCE(reviewed_by_actor_id,''), reviewed_at, applied_version,
|
|
created_at, updated_at
|
|
FROM skill_improvement_suggestions WHERE tenant_id = ? AND id = ?`,
|
|
tenantID.String(), id.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items, err := scanSQLiteSuggestions(rows)
|
|
if err != nil || len(items) == 0 {
|
|
return nil, err
|
|
}
|
|
if _, _, err := s.resolveSkill(ctx, items[0].SkillID); err != nil {
|
|
return nil, err
|
|
}
|
|
return &items[0], nil
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) UpdateSuggestionStatus(ctx context.Context, id uuid.UUID, status, actorType, actorID string) (*store.SkillImprovementSuggestion, error) {
|
|
tenantID := tenantIDForInsert(ctx)
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
res, err := s.db.ExecContext(ctx,
|
|
`UPDATE skill_improvement_suggestions
|
|
SET status = ?, reviewed_by_actor_type = ?, reviewed_by_actor_id = ?, reviewed_at = ?, updated_at = ?
|
|
WHERE tenant_id = ? AND id = ?`,
|
|
status, actorType, actorID, now, now, tenantID.String(), id.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return nil, fmt.Errorf("suggestion not found")
|
|
}
|
|
return s.GetSuggestion(ctx, id)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) MarkSuggestionApplied(ctx context.Context, id uuid.UUID, version int, actorType, actorID string) (*store.SkillImprovementSuggestion, error) {
|
|
tenantID := tenantIDForInsert(ctx)
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
res, err := s.db.ExecContext(ctx,
|
|
`UPDATE skill_improvement_suggestions
|
|
SET status = 'applied', applied_version = ?, reviewed_by_actor_type = ?,
|
|
reviewed_by_actor_id = ?, reviewed_at = COALESCE(reviewed_at, ?), updated_at = ?
|
|
WHERE tenant_id = ? AND id = ?`,
|
|
version, actorType, actorID, now, now, tenantID.String(), id.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return nil, fmt.Errorf("suggestion not found")
|
|
}
|
|
return s.GetSuggestion(ctx, id)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) CreateSkillVersion(ctx context.Context, v store.SkillVersion) (*store.SkillVersion, error) {
|
|
if _, _, err := s.resolveSkill(ctx, v.SkillID); err != nil {
|
|
return nil, err
|
|
}
|
|
if v.ID == uuid.Nil {
|
|
v.ID = uuid.New()
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
suggestionID := ""
|
|
if v.CreatedFromSuggestionID != nil {
|
|
suggestionID = v.CreatedFromSuggestionID.String()
|
|
}
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO skill_versions
|
|
(id, tenant_id, skill_id, version, content_hash, changed_files, created_by_actor_type,
|
|
created_by_actor_id, created_from_suggestion_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULLIF(?, ''), ?)`,
|
|
v.ID.String(), tenantID.String(), v.SkillID.String(), v.Version, v.ContentHash,
|
|
string(jsonOrDefault(v.ChangedFiles, "[]")), v.CreatedByActorType, v.CreatedByActorID, suggestionID, now,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.GetSkillVersion(ctx, v.SkillID, v.Version)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) ListSkillVersions(ctx context.Context, skillID uuid.UUID, limit int) ([]store.SkillVersion, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, tenant_id, skill_id, version, content_hash, changed_files,
|
|
COALESCE(created_by_actor_type,''), COALESCE(created_by_actor_id,''),
|
|
created_from_suggestion_id, created_at
|
|
FROM skill_versions WHERE tenant_id = ? AND skill_id = ?
|
|
ORDER BY version DESC LIMIT ?`,
|
|
tenantID.String(), skillID.String(), limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanSQLiteSkillVersions(rows)
|
|
}
|
|
|
|
func (s *SQLiteSkillEvolutionStore) GetSkillVersion(ctx context.Context, skillID uuid.UUID, version int) (*store.SkillVersion, error) {
|
|
if _, _, err := s.resolveSkill(ctx, skillID); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantID := tenantIDForInsert(ctx)
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, tenant_id, skill_id, version, content_hash, changed_files,
|
|
COALESCE(created_by_actor_type,''), COALESCE(created_by_actor_id,''),
|
|
created_from_suggestion_id, created_at
|
|
FROM skill_versions WHERE tenant_id = ? AND skill_id = ? AND version = ?`,
|
|
tenantID.String(), skillID.String(), version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items, err := scanSQLiteSkillVersions(rows)
|
|
if err != nil || len(items) == 0 {
|
|
return nil, err
|
|
}
|
|
return &items[0], nil
|
|
}
|
|
|
|
func scanSQLiteSuggestions(rows *sql.Rows) ([]store.SkillImprovementSuggestion, error) {
|
|
var out []store.SkillImprovementSuggestion
|
|
for rows.Next() {
|
|
var sg store.SkillImprovementSuggestion
|
|
var idStr, tenantStr, skillStr string
|
|
var evidence, draft []byte
|
|
var reviewedAt nullSqliteTime
|
|
var applied sql.NullInt64
|
|
var created, updated sqliteTime
|
|
if err := rows.Scan(&idStr, &tenantStr, &skillStr, &sg.SkillSlug, &sg.SuggestionType, &sg.Status,
|
|
&sg.Reason, &evidence, &draft, &sg.TargetFile, &sg.CreatedByActorType, &sg.CreatedByActorID,
|
|
&sg.ReviewedByActorType, &sg.ReviewedByActorID, &reviewedAt, &applied,
|
|
&created, &updated); err != nil {
|
|
return nil, err
|
|
}
|
|
sg.ID, _ = uuid.Parse(idStr)
|
|
sg.TenantID, _ = uuid.Parse(tenantStr)
|
|
sg.SkillID, _ = uuid.Parse(skillStr)
|
|
sg.Evidence = evidence
|
|
sg.DraftPatch = draft
|
|
if reviewedAt.Valid {
|
|
t := reviewedAt.Time
|
|
sg.ReviewedAt = &t
|
|
}
|
|
if applied.Valid {
|
|
v := int(applied.Int64)
|
|
sg.AppliedVersion = &v
|
|
}
|
|
sg.CreatedAt = created.Time
|
|
sg.UpdatedAt = updated.Time
|
|
out = append(out, sg)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func scanSQLiteSkillVersions(rows *sql.Rows) ([]store.SkillVersion, error) {
|
|
var out []store.SkillVersion
|
|
for rows.Next() {
|
|
var v store.SkillVersion
|
|
var idStr, tenantStr, skillStr string
|
|
var changed []byte
|
|
var suggestion sql.NullString
|
|
var created sqliteTime
|
|
if err := rows.Scan(&idStr, &tenantStr, &skillStr, &v.Version, &v.ContentHash, &changed,
|
|
&v.CreatedByActorType, &v.CreatedByActorID, &suggestion, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
v.ID, _ = uuid.Parse(idStr)
|
|
v.TenantID, _ = uuid.Parse(tenantStr)
|
|
v.SkillID, _ = uuid.Parse(skillStr)
|
|
v.ChangedFiles = changed
|
|
if suggestion.Valid && suggestion.String != "" {
|
|
parsed, _ := uuid.Parse(suggestion.String)
|
|
v.CreatedFromSuggestionID = &parsed
|
|
}
|
|
v.CreatedAt = created.Time
|
|
out = append(out, v)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func jsonOrDefault(raw json.RawMessage, fallback string) json.RawMessage {
|
|
if len(raw) == 0 {
|
|
return json.RawMessage(fallback)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
var _ store.SkillEvolutionStore = (*SQLiteSkillEvolutionStore)(nil)
|