From 446cccad817ffe35beaaaf1eae2f5b27eacfc86b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 27 Apr 2026 12:30:44 -0700 Subject: [PATCH] perf(markdown): preload export path metadata --- internal/markdown/export.go | 97 +++++++++++++++++++++++++++----- internal/markdown/export_test.go | 32 +++++++++++ internal/store/query.go | 70 +++++++++++++++++++++++ internal/store/types.go | 5 ++ 4 files changed, 190 insertions(+), 14 deletions(-) diff --git a/internal/markdown/export.go b/internal/markdown/export.go index a2b9742..c5e6467 100644 --- a/internal/markdown/export.go +++ b/internal/markdown/export.go @@ -39,10 +39,14 @@ func (e Exporter) Export(ctx context.Context) (Summary, error) { if err != nil { return Summary{}, err } + paths, err := newPathResolver(ctx, e.Store) + if err != nil { + return Summary{}, err + } var s Summary keep := map[string]bool{} for _, page := range pages { - path, err := e.writePage(ctx, page) + path, err := e.writePage(ctx, paths, page) if err != nil { return s, err } @@ -56,19 +60,10 @@ func (e Exporter) Export(ctx context.Context) (Summary, error) { return s, nil } -func (e Exporter) writePage(ctx context.Context, page store.Page) (string, error) { - spaceName, err := e.Store.SpaceName(ctx, page.SpaceID) - if err != nil { - return "", err - } - teamID, err := e.Store.PageTeamID(ctx, page) - if err != nil { - return "", err - } - teamName, err := e.Store.TeamName(ctx, teamID) - if err != nil { - return "", err - } +func (e Exporter) writePage(ctx context.Context, paths pathResolver, page store.Page) (string, error) { + spaceName := paths.spaceName(page.SpaceID) + teamID := paths.pageTeamID(page) + teamName := paths.teamName(teamID) blocks, err := e.Store.PageBlocks(ctx, page.ID) if err != nil { return "", err @@ -112,6 +107,80 @@ func (e Exporter) writePage(ctx context.Context, page store.Page) (string, error return path, os.WriteFile(path, []byte(out), 0o644) } +type pathResolver struct { + spaces map[string]string + teams map[string]string + blocks map[string]store.ParentRef + collections map[string]store.ParentRef +} + +func newPathResolver(ctx context.Context, st *store.Store) (pathResolver, error) { + spaces, err := st.SpaceNames(ctx) + if err != nil { + return pathResolver{}, err + } + teams, err := st.TeamNames(ctx) + if err != nil { + return pathResolver{}, err + } + blocks, err := st.BlockParents(ctx) + if err != nil { + return pathResolver{}, err + } + collections, err := st.CollectionParents(ctx) + if err != nil { + return pathResolver{}, err + } + return pathResolver{spaces: spaces, teams: teams, blocks: blocks, collections: collections}, nil +} + +func (r pathResolver) spaceName(id string) string { + if id == "" { + return "default" + } + if name := r.spaces[id]; name != "" { + return name + } + return "space-" + notiontext.ShortID(id) +} + +func (r pathResolver) teamName(id string) string { + if id == "" { + return "" + } + if name := r.teams[id]; name != "" { + return name + } + return "team-" + notiontext.ShortID(id) +} + +func (r pathResolver) pageTeamID(page store.Page) string { + return r.resolveTeamID(page.ParentTable, page.ParentID, page.CollectionID, map[string]bool{page.ID: true}) +} + +func (r pathResolver) resolveTeamID(table, id, collectionID string, seen map[string]bool) string { + if table == "team" { + return id + } + if table == "collection" && id == "" { + id = collectionID + } + if id == "" || seen[table+":"+id] { + return "" + } + seen[table+":"+id] = true + switch table { + case "block": + parent := r.blocks[id] + return r.resolveTeamID(parent.Table, parent.ID, "", seen) + case "collection", "database", "data_source": + parent := r.collections[id] + return r.resolveTeamID(parent.Table, parent.ID, "", seen) + default: + return "" + } +} + func writeFrontMatter(b *strings.Builder, page store.Page, spaceName, teamID, teamName string) { b.WriteString("---\n") writeKV(b, "id", page.ID) diff --git a/internal/markdown/export_test.go b/internal/markdown/export_test.go index 77167cb..c2c509c 100644 --- a/internal/markdown/export_test.go +++ b/internal/markdown/export_test.go @@ -142,6 +142,38 @@ func TestExporterUsesWorkspaceAndTeamspacePath(t *testing.T) { } } +func TestExporterResolvesTeamspaceThroughCollectionParent(t *testing.T) { + ctx := context.Background() + st, err := store.Open(filepath.Join(t.TempDir(), "notcrawl.db")) + if err != nil { + t.Fatal(err) + } + defer st.Close() + now := store.NowMS() + if err := st.UpsertSpace(ctx, store.Space{ID: "space1", Name: "Acme Org", Source: "test", SyncedAt: now}); err != nil { + t.Fatal(err) + } + if err := st.UpsertTeam(ctx, store.Team{ID: "team1", SpaceID: "space1", Name: "Research Lab", Source: "test", SyncedAt: now}); err != nil { + t.Fatal(err) + } + if err := st.UpsertCollection(ctx, store.Collection{ID: "collection1", SpaceID: "space1", ParentID: "team1", ParentTable: "team", Name: "Roadmap", Source: "test", SyncedAt: now}); err != nil { + t.Fatal(err) + } + if err := st.UpsertPage(ctx, store.Page{ID: "page1", SpaceID: "space1", ParentID: "collection1", ParentTable: "collection", CollectionID: "collection1", Title: "Row", Alive: true, Source: "test", SyncedAt: now}); err != nil { + t.Fatal(err) + } + + dir := t.TempDir() + s, err := Exporter{Store: st, Dir: dir}.Export(ctx) + if err != nil { + t.Fatal(err) + } + want := filepath.Join(dir, "acme-org", "research-lab", "row-page1.md") + if len(s.Files) != 1 || s.Files[0] != want { + t.Fatalf("unexpected export path: %+v, want %s", s.Files, want) + } +} + func TestExporterUsesReadableMissingSpaceFallback(t *testing.T) { ctx := context.Background() st, err := store.Open(filepath.Join(t.TempDir(), "notcrawl.db")) diff --git a/internal/store/query.go b/internal/store/query.go index ed8b7ac..1ac1916 100644 --- a/internal/store/query.go +++ b/internal/store/query.go @@ -119,6 +119,76 @@ func (s *Store) PageComments(ctx context.Context, pageID string) ([]Comment, err return comments, rows.Err() } +func (s *Store) SpaceNames(ctx context.Context) (map[string]string, error) { + rows, err := s.queryContext(ctx, `select id, name from spaces`) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[string]string{} + for rows.Next() { + var id, name string + if err := rows.Scan(&id, &name); err != nil { + return nil, err + } + out[id] = name + } + return out, rows.Err() +} + +func (s *Store) TeamNames(ctx context.Context) (map[string]string, error) { + rows, err := s.queryContext(ctx, `select id, name from teams`) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[string]string{} + for rows.Next() { + var id, name string + if err := rows.Scan(&id, &name); err != nil { + return nil, err + } + out[id] = name + } + return out, rows.Err() +} + +func (s *Store) BlockParents(ctx context.Context) (map[string]ParentRef, error) { + rows, err := s.queryContext(ctx, `select id, parent_id, parent_table from blocks`) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[string]ParentRef{} + for rows.Next() { + var id string + var parentID, parentTable sql.NullString + if err := rows.Scan(&id, &parentID, &parentTable); err != nil { + return nil, err + } + out[id] = ParentRef{ID: parentID.String, Table: parentTable.String} + } + return out, rows.Err() +} + +func (s *Store) CollectionParents(ctx context.Context) (map[string]ParentRef, error) { + rows, err := s.queryContext(ctx, `select id, parent_id, parent_table from collections`) + if err != nil { + return nil, err + } + defer rows.Close() + out := map[string]ParentRef{} + for rows.Next() { + var id string + var parentID, parentTable sql.NullString + if err := rows.Scan(&id, &parentID, &parentTable); err != nil { + return nil, err + } + out[id] = ParentRef{ID: parentID.String, Table: parentTable.String} + } + return out, rows.Err() +} + func (s *Store) SpaceName(ctx context.Context, id string) (string, error) { if id == "" { return "default", nil diff --git a/internal/store/types.go b/internal/store/types.go index b1abb17..a6b82a9 100644 --- a/internal/store/types.go +++ b/internal/store/types.go @@ -105,6 +105,11 @@ type RawRecord struct { SyncedAt int64 } +type ParentRef struct { + ID string + Table string +} + type SearchResult struct { Kind string ID string