Help us shape the future of Go! Take the 2026 Go Developer Survey

store

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: GPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package store implements sync82's SQLite-backed persistence layer: projects (each optionally holding subprojects), overwrite-style documents, append-only entries, and case-insensitive substring search across both. Manager caches one *Store per vault path so repeated calls against the same vault reuse a connection pool; Open handles per-vault schema migrations and connection setup. Every *Store method is safe for concurrent use. Lookup failures wrap ErrNotFound, naming collisions wrap ErrAlreadyExists, and Open failures wrap ErrOpenFailed; check them with errors.Is. Messages of all errors except ErrOpenFailed contain no filesystem paths.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyExists = errors.New("already exists")

ErrAlreadyExists is wrapped when an operation would create a naming collision, for example promoting a subproject to a name already used at the vault root.

View Source
var ErrManagerClosed = errors.New("store manager is closed")

ErrManagerClosed is returned by Get once Close has been called, so a goroutine still running during shutdown cannot open a connection that Close would never release.

View Source
var ErrNoSearchTerms = errors.New("the query has no words to search for")

ErrNoSearchTerms is returned by SearchText when a words or phrase query holds no letter or number to search for.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is wrapped by any lookup that fails to find its target (project, subproject, document kind, or entries kind). Check for it with errors.Is; the error message describes what was not found without including filesystem paths.

View Source
var ErrOpenFailed = errors.New("could not open vault")

ErrOpenFailed is wrapped by Open when the vault directory, file, database connection or migration step fails. Unlike every other store error, its messages embed a raw filesystem path, so callers that return errors to remote clients should detect it with errors.Is and substitute a generic message.

View Source
var ErrSchemaTooNew = errors.New("the vault's schema is newer than this sync82 supports; upgrade sync82")

ErrSchemaTooNew is wrapped, together with ErrOpenFailed, when the vault's schema version is newer than the latest one this binary knows: a newer sync82 has upgraded the vault, and this one refuses to read or write it.

View Source
var ErrVaultNotFound = errors.New("vault not found")

ErrVaultNotFound is wrapped by Manager.GetExisting when no vault file exists at the requested path.

Functions

func HasSearchTerms added in v1.1.0

func HasSearchTerms(query string) bool

HasSearchTerms reports whether query holds at least one searchable word for the words and phrase search modes, that is, at least one letter or number.

Types

type ArchiveResult

type ArchiveResult struct {
	Archived   int
	OldestDate string
	NewestDate string
	Kept       int
	NoDate     int
	SummaryID  int64
}

ArchiveResult summarizes the outcome of an ArchiveEntries call: Archived is the number of entries archived by the call, OldestDate and NewestDate the range of their dates ("" when none was archived), Kept the number of non-archived entries remaining afterwards (a summary entry included), and NoDate how many of those are undated. SummaryID is the id of the summary entry, or 0 when none was added.

type ArchiveSummary added in v1.1.0

type ArchiveSummary func(archived ArchiveResult) (entryDate, body string, err error)

ArchiveSummary builds the summary entry ArchiveEntries adds after archiving: given the archived range (Archived, OldestDate and NewestDate set), it returns the entry's date ("" for undated) and body. An error aborts the whole call.

type Entry

type Entry struct {
	ID        int64
	EntryDate string // "", if undated
	Body      string
	Archived  bool
	CreatedAt string
}

Entry is a single row of an append-only log. ID is the row's identifier, unique within the vault and stable for the life of the row (rewriting the whole kind, or importing it, creates new rows). EntryDate is empty when the entry is undated.

type EntrySection

type EntrySection struct {
	Date string // "YYYY-MM-DD", empty for undated content
	Body string
	// Preamble marks undated content that precedes the first dated section
	// (such as a title). It is stored with position preamblePosition so it
	// reads back first instead of after every dated entry.
	Preamble bool
}

