package artifacts import ( "context" "database/sql" "errors" "fmt" "time" "git.wntrmute.dev/kyle/exo/core" "git.wntrmute.dev/kyle/exo/db" ) // Citation holds bibliographic information for an artifact. type Citation struct { ID string DOI string Title string Year int Published time.Time Authors []string Publisher *Publisher Source string Abstract string Metadata core.Metadata } // Update applies non-zero fields from base into c where c's fields are empty. func (c *Citation) Update(base *Citation) { if c.DOI == "" { c.DOI = base.DOI } if c.Title == "" { c.Title = base.Title } if c.Year == 0 { c.Year = base.Year } if c.Published.IsZero() { c.Published = base.Published } if len(c.Authors) == 0 { c.Authors = base.Authors } if c.Publisher != nil && c.Publisher.Name == "" { c.Publisher.Name = base.Publisher.Name } if c.Publisher != nil && c.Publisher.Address == "" { c.Publisher.Address = base.Publisher.Address } if c.Source == "" { c.Source = base.Source } if c.Abstract == "" { c.Abstract = base.Abstract } for key, value := range base.Metadata { if _, ok := c.Metadata[key]; !ok { c.Metadata[key] = value } } } func (c *Citation) present(ctx context.Context, tx *sql.Tx) (bool, error) { var id string row := tx.QueryRowContext(ctx, `SELECT id FROM citations WHERE id=?`, c.ID) if err := row.Scan(&id); err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil } return false, fmt.Errorf("artifacts: failed to look up citation: %w", err) } return true, nil } // Store persists a Citation and its associated publisher and authors. func (c *Citation) Store(ctx context.Context, tx *sql.Tx) error { if c.ID == "" { c.ID = core.NewUUID() } else { ok, err := c.present(ctx, tx) if err != nil { return fmt.Errorf("artifacts: couldn't store citation: %w", err) } if ok { return nil } } if c.Publisher != nil { if err := c.Publisher.Store(ctx, tx); err != nil { return fmt.Errorf("artifacts: failed to store citation publisher: %w", err) } } publisherID := "" if c.Publisher != nil { publisherID = c.Publisher.ID } // Insert the citation row first so FK-dependent rows (authors, metadata) can reference it. _, err := tx.ExecContext(ctx, `INSERT INTO citations (id, doi, title, year, published, publisher, source, abstract) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, c.ID, c.DOI, c.Title, c.Year, db.ToDBTime(c.Published), publisherID, c.Source, c.Abstract) if err != nil { return fmt.Errorf("artifacts: failed to store citation: %w", err) } if err := storeAuthors(ctx, tx, c.ID, c.Authors); err != nil { return fmt.Errorf("artifacts: failed to store citation authors: %w", err) } if err := StoreMetadata(ctx, tx, c.ID, c.Metadata); err != nil { return fmt.Errorf("artifacts: failed to store citation metadata: %w", err) } return nil } // Get retrieves a Citation by its ID, including authors, publisher, and metadata. func (c *Citation) Get(ctx context.Context, tx *sql.Tx) error { if c.ID == "" { return fmt.Errorf("artifacts: citation missing ID: %w", core.ErrNoID) } // Get authors. rows, err := tx.QueryContext(ctx, `SELECT author_name FROM authors WHERE citation_id=?`, c.ID) if err != nil { return fmt.Errorf("artifacts: failed to retrieve citation authors: %w", err) } defer func() { _ = rows.Close() }() c.Authors = nil for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return fmt.Errorf("artifacts: failed to scan author: %w", err) } c.Authors = append(c.Authors, name) } if err := rows.Err(); err != nil { return err } // Get citation fields. c.Publisher = &Publisher{} var published string row := tx.QueryRowContext(ctx, `SELECT doi, title, year, published, publisher, source, abstract FROM citations WHERE id=?`, c.ID) if err := row.Scan(&c.DOI, &c.Title, &c.Year, &published, &c.Publisher.ID, &c.Source, &c.Abstract); err != nil { return fmt.Errorf("artifacts: failed to retrieve citation: %w", err) } c.Published, err = db.FromDBTime(published, nil) if err != nil { return err } if c.Publisher.ID != "" { ok, err := c.Publisher.Get(ctx, tx) if err != nil { return fmt.Errorf("artifacts: failed to retrieve citation publisher: %w", err) } if !ok { return fmt.Errorf("artifacts: citation references missing publisher %s", c.Publisher.ID) } } c.Metadata, err = GetMetadata(ctx, tx, c.ID) if err != nil { return fmt.Errorf("artifacts: failed to retrieve citation metadata: %w", err) } return nil } func storeAuthors(ctx context.Context, tx *sql.Tx, citationID string, authors []string) error { for _, name := range authors { // Check if this author already exists for this citation. var existing string row := tx.QueryRowContext(ctx, `SELECT author_name FROM authors WHERE citation_id=? AND author_name=?`, citationID, name) if err := row.Scan(&existing); err == nil { continue // already exists } _, err := tx.ExecContext(ctx, `INSERT INTO authors (citation_id, author_name) VALUES (?, ?)`, citationID, name) if err != nil { return fmt.Errorf("artifacts: failed to store author %q: %w", name, err) } } return nil }