dbmirror

package
v0.157.0 Latest Latest
Warning

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

Go to latest
Published: Nov 14, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package dbmirror copies a chosen part of a Firestore database into an inGitDB database laid out exactly as an inGitDB "mirror" defines it.

Firestore has no schema, so nothing is introspected: the collection list, the path of every record file and the shape every record must have come from the inGitDB collection definitions of the mirror (the TARGET), where one definition stands for one Firestore collection path, at any depth.

A mirror policy (format datatug-mirror-policy/v1), kept in the DataTug project, says which defined collections may be copied, which are excluded and which fields are masked. A collection the policy does not declare is never copied: it is listed in the report instead.

The definitions decide how records are stored: one file per record (named by the encoded id, KeyEncoding; an id too long for its file name fails the run) or all records of a collection in one file keyed by the real ids, in YAML or JSON. An output is a mirror of one database (SourceIdentity, kept in .datatug-mirror/output.json): no run, resume or prune of another touches it.

The source is only read. The engine depends on the Source interface, whose methods only read; the one Firestore implementation (firestore_source.go) opens a client whose every request passes a filter that lets only the four read RPCs through (TestFirestoreClientRefusesWrites sends writes through it), calls no write method of the Google SDK (TestNoWriteCallInPackage checks the source text), and a run against a fake Firestore server that fails every write RPC proves none is sent (TestFirestoreSourceSendsNoWrite). Credentials are never read or stored by this package: the Google SDK reads Application Default Credentials from the operator's environment.

Index

Constants

View Source
const (
	ActionCopy        = "copy"        // copied as read
	ActionMask        = "mask"        // copied with the policy's fields masked
	ActionExclude     = "exclude"     // never copied: the policy excludes it
	ActionUndeclared  = "undeclared"  // never copied: the policy does not name it
	ActionUnsupported = "unsupported" // the policy copies it, but its record layout cannot be written
)

What the policy says about a defined collection.

View Source
const DefaultPageSize = 300

DefaultPageSize is how many documents one read request asks for.

View Source
const PolicyFormat = "datatug-mirror-policy/v1"

PolicyFormat is the format a mirror policy file must declare.

View Source
const ReportFormat = "datatug-mirror-report/v1"

ReportFormat is the format of the report a run writes.

Variables

View Source
var ErrIDTooLong = errors.New("id too long for one file per record")

ErrIDTooLong is the failure of a run that read a document of a collection kept one file per record whose file name (its encoded id) or path is too long to write. The run stops there: the report says so, and a resume starts again at that document.

View Source
var ErrRoundTrip = errors.New("a record file would not read back as written")

ErrRoundTrip is the failure of a run that would write a record file that does not read back as what it was meant to hold. The file is not written; the run stops there, like at an id too long (ErrIDTooLong).

View Source
var ErrUsage = errors.New("usage")

ErrUsage marks an error in what the caller asked for (exit code 2).

Functions

This section is empty.

Types

type CollectionResult

type CollectionResult struct {
	Collection string `json:"collection"`
	Action     string `json:"action"`
	Selected   bool   `json:"selected"`
	Note       string `json:"note,omitempty"`
	Query      string `json:"query,omitempty"`
	Count      *int64 `json:"count,omitempty"`
	// CountAtLeast says Count stopped at the read budget: there are at least that many.
	CountAtLeast bool     `json:"count_at_least,omitempty"`
	Records      int64    `json:"records"`
	Rejected     int64    `json:"rejected,omitempty"`
	Masked       []string `json:"masked_fields,omitempty"`
	// BelowMissingParent counts records whose parent document does not exist
	// in the source (Firestore keeps such sub-collections).
	BelowMissingParent int64 `json:"below_missing_parent,omitempty"`
	// OtherParents counts documents of the collection that were read but not
	// copied because their path is not one the policy's paths allow, and
	// OtherParentPaths shows those paths with every unpinned id as "*".
	OtherParents     int64    `json:"other_parents,omitempty"`
	OtherParentPaths []string `json:"other_parent_paths,omitempty"`
	// IDTooLong counts documents not copied because their record file name
	// (the encoded id) is longer than 255 bytes, or their path longer than the
	// system writes: file per record does not hold them.
	IDTooLong int64 `json:"not_copied_id_too_long,omitempty"`
	// Resumed says part of the collection was read by an earlier run.
	Resumed  bool `json:"resumed,omitempty"`
	Complete bool `json:"complete"`
	// contains filtered or unexported fields
}