EntrySection is a single dated or undated chunk of append-only content, such as one "## YYYY-MM-DD" section of a log. The store persists already-parsed sections and does not split raw text itself.

type EntryStats added in v1.1.0

type EntryStats struct {
	Dated      int
	Undated    int
	LatestDate string
}

EntryStats summarizes the non-archived entries of a kind: Dated counts the dated ones (those ReadEntriesSince filters with since and maxEntries), Undated the undated ones other than a preamble, and LatestDate is the newest entry date ("" when none is dated).

type KindMetadata

type KindMetadata struct {
	SizeBytes       int
	EstimatedTokens int
	LastModified    string // "YYYY-MM-DD"
}

KindMetadata describes the size and freshness of a kind's content. SizeBytes is the content length in bytes, EstimatedTokens a rough estimate (SizeBytes / 4), and LastModified the date of the last change.

type KindStorage

type KindStorage string

KindStorage names the table a kind's data lives in for a project.

const (
	// KindStorageNone means the kind has no data yet.
	KindStorageNone KindStorage = ""
	// KindStorageDocument means the kind is a single overwrite-style
	// documents row.
	KindStorageDocument KindStorage = "document"
	// KindStorageEntries means the kind is a log of entries rows
	// (archived or not).
	KindStorageEntries KindStorage = "entries"
)

type KindWrite

type KindWrite struct {
	Kind     string
	Document *string
	Sections []EntrySection
	Archived bool
}

KindWrite is one kind's new content in a WriteKinds batch: Document for an overwrite-style kind, or Sections replacing its entries (the non-archived ones, or the archived ones when Archived is set). Document takes precedence when non-nil.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager caches one Store per vault path, so repeated calls against the same vault reuse one connection pool instead of reopening the database and re-running the migration check each time. A process may use several vault paths, hence one Store per path. A Manager is safe for concurrent use.

func NewManager

func NewManager() *Manager

NewManager returns an empty, open Manager.

func (*Manager) Close

func (m *Manager) Close() error

Close closes every cached Store, empties the cache and marks the Manager closed, so later Get calls fail with ErrManagerClosed. It returns the first error reported while closing a Store, after attempting to close all of them.

func (*Manager) Get

func (m *Manager) Get(ctx context.Context, path string) (*Store, error)

Get returns the Store for path, opening and caching it on first use. path is made absolute first, so equivalent spellings share one Store. It returns ErrManagerClosed once Close has run, and any error from Open (wrapping ErrOpenFailed) when the vault cannot be opened.

Open runs outside m.mu because it performs file I/O and migrations; a slow first open of one vault must not block calls against other vaults. The lock only guards the cache lookup and the reconciliation after Open returns.

func (*Manager) GetExisting

func (m *Manager) GetExisting(ctx context.Context, path string) (*Store, error)

GetExisting is Get for callers that must not create a vault: when no file exists at path it returns an error wrapping ErrVaultNotFound instead of creating an empty vault there. Other errors are those of Get.

type Project

type Project struct {
	ID        int64
	Name      string
	ParentID  sql.NullInt64
	CreatedAt string
}

