Move document_records duty-slot writes into docgov, project ledger heads through docgov/freedoc ports, and workspace deliverable I/O through DeliverableStore so arch-check baseline drops to sql_write:1 path:13.
494 lines
17 KiB
Go
494 lines
17 KiB
Go
package docrevision
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/app/uow"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// ErrNeedsContent indicates the document has no server-side body to bootstrap a ledger from.
|
|
var ErrNeedsContent = errors.New("docrevision: needs content to bootstrap")
|
|
|
|
const ErrCodeNeedsContent = "DOCUMENT_REVISION_NEEDS_CONTENT"
|
|
|
|
// BootstrapRequest supplies the fact-source markdown used for first-access ledger creation.
|
|
type BootstrapRequest struct {
|
|
DocID string
|
|
Kind string // controlled|freeform
|
|
Markdown string
|
|
ProjectID int64
|
|
WorkDir string
|
|
LifecycleState string
|
|
Actor ActorSnapshot
|
|
Origin string // template|migration
|
|
}
|
|
|
|
func (s *Service) usingPG() bool {
|
|
return s != nil && s.pool != nil
|
|
}
|
|
|
|
// hydrateDocLocked loads ledger rows for docID from PostgreSQL into memory maps.
|
|
// Caller must hold s.mu.
|
|
func (s *Service) hydrateDocLocked(ctx context.Context, docID string) error {
|
|
if !s.usingPG() || docID == "" {
|
|
return nil
|
|
}
|
|
if root, ok := s.roots[docID]; ok && root.LedgerVersionID != nil {
|
|
return nil
|
|
}
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id, doc_id, content_sha256, parent_snapshot_id, block_map, source_kind,
|
|
actor_user_id, actor_ref, actor_snapshot, source_run_id, created_at
|
|
FROM document_snapshots WHERE doc_id = $1 ORDER BY id`, docID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
type snapRow struct {
|
|
snap Snapshot
|
|
}
|
|
var snaps []snapRow
|
|
shas := map[string]struct{}{}
|
|
for rows.Next() {
|
|
var sn Snapshot
|
|
var blockRaw []byte
|
|
var actorRaw []byte
|
|
if err := rows.Scan(&sn.ID, &sn.DocID, &sn.ContentSHA256, &sn.ParentSnapshotID, &blockRaw,
|
|
&sn.SourceKind, &sn.ActorUserID, &sn.ActorRef, &actorRaw, &sn.SourceRunID, &sn.CreatedAt); err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(blockRaw, &sn.BlockMap); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal block_map for doc %s snapshot %d: %w", docID, sn.ID, err)
|
|
}
|
|
if err := json.Unmarshal(actorRaw, &sn.ActorSnapshot); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s snapshot %d: %w", docID, sn.ID, err)
|
|
}
|
|
snaps = append(snaps, snapRow{snap: sn})
|
|
shas[sn.ContentSHA256] = struct{}{}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if len(snaps) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for sha := range shas {
|
|
var md string
|
|
var byteSize int64
|
|
var created time.Time
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT content_markdown, byte_size, created_at FROM document_content_blobs WHERE content_sha256 = $1`, sha).
|
|
Scan(&md, &byteSize, &created)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.blobs[sha] = &ContentBlob{ContentSHA256: sha, ContentMarkdown: md, ByteSize: byteSize, CreatedAt: created}
|
|
}
|
|
for i := range snaps {
|
|
cp := snaps[i].snap
|
|
s.snapshots[cp.ID] = &cp
|
|
if cp.ID > s.nextSnapID {
|
|
s.nextSnapID = cp.ID
|
|
}
|
|
}
|
|
|
|
vrows, err := s.pool.Query(ctx, `
|
|
SELECT id, doc_id, version_no, status, origin_kind, baseline_snapshot_id, head_snapshot_id,
|
|
human_revision_count, source_run_id, change_report_id, opened_at, sealed_at
|
|
FROM document_versions WHERE doc_id = $1 ORDER BY version_no`, docID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer vrows.Close()
|
|
var openVer *Version
|
|
for vrows.Next() {
|
|
var v Version
|
|
if err := vrows.Scan(&v.ID, &v.DocID, &v.VersionNo, &v.Status, &v.OriginKind, &v.BaselineSnapshotID,
|
|
&v.HeadSnapshotID, &v.HumanRevisionCount, &v.SourceRunID, &v.ChangeReportID, &v.OpenedAt, &v.SealedAt); err != nil {
|
|
return err
|
|
}
|
|
cp := v
|
|
s.versions[cp.ID] = &cp
|
|
if cp.ID > s.nextVerID {
|
|
s.nextVerID = cp.ID
|
|
}
|
|
if cp.Status == VersionOpen {
|
|
openVer = &cp
|
|
}
|
|
}
|
|
if err := vrows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
erows, err := s.pool.Query(ctx, `
|
|
SELECT id, doc_id, version_id, event_no, from_snapshot_id, to_snapshot_id, change_kind, actor_kind,
|
|
actor_user_id, actor_ref, actor_snapshot, change_set, edit_session_id, request_id, idempotency_key, created_at
|
|
FROM document_change_events WHERE doc_id = $1 ORDER BY event_no`, docID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer erows.Close()
|
|
for erows.Next() {
|
|
var ev ChangeEvent
|
|
var actorRaw, changeRaw []byte
|
|
if err := erows.Scan(&ev.ID, &ev.DocID, &ev.VersionID, &ev.EventNo, &ev.FromSnapshotID, &ev.ToSnapshotID,
|
|
&ev.ChangeKind, &ev.ActorKind, &ev.ActorUserID, &ev.ActorRef, &actorRaw, &changeRaw,
|
|
&ev.EditSessionID, &ev.RequestID, &ev.IdempotencyKey, &ev.CreatedAt); err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(actorRaw, &ev.ActorSnapshot); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s event %d: %w", docID, ev.ID, err)
|
|
}
|
|
if err := json.Unmarshal(changeRaw, &ev.ChangeSet); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal change_set for doc %s event %d: %w", docID, ev.ID, err)
|
|
}
|
|
cp := ev
|
|
s.events[cp.ID] = &cp
|
|
if cp.IdempotencyKey != "" {
|
|
s.idemp[cp.IdempotencyKey] = cp.ID
|
|
}
|
|
if cp.ID > s.nextEvtID {
|
|
s.nextEvtID = cp.ID
|
|
}
|
|
}
|
|
if err := erows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
rrows, err := s.pool.Query(ctx, `
|
|
SELECT id, doc_id, from_snapshot_id, to_snapshot_id, schema_version, summary, risk_level, risk_items,
|
|
project_impacts, document_impacts, open_questions, analysis_status, model_snapshot, prompt_version,
|
|
deterministic_facts, corrects_report_id, raw_json, created_at
|
|
FROM document_change_reports WHERE doc_id = $1`, docID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rrows.Close()
|
|
for rrows.Next() {
|
|
var rep ChangeReport
|
|
var sumRaw, riskRaw, projRaw, docRaw, qRaw, factRaw []byte
|
|
if err := rrows.Scan(&rep.ID, &rep.DocID, &rep.FromSnapshotID, &rep.ToSnapshotID, &rep.SchemaVersion,
|
|
&sumRaw, &rep.RiskLevel, &riskRaw, &projRaw, &docRaw, &qRaw, &rep.AnalysisStatus,
|
|
&rep.ModelSnapshot, &rep.PromptVersion, &factRaw, &rep.CorrectsReportID, &rep.RawJSON, &rep.CreatedAt); err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(sumRaw, &rep.Summary); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal summary for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
if err := json.Unmarshal(riskRaw, &rep.RiskItems); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal risk_items for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
if err := json.Unmarshal(projRaw, &rep.ProjectImpacts); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal project_impacts for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
if err := json.Unmarshal(docRaw, &rep.DocumentImpacts); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal document_impacts for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
if err := json.Unmarshal(qRaw, &rep.OpenQuestions); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal open_questions for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
if err := json.Unmarshal(factRaw, &rep.DeterministicFacts); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal deterministic_facts for doc %s report %d: %w", docID, rep.ID, err)
|
|
}
|
|
liftReportDisposition(&rep)
|
|
cp := rep
|
|
s.reports[cp.ID] = &cp
|
|
if cp.ID > s.nextRepID {
|
|
s.nextRepID = cp.ID
|
|
}
|
|
}
|
|
if err := rrows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
crows, err := s.pool.Query(ctx, `
|
|
SELECT id, doc_id, input_snapshot_id, output_snapshot_id, run_id, proposal_id, status, authorized_scope,
|
|
quality_result, change_report_id, actor_kind, actor_snapshot, created_at, decided_at
|
|
FROM document_agent_candidates WHERE doc_id = $1`, docID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer crows.Close()
|
|
for crows.Next() {
|
|
var c AgentCandidate
|
|
var actorRaw []byte
|
|
if err := crows.Scan(&c.ID, &c.DocID, &c.InputSnapshotID, &c.OutputSnapshotID, &c.RunID, &c.ProposalID,
|
|
&c.Status, &c.AuthorizedScope, &c.QualityResult, &c.ChangeReportID, &c.ActorKind, &actorRaw,
|
|
&c.CreatedAt, &c.DecidedAt); err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(actorRaw, &c.ActorSnapshot); err != nil {
|
|
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s candidate %d: %w", docID, c.ID, err)
|
|
}
|
|
cp := c
|
|
s.candidates[cp.ID] = &cp
|
|
if cp.ID > s.nextCandID {
|
|
s.nextCandID = cp.ID
|
|
}
|
|
}
|
|
if err := crows.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
root := &DocRootPointers{DocID: docID, RevisionSchemaVer: SchemaVersion, Kind: "controlled", LifecycleState: "drafting"}
|
|
if openVer != nil {
|
|
vid := openVer.ID
|
|
hid := openVer.HeadSnapshotID
|
|
root.LedgerVersionID = &vid
|
|
root.HeadSnapshotID = &hid
|
|
root.HeadRevisionNo = openVer.HumanRevisionCount
|
|
if head := s.snapshots[openVer.HeadSnapshotID]; head != nil {
|
|
root.ContentSHA256 = head.ContentSHA256
|
|
}
|
|
}
|
|
|
|
var ledgerID, headID *int64
|
|
var headRev int
|
|
var workDir, lifecycle string
|
|
var projectID int64
|
|
err = s.pool.QueryRow(ctx, `
|
|
SELECT ledger_version_id, head_snapshot_id, head_revision_no, COALESCE(work_dir,''), COALESCE(current_state,'drafting'), project_id
|
|
FROM document_records WHERE doc_id = $1`, docID).
|
|
Scan(&ledgerID, &headID, &headRev, &workDir, &lifecycle, &projectID)
|
|
if err == nil {
|
|
root.Kind = "controlled"
|
|
root.WorkDir = workDir
|
|
root.LifecycleState = lifecycle
|
|
root.ProjectID = projectID
|
|
if ledgerID != nil {
|
|
root.LedgerVersionID = ledgerID
|
|
}
|
|
if headID != nil {
|
|
root.HeadSnapshotID = headID
|
|
}
|
|
root.HeadRevisionNo = headRev
|
|
} else if errors.Is(err, pgx.ErrNoRows) {
|
|
var fLedger, fHead *int64
|
|
var fRev int
|
|
var fProj int64
|
|
ferr := s.pool.QueryRow(ctx, `
|
|
SELECT ledger_version_id, head_snapshot_id, head_revision_no, project_id
|
|
FROM freeform_documents WHERE doc_id = $1 AND NOT is_deleted`, docID).
|
|
Scan(&fLedger, &fHead, &fRev, &fProj)
|
|
if ferr == nil {
|
|
root.Kind = "freeform"
|
|
root.LifecycleState = "drafting"
|
|
root.ProjectID = fProj
|
|
if fLedger != nil {
|
|
root.LedgerVersionID = fLedger
|
|
}
|
|
if fHead != nil {
|
|
root.HeadSnapshotID = fHead
|
|
}
|
|
root.HeadRevisionNo = fRev
|
|
} else if !errors.Is(ferr, pgx.ErrNoRows) {
|
|
return ferr
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
|
|
if root.LedgerVersionID != nil {
|
|
s.roots[docID] = root
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ensureBlobTX(ctx context.Context, tx pgx.Tx, blob *ContentBlob) error {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO document_content_blobs (content_sha256, content_markdown, byte_size, created_at)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (content_sha256) DO NOTHING`,
|
|
blob.ContentSHA256, blob.ContentMarkdown, blob.ByteSize, blob.CreatedAt)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) insertSnapshotTX(ctx context.Context, tx pgx.Tx, sn *Snapshot) (int64, error) {
|
|
blockRaw, _ := json.Marshal(sn.BlockMap)
|
|
if blockRaw == nil {
|
|
blockRaw = []byte("[]")
|
|
}
|
|
actorRaw, _ := json.Marshal(sn.ActorSnapshot)
|
|
if actorRaw == nil {
|
|
actorRaw = []byte("{}")
|
|
}
|
|
var id int64
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO document_snapshots (
|
|
doc_id, content_sha256, parent_snapshot_id, block_map, source_kind,
|
|
actor_user_id, actor_ref, actor_snapshot, source_run_id, created_at
|
|
) VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8::jsonb,$9,$10) RETURNING id`,
|
|
sn.DocID, sn.ContentSHA256, sn.ParentSnapshotID, blockRaw, sn.SourceKind,
|
|
sn.ActorUserID, sn.ActorRef, actorRaw, sn.SourceRunID, sn.CreatedAt,
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Service) insertVersionTX(ctx context.Context, tx pgx.Tx, v *Version) (int64, error) {
|
|
var id int64
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO document_versions (
|
|
doc_id, version_no, status, origin_kind, baseline_snapshot_id, head_snapshot_id,
|
|
human_revision_count, source_run_id, change_report_id, opened_at, sealed_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id`,
|
|
v.DocID, v.VersionNo, v.Status, v.OriginKind, v.BaselineSnapshotID, v.HeadSnapshotID,
|
|
v.HumanRevisionCount, v.SourceRunID, v.ChangeReportID, v.OpenedAt, v.SealedAt,
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Service) updateVersionTX(ctx context.Context, tx pgx.Tx, v *Version) error {
|
|
_, err := tx.Exec(ctx, `
|
|
UPDATE document_versions SET status=$2, head_snapshot_id=$3, human_revision_count=$4,
|
|
change_report_id=$5, sealed_at=$6 WHERE id=$1`,
|
|
v.ID, v.Status, v.HeadSnapshotID, v.HumanRevisionCount, v.ChangeReportID, v.SealedAt)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) insertEventTX(ctx context.Context, tx pgx.Tx, ev *ChangeEvent) (int64, error) {
|
|
actorRaw, _ := json.Marshal(ev.ActorSnapshot)
|
|
if actorRaw == nil {
|
|
actorRaw = []byte("{}")
|
|
}
|
|
changeRaw, _ := json.Marshal(ev.ChangeSet)
|
|
if changeRaw == nil {
|
|
changeRaw = []byte("[]")
|
|
}
|
|
var id int64
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO document_change_events (
|
|
doc_id, version_id, event_no, from_snapshot_id, to_snapshot_id, change_kind, actor_kind,
|
|
actor_user_id, actor_ref, actor_snapshot, change_set, edit_session_id, request_id, idempotency_key, created_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11::jsonb,$12,$13,$14,$15) RETURNING id`,
|
|
ev.DocID, ev.VersionID, ev.EventNo, ev.FromSnapshotID, ev.ToSnapshotID, ev.ChangeKind, ev.ActorKind,
|
|
ev.ActorUserID, ev.ActorRef, actorRaw, changeRaw, ev.EditSessionID, ev.RequestID, ev.IdempotencyKey, ev.CreatedAt,
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Service) insertReportTX(ctx context.Context, tx pgx.Tx, rep *ChangeReport) (int64, error) {
|
|
sumRaw, _ := json.Marshal(rep.Summary)
|
|
if sumRaw == nil {
|
|
sumRaw = []byte("[]")
|
|
}
|
|
riskRaw, _ := json.Marshal(rep.RiskItems)
|
|
if riskRaw == nil {
|
|
riskRaw = []byte("[]")
|
|
}
|
|
projRaw, _ := json.Marshal(rep.ProjectImpacts)
|
|
if projRaw == nil {
|
|
projRaw = []byte("{}")
|
|
}
|
|
docRaw, _ := json.Marshal(rep.DocumentImpacts)
|
|
if docRaw == nil {
|
|
docRaw = []byte("[]")
|
|
}
|
|
qRaw, _ := json.Marshal(rep.OpenQuestions)
|
|
if qRaw == nil {
|
|
qRaw = []byte("[]")
|
|
}
|
|
factRaw, _ := json.Marshal(rep.DeterministicFacts)
|
|
if factRaw == nil {
|
|
factRaw = []byte("{}")
|
|
}
|
|
var id int64
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO document_change_reports (
|
|
doc_id, from_snapshot_id, to_snapshot_id, schema_version, summary, risk_level, risk_items,
|
|
project_impacts, document_impacts, open_questions, analysis_status, model_snapshot, prompt_version,
|
|
deterministic_facts, corrects_report_id, raw_json, created_at
|
|
) VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11,$12,$13,$14::jsonb,$15,$16,$17)
|
|
RETURNING id`,
|
|
rep.DocID, rep.FromSnapshotID, rep.ToSnapshotID, rep.SchemaVersion, sumRaw, rep.RiskLevel, riskRaw,
|
|
projRaw, docRaw, qRaw, rep.AnalysisStatus, rep.ModelSnapshot, rep.PromptVersion,
|
|
factRaw, rep.CorrectsReportID, rep.RawJSON, rep.CreatedAt,
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Service) updateReportFactsTX(ctx context.Context, tx pgx.Tx, rep *ChangeReport) error {
|
|
if rep == nil || rep.ID == 0 {
|
|
return nil
|
|
}
|
|
stampReportDisposition(rep)
|
|
factRaw, _ := json.Marshal(rep.DeterministicFacts)
|
|
if factRaw == nil {
|
|
factRaw = []byte("{}")
|
|
}
|
|
_, err := tx.Exec(ctx, `UPDATE document_change_reports SET deterministic_facts=$2::jsonb WHERE id=$1`, rep.ID, factRaw)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) insertCandidateTX(ctx context.Context, tx pgx.Tx, c *AgentCandidate) (int64, error) {
|
|
actorRaw, _ := json.Marshal(c.ActorSnapshot)
|
|
if actorRaw == nil {
|
|
actorRaw = []byte("{}")
|
|
}
|
|
var id int64
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO document_agent_candidates (
|
|
doc_id, input_snapshot_id, output_snapshot_id, run_id, proposal_id, status, authorized_scope,
|
|
quality_result, change_report_id, actor_kind, actor_snapshot, created_at, decided_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13) RETURNING id`,
|
|
c.DocID, c.InputSnapshotID, c.OutputSnapshotID, c.RunID, c.ProposalID, c.Status, c.AuthorizedScope,
|
|
c.QualityResult, c.ChangeReportID, c.ActorKind, actorRaw, c.CreatedAt, c.DecidedAt,
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Service) updateCandidateTX(ctx context.Context, tx pgx.Tx, c *AgentCandidate) error {
|
|
_, err := tx.Exec(ctx, `
|
|
UPDATE document_agent_candidates SET status=$2, decided_at=$3, change_report_id=$4, output_snapshot_id=$5
|
|
WHERE id=$1`, c.ID, c.Status, c.DecidedAt, c.ChangeReportID, c.OutputSnapshotID)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) projectRootPointersTX(ctx context.Context, tx pgx.Tx, root *DocRootPointers) error {
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
if s.rootPointers == nil {
|
|
// Unwired unit/PG tests still persist ledger rows; composition root must wire for production.
|
|
return nil
|
|
}
|
|
scope, ok := uow.ScopeFrom(ctx)
|
|
if !ok {
|
|
return fmt.Errorf("docrevision: root pointer projection requires uow scope")
|
|
}
|
|
_ = tx
|
|
return s.rootPointers.Project(ctx, scope, root)
|
|
}
|
|
|
|
func (s *Service) withTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error {
|
|
if !s.usingPG() {
|
|
return nil
|
|
}
|
|
if scope, ok := uow.ScopeFrom(ctx); ok {
|
|
tx, err := uow.AsPG(scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fn(ctx, tx)
|
|
}
|
|
return uow.NewPG(s.pool).Within(ctx, func(scope uow.TxScope) error {
|
|
tx, err := uow.AsPG(scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fn(uow.WithScope(ctx, scope), tx)
|
|
})
|
|
}
|
|
|
|
// Pool exposes the underlying pool for integration tests.
|
|
func (s *Service) Pool() *pgxpool.Pool { return s.pool }
|