CollectionResult is what happened to one defined collection.

type Definitions

type Definitions struct {
	Dir   string // absolute folder of the definitions
	Specs []*Spec
	// contains filtered or unexported fields
}

Definitions is the loaded mirror.

func LoadDefinitions

func LoadDefinitions(dir string) (*Definitions, error)

LoadDefinitions reads the inGitDB definitions of a mirror from dir (an inGitDB database folder: .ingitdb/root-collections.yaml and the collection definitions). Only definitions are used; any records in dir are ignored.

func (*Definitions) CopyTo

func (d *Definitions) CopyTo(out string) (digest string, err error)

CopyTo writes the definition files (never the records) of the mirror into out, so that out is an inGitDB database with the same collections. A file already holding the same bytes is not rewritten.

It returns a digest of the files copied (their paths and content), which a resumed run compares, so it never continues under changed definitions.

func (*Definitions) Spec

func (d *Definitions) Spec(id string) *Spec

Spec returns the defined collection named id, or nil.

type Doc

type Doc struct {
	// Path is the document path relative to the database (a/1/b/2).
	Path string
	// Data holds the fields as the Google Firestore SDK returns them.
	Data map[string]any
}

Doc is one document read from the source.

type FirestoreSource

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

FirestoreSource reads one Firestore database through the Google SDK, with the credentials the SDK finds in the operator's environment (Application Default Credentials, or FIRESTORE_EMULATOR_HOST for an emulator).

func OpenFirestore

func OpenFirestore(ctx context.Context, project, database string) (*FirestoreSource, error)

OpenFirestore opens a reader of database (use "(default)") of project.

func (*FirestoreSource) Close

func (s *FirestoreSource) Close() error

Close releases the client, and with it the emulator connection.

func (*FirestoreSource) Count

func (s *FirestoreSource) Count(ctx context.Context, q Query) (int64, error)

Count implements Source.

func (*FirestoreSource) Get

func (s *FirestoreSource) Get(ctx context.Context, path string) (*Doc, error)

Get implements Source.

func (*FirestoreSource) Page

func (s *FirestoreSource) Page(ctx context.Context, q Query) ([]Doc, error)

Page implements Source.

func (*FirestoreSource) ReadAt

func (s *FirestoreSource) ReadAt(t time.Time)

ReadAt makes every later document read return the documents as they were at t (Firestore keeps them for an hour, or for 7 days with point-in-time recovery). Counts are not affected.

func (*FirestoreSource) RootCollections

func (s *FirestoreSource) RootCollections(ctx context.Context) ([]string, error)

RootCollections implements Source.

type KeyEncoding

type KeyEncoding interface {
	// Name identifies the encoding in the output's state.
	Name() string
	// Encode is the file name of id.
	Encode(id string) string
	// Decode is the id of a file name, and false for a name Encode never writes.
	Decode(name string) (string, bool)
}

KeyEncoding turns a Firestore document id into the file name (and so the inGitDB record key) a mirror writes, and a file name back into the id.

The encoding is tildeHexKeyEncoding ("~XX", ids.go; specified in docs/db-mirror.md "Key encoding"). It is to be declared in the inGitDB schema and implemented once in ingitdb-go; until then the mirror implements it here, behind this interface, and records its name in the output's state (.datatug-mirror/output.json) instead of the collection definitions. A run refuses an output written with another encoding, so two are never mixed.

type Options

type Options struct {
	// Output is the folder the inGitDB database is written to. Not used by a dry run.
	Output string
	// Collections, when not empty, copies only these defined collections
	// (a collection id path, or "<path>/**" for it and every collection below it).
	Collections []string
	// Under, when set, copies only the documents at or below this path: a
	// document path (spaces/abc) or a collection path (spaces/abc/ext).
	Under string
	// DryRun counts the documents of each collection and writes nothing.
	DryRun bool
	// MaxReads stops the run before more than this many Firestore reads (0: no limit).
	MaxReads int64
	// PageSize is how many documents one request reads (default DefaultPageSize).
	PageSize int
	// KeepRejected also writes each rejected document under
	// .datatug-mirror/rejected/, as read with only the policy's masks applied:
	// its undeclared fields and the values that break its definition are
	// written in clear. By default only its path, reasons and declared field
	// names are kept, in the report.
	KeepRejected bool
	// ContinueOnLongIDs copies the rest when a document's id is too long for
	// one file per record, and reports it (the run is then incomplete),
	// instead of failing at the first one.
	ContinueOnLongIDs bool
	// Prune deletes the record files whose document no longer exists, in the
	// collections this run read completely. By default they are only reported.
	Prune bool
	// Restart ignores the resume state an earlier run left (see Resume).
	Restart bool
	// Snapshot reads every document as it was when the run (or the run it
	// resumes, within the hour) started, when the source can.
	Snapshot bool
	// Source is how the source is shown in the report (never a credential).
	Source string
	// Identity is the database read. The output records the identity of the
	// source it was created from, and every later run into it, its resume
	// and its prune, must read the same one.
	Identity SourceIdentity
	// Log receives one line per collection and the summary (nil: discarded).
	Log io.Writer
	// Progress, when set, receives one line per page read.
	Progress io.Writer
}