Project is a row of the projects table: a top-level project (ParentID not valid) or a subproject (ParentID holds the parent's ID). CreatedAt is an RFC 3339 timestamp.

type SearchMode added in v1.1.0

type SearchMode string

SearchMode selects how SearchText matches a query.

const (
	SearchWords  SearchMode = "words"
	SearchPhrase SearchMode = "phrase"
	SearchExact  SearchMode = "exact"
)

The search modes. SearchWords finds rows holding every word of the query and SearchPhrase rows holding the words in that order, both through the full-text indexes, ignoring case and Latin diacritics, ranked by relevance. SearchExact finds rows holding the query as a literal, case-insensitive substring, in reading order.

type SearchOptions added in v1.1.0

type SearchOptions struct {
	Query        string
	Mode         SearchMode
	Scope        SearchScope
	Kinds        []string
	Since        string
	Until        string
	Offset       int
	Limit        int
	ContextLines int
}

SearchOptions describes a SearchText call. Mode defaults to SearchWords when empty. Kinds, when not empty, limits the search to those kinds (normalized names). Since and Until ("YYYY-MM-DD", inclusive), when set, limit the search to dated entries in that range; documents and undated entries are then left out. Offset and Limit select the page of matching lines; ContextLines, when positive, adds that many lines before and after each match.

type SearchResult

type SearchResult struct {
	Project       string
	Subproject    string
	Kind          string
	EntryID       int64
	EntryDate     string
	LineNumber    int
	Line          string
	ContextBefore []string
	ContextAfter  []string
}

SearchResult is a single matching line within a document or entry. Project is the top-level project name and Subproject the subproject name (empty for a top-level project). For an entry, EntryID is its id (0 for a document) and EntryDate its date ("" if undated); LineNumber is the 1-based line within the document or entry body. Line is the matching line, and ContextBefore and ContextAfter hold the surrounding lines when context was requested.

type SearchScope

type SearchScope struct {
	Project    string
	Subproject string
}

SearchScope narrows a search to a project (and optionally a specific subproject). An empty Project means the whole vault; a Project with an empty Subproject means that project plus all of its subprojects.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a SQLite-backed vault: one physical file holds every project's documents and entries. A Store is safe for concurrent use by multiple goroutines.

func Open

func Open(ctx context.Context, path string) (*Store, error)

Open opens (creating if necessary) the SQLite database at path and applies pending schema migrations. ctx bounds the initial ping and the migration, so a cancelled or expired context stops Open from waiting on a slow disk or filesystem mount.

It returns an error wrapping ErrOpenFailed when path contains '?' or '#', when the vault directory or file cannot be created, or when opening, pinging or migrating the database fails (including a vault whose schema is newer than this binary supports).

SQLite pragmas are connection-scoped, not database-scoped, so foreign_keys and journal_mode are set via DSN query parameters (modernc.org/sqlite applies "_pragma" params to every new physical connection the pool creates) rather than a one-time PRAGMA statement — that guarantees every connection enforces foreign keys and uses WAL, including a connection database/sql reopens after closing one.

The pool holds a single connection, so the operations of one Store run one at a time and only other processes contend for the file lock.

func (*Store) AppendEntry

func (s *Store) AppendEntry(ctx context.Context, project, subproject, kind, entryDate, body string) error

AppendEntry inserts one new, non-archived entry with the given date (empty for undated) and body at the next insertion position for its (project, kind) pair. The position is computed by a subquery in the same INSERT statement, so concurrent appends cannot read the same position. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ArchiveEntries

func (s *Store) ArchiveEntries(ctx context.Context, project, subproject, kind, cutoff string, summary ArchiveSummary) (ArchiveResult, error)

ArchiveEntries marks every non-archived, dated entry older than cutoff (format "YYYY-MM-DD") as archived. Undated entries are never archived. When summary is not nil and at least one entry is archived, the entry it builds is appended as a new, non-archived entry. Everything happens in one transaction, so a failure leaves the entries unchanged. It returns the counts as an ArchiveResult. It returns an error wrapping ErrNotFound when the project or subproject does not exist, summary's error, or a database error.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database connection pool and returns any error reported by the driver.

func (*Store) CreateProject

func (s *Store) CreateProject(ctx context.Context, name string, parentID *int64) (*Project, error)

CreateProject inserts a new project row named name (normalized), top-level when parentID is nil and otherwise a child of parentID. It returns the created Project, or a database error; a duplicate name in the same scope fails with a UNIQUE constraint error. The existence of parentID is checked by the foreign key.

func (*Store) DeleteEntry added in v1.1.0

func (s *Store) DeleteEntry(ctx context.Context, project, subproject, kind string, id int64) error

DeleteEntry deletes the entry with the given id, archived or not. It returns an error wrapping ErrNotFound when the project or subproject does not exist or no such entry belongs to (project, kind), or a database error.

func (*Store) DeleteKind

func (s *Store) DeleteKind(ctx context.Context, project, subproject, kind string) error

DeleteKind deletes every row (document and entries, archived or not) of a kind in one transaction, so a kind is never left half-deleted. The store does not protect any kind from deletion. It returns an error wrapping ErrNotFound when the project or subproject does not exist or the kind has no rows, or a database error.

func (*Store) DeleteProject

func (s *Store) DeleteProject(ctx context.Context, name string) error

DeleteProject deletes a top-level project by name, cascading to its subprojects, documents and entries via ON DELETE CASCADE. It returns an error wrapping ErrNotFound when no such project exists, or a database error.

func (*Store) DeleteSubproject

func (s *Store) DeleteSubproject(ctx context.Context, parentName, subName string) error

DeleteSubproject deletes the subproject subName of parentName, cascading to its documents and entries. It returns an error wrapping ErrNotFound when the parent or subproject does not exist, or a database error.

func (*Store) EnsureProject

func (s *Store) EnsureProject(ctx context.Context, project, subproject string) (proj *Project, created bool, err error)

EnsureProject returns the project or subproject, creating it (and its parent project when missing) if needed. subproject may be empty for a top-level project. created reports whether the leaf (project or subproject) was created by this call; it does not report whether a missing parent was also created. It returns a database error on failure.

The check-then-create sequence is not a single transaction, so concurrent calls for the same new name can both attempt the insert. The UNIQUE index decides the winner, and createOrRecoverExisting turns the loser's constraint violation into the existing row.

func (*Store) EntryStats added in v1.1.0

func (s *Store) EntryStats(ctx context.Context, project, subproject, kind string) (EntryStats, error)

EntryStats returns the EntryStats of (project, kind), all zero for a kind with no entries. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) FindProjectByName

