@@ -43,6 +43,18 @@ type Document struct {
4343 Metadata map [string ]string
4444 CreatedAt time.Time
4545 UpdatedAt time.Time
46+
47+ // TOCTree is the JSONB blob persisted by the ingest pipeline's
48+ // LLM-driven TOC builder ([]tree.TOCNode marshalled). nil
49+ // (NULL in DB) means "not yet generated" — the expected state
50+ // for non-PDF documents, for documents ingested before the
51+ // 0006 migration, and when the builder failed (builder
52+ // failures are non-fatal and leave this column NULL).
53+ //
54+ // Stored raw so the column round-trips byte-identically
55+ // regardless of slice-element ordering inside the encoder.
56+ // Callers that need the typed shape unmarshal at read time.
57+ TOCTree []byte
4658}
4759
4860// NewDocument inserts a fresh document row in the "pending" state.
@@ -83,7 +95,7 @@ func (p *Pool) GetDocument(ctx context.Context, id tree.DocumentID, orgID, store
8395 }
8496 q := `
8597 SELECT id, org_id, store_id, title, content_type, source_ref, status, error_message,
86- byte_size, metadata, created_at, updated_at
98+ byte_size, metadata, created_at, updated_at, toc_tree
8799 FROM documents WHERE id = $1 AND org_id = $2`
88100 args := []any {string (id ), orgID }
89101 if storeID != "" {
@@ -94,13 +106,14 @@ func (p *Pool) GetDocument(ctx context.Context, id tree.DocumentID, orgID, store
94106
95107 var d Document
96108 var status string
97- var rawMeta []byte
109+ var rawMeta , rawTOC []byte
98110 if err := row .Scan (& d .ID , & d .OrgID , & d .StoreID , & d .Title , & d .ContentType , & d .SourceRef , & status ,
99- & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt ); err != nil {
111+ & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt , & rawTOC ); err != nil {
100112 return nil , mapErr (err )
101113 }
102114 d .Status = DocumentStatus (status )
103115 d .Metadata = unmarshalMeta (rawMeta )
116+ d .TOCTree = rawTOC
104117 return & d , nil
105118}
106119
@@ -111,18 +124,19 @@ func (p *Pool) GetDocument(ctx context.Context, id tree.DocumentID, orgID, store
111124func (p * Pool ) GetDocumentForWorker (ctx context.Context , id tree.DocumentID ) (* Document , error ) {
112125 row := p .QueryRow (ctx , `
113126 SELECT id, org_id, store_id, title, content_type, source_ref, status, error_message,
114- byte_size, metadata, created_at, updated_at
127+ byte_size, metadata, created_at, updated_at, toc_tree
115128 FROM documents WHERE id = $1` , string (id ))
116129
117130 var d Document
118131 var status string
119- var rawMeta []byte
132+ var rawMeta , rawTOC []byte
120133 if err := row .Scan (& d .ID , & d .OrgID , & d .StoreID , & d .Title , & d .ContentType , & d .SourceRef , & status ,
121- & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt ); err != nil {
134+ & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt , & rawTOC ); err != nil {
122135 return nil , mapErr (err )
123136 }
124137 d .Status = DocumentStatus (status )
125138 d .Metadata = unmarshalMeta (rawMeta )
139+ d .TOCTree = rawTOC
126140 return & d , nil
127141}
128142
@@ -143,6 +157,24 @@ func (p *Pool) SetDocumentTitle(ctx context.Context, id tree.DocumentID, title s
143157 return mapErr (err )
144158}
145159
160+ // UpdateDocumentTOCTree persists the LLM-built table-of-contents
161+ // tree onto the documents.toc_tree column. treeJSON is the already
162+ // JSON-marshalled []tree.TOCNode; pass a nil slice to clear (writes
163+ // SQL NULL — the "not yet generated" state). Mirrors
164+ // UpdateSectionSummaryAxes so the column can be patched
165+ // independently of the rest of the document row.
166+ func (p * Pool ) UpdateDocumentTOCTree (ctx context.Context , id tree.DocumentID , treeJSON []byte ) error {
167+ var arg any
168+ if len (treeJSON ) > 0 {
169+ arg = treeJSON
170+ }
171+ _ , err := p .Exec (ctx , `
172+ UPDATE documents
173+ SET toc_tree = $2, updated_at = now()
174+ WHERE id = $1` , string (id ), arg )
175+ return mapErr (err )
176+ }
177+
146178// ListDocumentsOpts controls pagination + filtering for ListDocuments.
147179type ListDocumentsOpts struct {
148180 // OrgID restricts the listing to a single tenant. Required.
@@ -197,7 +229,7 @@ func (p *Pool) ListDocuments(ctx context.Context, o ListDocumentsOpts) ([]Docume
197229
198230 q := `
199231 SELECT id, org_id, store_id, title, content_type, source_ref, status, error_message,
200- byte_size, metadata, created_at, updated_at
232+ byte_size, metadata, created_at, updated_at, toc_tree
201233 FROM documents ` + where + `
202234 ORDER BY created_at DESC
203235 LIMIT $` + itoa (next )
@@ -212,13 +244,14 @@ func (p *Pool) ListDocuments(ctx context.Context, o ListDocumentsOpts) ([]Docume
212244 for rows .Next () {
213245 var d Document
214246 var status string
215- var rawMeta []byte
247+ var rawMeta , rawTOC []byte
216248 if err := rows .Scan (& d .ID , & d .OrgID , & d .StoreID , & d .Title , & d .ContentType , & d .SourceRef , & status ,
217- & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt ); err != nil {
249+ & d .ErrorMessage , & d .ByteSize , & rawMeta , & d .CreatedAt , & d .UpdatedAt , & rawTOC ); err != nil {
218250 return nil , time.Time {}, err
219251 }
220252 d .Status = DocumentStatus (status )
221253 d .Metadata = unmarshalMeta (rawMeta )
254+ d .TOCTree = rawTOC
222255 out = append (out , d )
223256 }
224257 if err := rows .Err (); err != nil {
0 commit comments