Options of a run.

type OutsideRead

type OutsideRead struct {
	Collection string `json:"collection"`
	Defined    bool   `json:"defined"`
	Documents  int64  `json:"documents"`
}

OutsideRead counts documents a collection-group read returned that belong to no selected collection: their collection path is shown, so a collection that is missing from the definitions becomes visible.

type Policy

type Policy struct {
	Format      string              `yaml:"format"`
	Definitions string              `yaml:"definitions,omitempty"`
	Output      string              `yaml:"output,omitempty"`
	Copy        []string            `yaml:"copy,omitempty"`
	Exclude     []string            `yaml:"exclude,omitempty"`
	Mask        map[string][]string `yaml:"mask,omitempty"`
	Paths       map[string][]string `yaml:"paths,omitempty"`

	// Path is the file the policy was read from ("" when built in code).
	Path string `yaml:"-"`
}

Policy says which defined collections of a mirror may be copied out of the source, which may not, and which fields are masked. It is a file of the DataTug project (YAML):

format: datatug-mirror-policy/v1
definitions: ../firestore         # the inGitDB mirror, relative to this file
output: ../data/prod-backup       # where records go, relative to this file
copy:
  - teams
  - teams/ext
  - teams/ext/events
exclude:
  - ext/secrets
paths:                            # only these Firestore paths of a collection
  teams/ext: [teams/*/ext/calendar]
  teams/ext/events: [teams/*/ext/calendar/events]
mask:
  ext/customers: [email, displayName, members.*.phone]

A collection is named by the collection ids of its path joined by "/". A collection in neither copy nor exclude is undeclared: it is never copied and the report lists it, so a collection added to the definitions later is not copied until someone decides about it.

paths pins a copied collection to Firestore paths: a collection path (teams/*/ext/calendar/events) or a document path (teams/*/ext/calendar), in which "*" stands for any document id and every other id must be the one in the document's path. A document of the collection at another path (an events collection of another module) is not copied and is counted as an "other parent". A mask path names a declared column, and below it only what the column's type can hold. The policy holds no credential and no connection: unknown keys are refused.

func ParsePolicy

func ParsePolicy(content []byte) (*Policy, error)

ParsePolicy decodes policy YAML, refusing unknown keys and a second document.

func ReadPolicy

func ReadPolicy(path string) (*Policy, error)

ReadPolicy reads and checks the structure of a policy file. Whether its collections are defined is checked by Check, once the definitions are read.

func (*Policy) Check

func (p *Policy) Check(defs *Definitions) error

Check verifies the policy against the definitions: every collection it names is defined, none is both copied and excluded or named twice, every masked or pinned collection is copied, every mask path names a declared column and what its type can hold, and every path pattern fits its collection.

func (*Policy) DefinitionsDir

func (p *Policy) DefinitionsDir() string

DefinitionsDir is the definitions folder the policy names ("" if none).

func (*Policy) OutputDir

func (p *Policy) OutputDir() string

OutputDir is the output folder the policy names ("" if none).

type Query

type Query struct {
	// Collection is a collection path (a root collection id, or a/1/b for the
	// collection b of the document a/1), or, for a group, a collection id.
	Collection string
	// Group reads every collection named Collection, at any depth.
	Group bool
	// Under, when set, keeps a group query to the documents at or below this
	// document path: a range on the document name, so documents of a sibling
	// whose id starts with the same text (a/1x below a/1) are not read.
	Under string
	// After continues the read after this document path.
	After string
	// Limit is, for Page, the most documents to return (> 0); for Count, the
	// most documents to count (0: all of them).
	Limit int
}

Query reads the documents of one collection, or of every collection with one id (a collection group), in document-path order.

type Rejection

type Rejection struct {
	Path       string   `json:"path"`
	Collection string   `json:"collection"`
	Reasons    []string `json:"reasons"`
	Fields     []string `json:"fields,omitempty"`
}