func (s *Store) FindProjectByName(ctx context.Context, name string, parentID *int64) (*Project, error)

FindProjectByName looks up a project by name within the given parent scope (parentID nil means top-level). The name is normalized. It returns (nil, nil) when no matching row exists, and a database error otherwise.

func (*Store) IsBlankOrTemplate

func (s *Store) IsBlankOrTemplate(ctx context.Context, project, subproject, kind string) (bool, error)

IsBlankOrTemplate reports whether the document of the given kind is blank: either no row exists or its content is empty or whitespace-only. Errors are those of ReadDocument.

func (*Store) KindExists

func (s *Store) KindExists(ctx context.Context, project, subproject, kind string) (bool, error)

KindExists reports whether a kind has any data for the project: a documents row (even with empty content) or at least one entries row, archived or not. Errors are those of ReadDocument and ReadEntries.

func (*Store) KindMode

func (s *Store) KindMode(ctx context.Context, project, subproject, kind string) (KindStorage, error)

KindMode reports which table holds a kind's data for the project, or KindStorageNone when it has none. A kind present in both tables reports KindStorageDocument, the table ReadContent reads first. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ListArchivable added in v1.1.0

func (s *Store) ListArchivable(ctx context.Context, project, subproject, kind, cutoff string) ([]Entry, error)

ListArchivable returns the entries ArchiveEntries would archive with the same cutoff: non-archived, dated entries older than cutoff, in reading order. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ListKinds

func (s *Store) ListKinds(ctx context.Context, project, subproject string, includeArchived bool) ([]string, error)

ListKinds returns every kind known for the project, the union of document kinds and distinct entries kinds, sorted by name. A kind whose entries are all archived is included only when includeArchived is true. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ListSubprojects

func (s *Store) ListSubprojects(ctx context.Context, parentID int64) ([]Project, error)

ListSubprojects returns every project whose parent is parentID, sorted by name. It returns a database error on failure.

