oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1// handle_grid.go is the grid-growth and format-write surface a bulk
2// importer needs and ordinary editing does not: EnsureRow/EnsureCol grow
3// a tab's row/col set on demand for a cell addressed by spreadsheet
4// position, SetFormat interns and assigns a cell's format (the write side
5// of Handle.Format, which is read-only — see handle.go), and RawInput
6// reads back the exact text a cell was last Set with, so an exporter can
7// tell a formula from a literal and re-emit the formula verbatim rather
8// than only its computed result.
9package store
10
11import (
12 "crypto/sha256"
13 "encoding/hex"
14 "errors"
15 "fmt"
16 "strconv"
17 "strings"
18
19 "gorm.io/gorm"
20)
21
22// EnsureRow returns the id of the row at pos, creating it if this tab has
23// none there yet. Idempotent: a second call for the same pos returns the
24// same id without writing anything.
25//
26// (TabID, Position) is a uniqueIndex (models.go), not just a plain one:
27// within THIS handle, its own mutex plus rowByPos already make a
28// duplicate Create impossible. But a second process — another app
29// instance, a retried background job, a double-clicked import — builds
30// its own rowByPos and can lose exactly this race: both see no row at
31// pos, both Create. Without the constraint that succeeds twice, leaving
32// two Row records claiming one grid coordinate. With it, the loser's
33// Create fails on the unique violation, which is not treated as a
34// failure here: it means someone else already created the row this call
35// wanted, so this re-reads it by (TabID, pos) instead of propagating the
36// error — the same recovery shape internFormat uses below for
37// Format.Digest's own uniqueIndex.
38func (h *Handle) EnsureRow(pos int) (uint64, error) {
39 h.mu.Lock()
40 defer h.mu.Unlock()
41 if id, ok := h.rowByPos[pos]; ok {
42 return id, nil
43 }
44 r := Row{TabID: h.tabID, Position: pos}
45 if err := h.db.Create(&r).Error; err != nil {
46 var existing Row
47 if err2 := h.db.Where("tab_id = ? AND position = ?", h.tabID, pos).First(&existing).Error; err2 == nil {
48 h.rowPos[existing.ID] = pos
49 h.rowByPos[pos] = existing.ID
50 return existing.ID, nil
51 }
52 return 0, fmt.Errorf("store: create row at position %d: %w", pos, err)
53 }
54 h.rowPos[r.ID] = pos
55 h.rowByPos[pos] = r.ID
56 return r.ID, nil
57}
58
59// EnsureCol is EnsureRow's column counterpart, including the same
60// unique-violation-means-someone-else-won-the-race recovery.
61func (h *Handle) EnsureCol(pos int) (uint64, error) {
62 h.mu.Lock()
63 defer h.mu.Unlock()
64 if id, ok := h.colByPos[pos]; ok {
65 return id, nil
66 }
67 c := Col{TabID: h.tabID, Position: pos}
68 if err := h.db.Create(&c).Error; err != nil {
69 var existing Col
70 if err2 := h.db.Where("tab_id = ? AND position = ?", h.tabID, pos).First(&existing).Error; err2 == nil {
71 h.colPos[existing.ID] = pos
72 h.colByPos[pos] = existing.ID
73 return existing.ID, nil
74 }
75 return 0, fmt.Errorf("store: create col at position %d: %w", pos, err)
76 }
77 h.colPos[c.ID] = pos
78 h.colByPos[pos] = c.ID
79 return c.ID, nil
80}
81
82// PosID pairs a row or column's spreadsheet position with its stable id.
83type PosID struct {
84 Pos int
85 ID uint64
86}
87
88// GridRows returns every row this tab has a Row record for, sorted by
89// position ascending. An exporter walks this (and GridCols) to know which
90// spreadsheet coordinates to visit — Handle otherwise only answers
91// questions about one cell at a time.
92func (h *Handle) GridRows() []PosID {
93 h.mu.Lock()
94 defer h.mu.Unlock()
95 return sortedPosIDs(h.rowByPos)
96}
97
98// GridCols is GridRows' column counterpart.
99func (h *Handle) GridCols() []PosID {
100 h.mu.Lock()
101 defer h.mu.Unlock()
102 return sortedPosIDs(h.colByPos)
103}
104
105func sortedPosIDs(byPos map[int]uint64) []PosID {
106 out := make([]PosID, 0, len(byPos))
107 for pos, id := range byPos {
108 out = append(out, PosID{Pos: pos, ID: id})
109 }
110 for i := 1; i < len(out); i++ {
111 for j := i; j > 0 && out[j].Pos < out[j-1].Pos; j-- {
112 out[j], out[j-1] = out[j-1], out[j]
113 }
114 }
115 return out
116}
117
118// CellCoord is a cell's stable, real identity — the (row, col) shape
119// Handle's own public methods (Set, Value, Format, RawInput, SetFormat)
120// all take.
121type CellCoord struct{ RowID, ColID uint64 }
122
123// Cells returns every cell this tab actually has a Cell row for — content,
124// a format, or both (models.go: Cell is sparse, so this is the complete
125// non-default set). An exporter walking GridRows x GridCols directly
126// would visit every row/col PAIR, which is quadratic in the wrong case
127// (many cells scattered across many distinct rows and columns, e.g. a
128// diagonal); this is the actual population, read straight from storage
129// the same way Format already does (unlike Value, which reads through
130// the in-memory graph — see Format's own doc comment on handle.go).
131func (h *Handle) Cells() ([]CellCoord, error) {
132 var cells []Cell
133 if err := h.db.Select("row_id", "col_id").
134 Where("tab_id = ?", h.tabID).Find(&cells).Error; err != nil {
135 return nil, fmt.Errorf("store: list cells: %w", err)
136 }
137 out := make([]CellCoord, len(cells))
138 for i, c := range cells {
139 out[i] = CellCoord{RowID: c.RowID, ColID: c.ColID}
140 }
141 return out, nil
142}
143
144// RawInput returns the exact text a cell was last Set with (or loaded
145// with, on Open) — never the computed value. false means this handle has
146// nothing recorded for the coordinate: either it is not part of the tab,
147// or it has always been blank.
148func (h *Handle) RawInput(rowID, colID uint64) (string, bool) {
149 h.mu.Lock()
150 defer h.mu.Unlock()
151 key, ok := h.cellKey(rowID, colID)
152 if !ok {
153 return "", false
154 }
155 raw, ok := h.rawText[key]
156 return raw, ok
157}
158
159// SetFormat interns f (Format is deduplicated by content — models.go) and
160// assigns it to the cell at (rowID, colID), creating a Cell row with
161// empty sealed input if the cell is otherwise blank: FormatID lives on
162// Cell, so a formatted-but-empty cell (a coloured header nothing has been
163// typed into yet) still needs a row to carry it.
164func (h *Handle) SetFormat(rowID, colID uint64, f Format) error {
165 h.mu.Lock()
166 defer h.mu.Unlock()
167 if _, ok := h.cellKey(rowID, colID); !ok {
168 return fmt.Errorf("store: row %d col %d is not part of tab %d", rowID, colID, h.tabID)
169 }
170 formatID, err := h.internFormat(f)
171 if err != nil {
172 return err
173 }
174 return h.assignFormat(rowID, colID, formatID)
175}
176
177// internFormat finds the existing Format row matching f's content
178// (by digest) or creates one. A duplicate-key race against a concurrent
179// interner of the identical format is resolved by re-reading rather than
180// failing: the digest's uniqueIndex is what makes that safe.
181func (h *Handle) internFormat(f Format) (uint64, error) {
182 digest := formatDigest(f)
183
184 var existing Format
185 err := h.db.Where("digest = ?", digest).First(&existing).Error
186 if err == nil {
187 return existing.ID, nil
188 }
189 if !errors.Is(err, gorm.ErrRecordNotFound) {
190 return 0, fmt.Errorf("store: look up format: %w", err)
191 }
192
193 f.Digest = digest
194 if err := h.db.Create(&f).Error; err != nil {
195 // Another writer interned the same content between the lookup
196 // above and this Create — re-read rather than propagating what
197 // is, from the caller's point of view, not really a failure.
198 var again Format
199 if err2 := h.db.Where("digest = ?", digest).First(&again).Error; err2 == nil {
200 return again.ID, nil
201 }
202 return 0, fmt.Errorf("store: create format: %w", err)
203 }
204 return f.ID, nil
205}
206
207// formatDigest canonicalizes f's fields into the string Format.Digest
208// hashes — every field that participates in Format's own uniqueIndex
209// (models.go), and none that doesn't (ID, Digest itself).
210func formatDigest(f Format) string {
211 var sb strings.Builder
212 sb.WriteString(f.NumberFmt)
213 sb.WriteByte('|')
214 sb.WriteString(strconv.FormatBool(f.Bold))
215 sb.WriteByte('|')
216 sb.WriteString(strconv.FormatBool(f.Italic))
217 sb.WriteByte('|')
218 if f.FillIntentH != nil {
219 sb.WriteString(strconv.FormatFloat(*f.FillIntentH, 'g', -1, 64))
220 }
221 sb.WriteByte('|')
222 if f.FillIntentC != nil {
223 sb.WriteString(strconv.FormatFloat(*f.FillIntentC, 'g', -1, 64))
224 }
225 sb.WriteByte('|')
226 sb.WriteString(f.FillHex)
227 sb.WriteByte('|')
228 // InkHex participates for the same reason every other field does:
229 // formats are interned by this digest, so a field left out here
230 // makes two genuinely different formats collide onto one row, and
231 // the second cell silently adopts the first's colour.
232 sb.WriteString(f.InkHex)
233 sb.WriteByte('|')
234 if f.InkIntentH != nil {
235 sb.WriteString(strconv.FormatFloat(*f.InkIntentH, 'g', -1, 64))
236 }
237 sb.WriteByte('|')
238 if f.InkIntentC != nil {
239 sb.WriteString(strconv.FormatFloat(*f.InkIntentC, 'g', -1, 64))
240 }
241 sb.WriteByte('|')
242 sb.WriteString(f.Align)
243 sum := sha256.Sum256([]byte(sb.String()))
244 return hex.EncodeToString(sum[:])
245}
246
247// assignFormat points the cell at (rowID, colID)'s FormatID at formatID,
248// creating the Cell row (with empty sealed input) if none exists yet.
249func (h *Handle) assignFormat(rowID, colID, formatID uint64) error {
250 var cell Cell
251 err := h.db.Where("tab_id = ? AND row_id = ? AND col_id = ?", h.tabID, rowID, colID).
252 First(&cell).Error
253 if errors.Is(err, gorm.ErrRecordNotFound) {
254 sealedEmpty, serr := h.sealer.Seal("cell.input", []byte(""))
255 if serr != nil {
256 return fmt.Errorf("store: seal empty input for format-only cell: %w", serr)
257 }
258 id := formatID
259 return h.db.Create(&Cell{
260 TabID: h.tabID, RowID: rowID, ColID: colID,
261 InputSealed: sealedEmpty, FormatID: &id,
262 }).Error
263 }
264 if err != nil {
265 return fmt.Errorf("store: look up cell for format assignment: %w", err)
266 }
267 id := formatID
268 return h.db.Model(&Cell{}).
269 Where("tab_id = ? AND row_id = ? AND col_id = ?", h.tabID, rowID, colID).
270 Update("format_id", &id).Error
271}
272