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
- Variables
- type CollectionResult
- type Definitions
- type Doc
- type FirestoreSource
- func (s *FirestoreSource) Close() error
- func (s *FirestoreSource) Count(ctx context.Context, q Query) (int64, error)
- func (s *FirestoreSource) Get(ctx context.Context, path string) (*Doc, error)
- func (s *FirestoreSource) Page(ctx context.Context, q Query) ([]Doc, error)
- func (s *FirestoreSource) ReadAt(t time.Time)
- func (s *FirestoreSource) RootCollections(ctx context.Context) ([]string, error)
- type KeyEncoding
- type Options
- type OutsideRead
- type Policy
- type Query
- type Rejection
- type Report
- type Source
- type SourceIdentity
- type Spec
Constants ¶
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.
const DefaultPageSize = 300
DefaultPageSize is how many documents one read request asks for.
const PolicyFormat = "datatug-mirror-policy/v1"
PolicyFormat is the format a mirror policy file must declare.
const ReportFormat = "datatug-mirror-report/v1"
ReportFormat is the format of the report a run writes.
Variables ¶
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.
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).
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) 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-copy.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 ¶
ParsePolicy decodes policy YAML, refusing unknown keys and a second document.
func ReadPolicy ¶
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 ¶
DefinitionsDir is the definitions 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.