Rejection is a document that was read but not written into the database: it does not fit its definition, or its file cannot be written. The report keeps its path, the reasons and the names of the declared fields concerned, never a value. A field name the definition does not declare, and a map key, may be data (an email used as a key): they are only counted, with a fingerprint that tells equal sets of names apart within one run.

type Report

type Report struct {
	Format         string          `json:"format"`
	Source         string          `json:"source"`
	SourceIdentity *SourceIdentity `json:"source_identity,omitempty"`
	Under          string          `json:"under,omitempty"`
	DryRun         bool            `json:"dry_run,omitempty"`
	Complete       bool            `json:"complete"`
	BudgetReached  bool            `json:"budget_reached,omitempty"`
	// Resumed says the run continued one that stopped at its read budget.
	Resumed    bool     `json:"resumed,omitempty"`
	ReadTime   string   `json:"read_time,omitempty"`
	Incomplete []string `json:"incomplete,omitempty"`
	// Reads are this run's billed reads; ReadsTotal adds those of the runs it resumed.
	Reads                    int64              `json:"reads"`
	ReadsTotal               int64              `json:"reads_total,omitempty"`
	MaxReads                 int64              `json:"max_reads,omitempty"`
	Records                  int64              `json:"records"`
	Collections              []CollectionResult `json:"collections"`
	Rejected                 []Rejection        `json:"rejected,omitempty"`
	UndefinedRootCollections []string           `json:"undefined_root_collections,omitempty"`
	Outside                  []OutsideRead      `json:"read_outside_selection,omitempty"`
	Stale                    []string           `json:"stale_record_files,omitempty"`
	Pruned                   []string           `json:"pruned_record_files,omitempty"`
	Validation               []string           `json:"validation_errors,omitempty"`

	// Written and Unchanged count record files of this run (not in the
	// report file: they differ between a first run and a re-run).
	Written   int64 `json:"-"`
	Unchanged int64 `json:"-"`
}

Report is the result of a run. Without Snapshot and resume it holds no time, host or credential, so the same source and options give the same report.

func Run

func Run(ctx context.Context, src Source, defs *Definitions, policy *Policy, opts Options) (*Report, error)

Run copies what the policy and options select from src into opts.Output (or, for a dry run, only counts it) and returns the report.

The returned error is a failure to read or write (or ErrUsage for a bad option). A run that stops at MaxReads, or that read documents which do not fit their definition, returns a report with Complete false and no error.

type Source

type Source interface {
	// RootCollections lists the ids of the root collections.
	RootCollections(ctx context.Context) ([]string, error)
	// Page returns at most q.Limit documents of q, in document-path order.
	Page(ctx context.Context, q Query) ([]Doc, error)
	// Get returns the document at path, or nil when there is none.
	Get(ctx context.Context, path string) (*Doc, error)
	// Count returns how many documents q (without After) matches, counting at
	// most q.Limit of them when it is set.
	Count(ctx context.Context, q Query) (int64, error)
}

Source reads a Firestore database. Every method only reads: there is no method that could change the source, so the engine cannot write to it.

type SourceIdentity

type SourceIdentity struct {
	Project  string `json:"project"`
	Database string `json:"database"`
	Emulator string `json:"emulator,omitempty"`
}

SourceIdentity names the database a mirror reads. It holds no credential: the resolved project and database ids, and the emulator host when FIRESTORE_EMULATOR_HOST was in effect (an emulator and a production project of one id are different sources).

func (SourceIdentity) String

func (s SourceIdentity) String() string

type Spec

type Spec struct {
	// ID is the collection ids of the path joined by "/".
	ID string
	// Segments are the collection ids of the path, root first.
	Segments []string
	// Def is the inGitDB definition of this collection.
	Def *ingitdb.CollectionDef
	// Unsupported, when not empty, says why records of this collection cannot
	// be written; the collection is then reported and never read.
	Unsupported string
	// contains filtered or unexported fields
}

Spec is one defined collection of the mirror: one Firestore collection path such as teams/{team}/ext/{module}/events, named by its collection ids alone ("teams/ext/events"), the way inGitDB names a sub-collection.

func (*Spec) Depth

func (s *Spec) Depth() int

Depth is the number of collection levels of the path (1 for a root collection).

func (*Spec) Leaf

func (s *Spec) Leaf() string

Leaf is the id of the collection itself (the last segment).

Jump to

Keyboard shortcuts

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