func (*Store) ListTopLevelProjects

func (s *Store) ListTopLevelProjects(ctx context.Context) ([]Project, error)

ListTopLevelProjects returns every project with no parent, sorted by name. It returns a database error on failure.

func (*Store) Metadata

func (s *Store) Metadata(ctx context.Context, project, subproject, kind string) (KindMetadata, error)

Metadata computes KindMetadata for a document or entries kind. For a document it uses the content size and update date; for entries it sums the sizes of the non-archived bodies and uses the latest creation date of the non-archived entries, or of all entries when every one is archived (size 0), so a kind KindExists reports always has metadata. Sizes use length(CAST(x AS BLOB)) because SQLite's length() counts characters, not bytes, for TEXT values. It returns an error wrapping ErrNotFound when the project does not exist or the kind has no document and no entries, or a database error.

func (*Store) ProjectExists

func (s *Store) ProjectExists(ctx context.Context, project, subproject string) (bool, error)

ProjectExists reports whether the (project, subproject) pair exists. subproject may be empty to check the top-level project itself. It returns an error only for database failures.

func (*Store) PromoteAllSubprojects

func (s *Store) PromoteAllSubprojects(ctx context.Context, parentName string) ([]string, error)

PromoteAllSubprojects promotes every subproject of parentName to the vault root in a single transaction: either all of them are promoted or none is. The parent and its subprojects are read inside that transaction, so the promoted set is the one present when the writes are applied. It returns the promoted names (nil when there are none). It returns an error wrapping ErrNotFound when the parent does not exist, an error wrapping ErrAlreadyExists when any name collides with a top-level project (rolling the whole batch back), or a database error.

func (*Store) PromoteSubproject

func (s *Store) PromoteSubproject(ctx context.Context, parentName, subName string) error

PromoteSubproject moves the subproject subName of parentName to the vault root as a top-level project. It returns an error wrapping ErrNotFound when the parent or subproject does not exist, an error wrapping ErrAlreadyExists when a top-level project with that name exists, or a database error.

func (*Store) ReadContent

func (s *Store) ReadContent(ctx context.Context, project, subproject, kind string) (content string, ok bool, err error)

ReadContent returns the readable content of a kind, from whichever table holds it. A document takes precedence; otherwise the bodies of the non-archived entries are joined with a blank line between them. ok is false when the kind has neither a document nor non-archived entries. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ReadDocument

func (s *Store) ReadDocument(ctx context.Context, project, subproject, kind string) (content string, ok bool, err error)

ReadDocument returns the raw content of the overwrite-style document of the given kind for project/subproject. ok is false, with no error, when the document row does not exist. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ReadEntries

func (s *Store) ReadEntries(ctx context.Context, project, subproject, kind string, includeArchived bool) ([]Entry, error)

ReadEntries returns entries for (project, kind) in reading order: a preamble first, then dated entries by date, then other undated entries, ties broken by insertion position. Archived entries are excluded unless includeArchived is true. A kind with no entries yields an empty slice and no error. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ReadEntriesSince

func (s *Store) ReadEntriesSince(ctx context.Context, project, subproject, kind, since string, maxEntries int) ([]Entry, error)

ReadEntriesSince returns non-archived entries for (project, kind), filtered in SQL so the full history is never loaded just to return recent entries.

Both filters apply to dated entries only; undated entries (a preamble, undated notes) are always kept, as in ArchiveEntries. since, if non-empty (format "YYYY-MM-DD"), keeps only dated entries on or after it. maxEntries, if > 0, then keeps only the most recent maxEntries of those dated entries. The result is in the same reading order ReadEntries uses. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ReadEntry added in v1.1.0

func (s *Store) ReadEntry(ctx context.Context, project, subproject, kind string, id int64) (Entry, error)

ReadEntry returns the entry with the given id, archived or not, when it belongs to (project, kind). It returns an error wrapping ErrNotFound when the project or subproject does not exist or no such entry belongs to it, or a database error.

