1package sheet23import (4 "errors"5 "slices"6 "strconv"7)89// Linked sources: a Parquet file or a SQLite table or query linked as a10// read-only table too big for any grid. A source is a tab of its own, a11// paged sheet, holding no cells: its table is one region at A1 whose12// definition (a LinkSource with Paged set) names what it reads, so it13// is a table as a linked file's is, sales[amount] and nu.sales, but its14// rows are never written into cells. The UI shows a window of them as15// it scrolls; formulas and pivot tables ask the workbook's SourceHost16// (sourceask.go), which answers them by streaming the source in the17// background. Its table's rows may go past the grid's last row.1819// SourceShape is what a source is, as its host finds on opening it:20// its columns' names, the formats their types show in, which hold21// numbers, and how many rows it has under the header.22type SourceShape struct {23 Cols []string24 Formats []Format25 Numeric []bool26 Rows int27}2829// SourceSort is a column a source's view is sorted by.30type SourceSort struct {31 Col int32 Desc bool33}3435// SourceFilter is a condition a source's view shows the rows meeting,36// on a column.37type SourceFilter struct {38 Col int39 Cond Condition40}4142// SourceOrder is how a source's tab sorts and filters the rows it43// shows. Formulas read the source in its own order, every row, as they44// read a sheet whatever its filter hides.45type SourceOrder struct {46 Sort []SourceSort47 Filter []SourceFilter48}4950// IsZero reports whether the order is the source's own, every row.51func (o *SourceOrder) IsZero() bool { return o == nil || len(o.Sort) == 0 && len(o.Filter) == 0 }5253// sourceMeta is what a source's host found of it, never undone.54type sourceMeta struct {55 shape SourceShape56 known bool // shape holds what the host found57 err string // why the source can't be read, or ""58}5960// Errors of sources.61var (62 // ErrSourceEdit is what typing into a source's tab says.63 ErrSourceEdit = errors.New("A linked source is read-only: its rows are the file's")64 // ErrNoSource is returned for a source the workbook doesn't hold.65 ErrNoSource = errors.New("There's no linked source by that name")66)6768// SourceInfo is a linked source as the UI shows it.69type SourceInfo struct {70 // Name names it in formulas (sales[amount], nu.sales) and in the71 // SourceHost's questions.72 Name string73 Sheet *Sheet74 Source LinkSource75 Shape SourceShape76 // Known is set once its host has opened it; Err says why it can't.77 Known bool78 Err string79}8081// Source returns the linked source the sheet is the tab of.82func (s *Sheet) Source() (SourceInfo, bool) {83 if r, ok := s.pagedRegion(); ok {84 return s.sourceInfo(r), true85 }86 return SourceInfo{}, false87}8889// IsSource reports whether the sheet is a linked source's tab.90func (s *Sheet) IsSource() bool {91 _, ok := s.pagedRegion()92 return ok93}9495func (s *Sheet) sourceInfo(r Region) SourceInfo {96 info := SourceInfo{Name: r.Name, Sheet: s, Source: r.File}97 if sm := s.meta(nameKey(r.Name)).src; sm != nil {98 info.Shape, info.Known, info.Err = sm.shape, sm.known, sm.err99 }100 return info101}102103// Sources returns the workbook's linked sources, in the order of their104// tabs.105func (w *Workbook) Sources() []SourceInfo {106 var out []SourceInfo107 for _, s := range w.sheets {108 if info, ok := s.Source(); ok {109 out = append(out, info)110 }111 }112 return out113}114115// LookupSource finds a linked source by name, ignoring case.116func (w *Workbook) LookupSource(name string) (SourceInfo, bool) {117 if s, r, ok := w.Region(name); ok && r.File.Paged {118 return s.sourceInfo(r), true119 }120 return SourceInfo{}, false121}122123// hasSources reports whether the workbook holds a linked source.124func (w *Workbook) hasSources() bool {125 return slices.ContainsFunc(w.sheets, (*Sheet).IsSource)126}127128// AddSource adds a tab linking src as a source named name (a free name129// made from the file's when ""), after the sheet after, as one undo130// step. The tab and the table share the name. Its host opens it.131func (w *Workbook) AddSource(name string, src LinkSource, after *Sheet) (*Sheet, error) {132 if name == "" {133 name = w.sourceName(src.Path)134 }135 if err := w.checkRegionName(name, false); err != nil {136 return nil, err137 }138 if err := w.checkName(nil, name); err != nil {139 return nil, err140 }141 src.Paged = true142 s := w.newSheet(name)143 s.regions.list = []Region{{Name: name, File: src}}144 at := len(w.sheets)145 if i := w.Index(after); i >= 0 {146 at = i + 1147 }148 w.change(s, "link source "+name, Rect{}, func() {149 w.recordSheets()150 w.insert(s, at)151 })152 return s, nil153}154155// SetSourceOrder changes how a source's tab sorts and filters its rows,156// as one undo step; o nil shows the source's own order.157func (w *Workbook) SetSourceOrder(name string, o *SourceOrder) error {158 s, r, ok := w.Region(name)159 if !ok || !r.File.Paged {160 return ErrNoSource161 }162 if o.IsZero() {163 o = nil164 }165 if o == nil && r.File.Order == nil {166 return nil167 }168 k := nameKey(r.Name)169 s.change("order "+r.Name, Rect{}, func() {170 st := s.regionsCopy()171 st.list[s.regionIndex(k)].File.Order = o172 s.putRegions(st)173 })174 return nil175}176177// SetSourceShape records what the host found on opening a source, or178// why it couldn't (err), and recalculates what reads it. It isn't an179// edit: the file is what it is.180func (w *Workbook) SetSourceShape(name string, shape SourceShape, err string) {181 s, r, ok := w.Region(name)182 if !ok || !r.File.Paged {183 return184 }185 s.meta(nameKey(r.Name)).src = &sourceMeta{shape: shape, known: err == "", err: err}186 w.SourceChanged(name)187}188189// SourceChanged recalculates what reads a source, whose file changed:190// the formulas that asked it something or name its table, and the191// pivot tables over it. Its host has forgotten its answers.192func (w *Workbook) SourceChanged(name string) {193 s, r, ok := w.Region(name)194 if !ok || !r.File.Paged {195 return196 }197 k := nameKey(r.Name)198 var changed []loc199 for _, set := range []map[loc]struct{}{w.src.users[k], w.nameUsers[k], w.nameUsers[nameKey(regionPrefix+k)]} {200 for l := range set {201 if l.s.live && l.s.cells.has(l.a) {202 changed = append(changed, l)203 }204 }205 }206 delete(w.src.users, k)207 for _, p := range w.pivotsOn(s) {208 p.pivot.stale = true209 }210 w.recalcFrom(changed, false)211}212213// sourceTable is the table of the paged region r on s, header row214// included, and whether it is known: its rows may pass the grid's.215func (s *Sheet) sourceTable(r Region) (Rect, SourceShape, bool) {216 sm := s.meta(nameKey(r.Name)).src217 if sm == nil || !sm.known || len(sm.shape.Cols) == 0 {218 return Rect{}, SourceShape{}, false219 }220 return Rect{To: Addr{Col: len(sm.shape.Cols) - 1, Row: sm.shape.Rows}}, sm.shape, true221}222223// sourceErr is why the paged region r can't be read, "" when it can or224// its host hasn't said yet.225func (s *Sheet) sourceErr(r Region) string {226 if sm := s.meta(nameKey(r.Name)).src; sm != nil {227 return sm.err228 }229 return ""230}231232// sourceName is a free name for a source reading the file at path, as233// both a table and a tab: the file's name, numbered when taken.234func (w *Workbook) sourceName(path string) string {235 name := w.linkName(path)236 base := name237 for n := 2; w.checkName(nil, name) != nil || w.checkRegionName(name, false) != nil; n++ {238 name = base + "_" + strconv.Itoa(n)239 }240 return name241}