func (*Store) RenameProject

func (s *Store) RenameProject(ctx context.Context, project, subproject, newName string) error

RenameProject renames a top-level project (subproject == "") or a specific subproject in place to newName (normalized). It returns an error wrapping ErrNotFound when the project or subproject does not exist, an error wrapping ErrAlreadyExists when a sibling in the same scope already has newName, or a database error. Only the database is changed.

func (*Store) ReplaceAllEntries

func (s *Store) ReplaceAllEntries(ctx context.Context, project, subproject, kind string, sections []EntrySection) error

ReplaceAllEntries atomically replaces every non-archived entries row for (project, kind) with the given sections, in order. Archived rows are left untouched. The result reads the same as appending the sections one by one with AppendEntry. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) ReplaceArchivedEntries

func (s *Store) ReplaceArchivedEntries(ctx context.Context, project, subproject, kind string, sections []EntrySection) error

ReplaceArchivedEntries atomically replaces every archived entries row for (project, kind) with the given sections, stored as archived. Non-archived rows are left untouched. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

func (*Store) SearchText

func (s *Store) SearchText(ctx context.Context, opts SearchOptions) ([]SearchResult, bool, error)

SearchText searches the documents and non-archived entries in scope and returns their matching lines. In SearchWords and SearchPhrase modes the full-text indexes select the rows, ordered by relevance (bm25), then by project, subproject, kind and reading order; a line matches when it holds one of the query's words (words mode) or the whole phrase (phrase mode), and a row whose phrase spans several lines reports the lines holding any of its words. In SearchExact mode the query is a literal, case-insensitive substring (Unicode-aware: "DECISÃO" matches "decisão") and matches are ordered by project, subproject, kind, then reading order.

It returns the matching lines skipping the first opts.Offset, up to opts.Limit+1 of them: the extra one lets the caller detect that more results exist. The boolean reports whether the scan stopped at maxScannedRows or maxScannedBytes, in which case later matches are missing. It returns an error wrapping ErrNotFound when the scoped project or subproject does not exist, an error wrapping ErrNoSearchTerms when a words or phrase query has no letter or number, or a database error.

func (*Store) SupersedeEntry added in v1.1.0

func (s *Store) SupersedeEntry(ctx context.Context, project, subproject, kind string, id int64, entryDate, body string, mark func(oldBody string, newID int64) string) (int64, error)

SupersedeEntry appends a new, non-archived entry with entryDate (empty for undated) and body, as AppendEntry does, and replaces the body of the entry with the given id by mark(oldBody, newID), all in one transaction. It returns the new entry's id. It returns an error wrapping ErrNotFound when the project or subproject does not exist or no such entry belongs to (project, kind), or a database error.

func (*Store) UpdateEntry added in v1.1.0

func (s *Store) UpdateEntry(ctx context.Context, project, subproject, kind string, id int64, body string, entryDate *string) error

UpdateEntry replaces the body of the entry with the given id, keeping its position and archived flag. When entryDate is non-nil it also sets the entry's date ("" for undated); otherwise the date is kept. It returns an error wrapping ErrNotFound when the project or subproject does not exist or no such entry belongs to (project, kind), or a database error.

func (*Store) WriteDocument

func (s *Store) WriteDocument(ctx context.Context, project, subproject, kind, content string) error

WriteDocument sets the content of the overwrite-style document of the given kind for project/subproject, inserting it when it does not exist yet. The names are normalized. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error. A single upsert statement keeps the write atomic.

func (*Store) WriteKinds

func (s *Store) WriteKinds(ctx context.Context, project, subproject string, writes []KindWrite) error

WriteKinds applies every write to (project, subproject) in one transaction: either all of them take effect or none does. Kind names are normalized. It returns an error wrapping ErrNotFound when the project or subproject does not exist, or a database error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL