datastore

package
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

datastore — Firestore in Datastore mode for TinyGo

nosql/datastore speaks the Datastore v1 JSON API, which is what Firestore exposes when a database is created in Datastore mode.

client, err := datastore.New("my-project")
defer client.Close()

key := datastore.NameKey("Task", "first")
_, err = client.Put(ctx, datastore.NewEntity(key).
    Set("title", datastore.String("write the driver")).
    Set("done", datastore.Bool(false)).
    Set("priority", datastore.Int(3)))

entity, err := client.Get(ctx, key)
title, _ := entity.Properties["title"].AsString()

Why this exists

Google ships two Go clients for this API and neither builds under TinyGo, for unrelated reasons.

cloud.google.com/go/datastore is gRPC-only with no REST fallback, and gRPC fails at google.golang.org/grpc/internal/credentials: cfg.Clone undefined (type *tls.Config has no field or method Clone). That is TinyGo's crypto/tls stub surfacing one layer up. 465 packages in the closure.

google.golang.org/api/datastore/v1, the generated REST client, does exist and also fails — at TinyGo's empty net/http.Transport, inside cloud.google.com/go/compute/metadata. 428 packages, 64 of them still gRPC. The deprecated datastore.New(*http.Client) constructor does not help: the generated package imports google.golang.org/api/internal unconditionally, so the closure and the error are identical.

Implementation selection

The HTTP stack follows the repository convention: net/http on host Go, this repository's https package on TinyGo builds. -tags force_tinygo_logic runs the native path under host Go so it is testable without TinyGo.

google.Backend names the HTTP stack and google.SignerBackend() names the RSA implementation.

What maps from DynamoDB, and what does not

"Equivalent to the DynamoDB client" is a statement about effort and shape, not about API parity.

Same shape: POST plus JSON, one operation per request; a one-member union value type, hand-encoded; int64 as text; an opaque cursor the caller feeds back; one host and many small requests; a typed error with sentinels; Close releasing pooled TLS handles.

Different:

DynamoDB Datastore
Auth SigV4 per request bearer token, minted once an hour
Identity table + partition/sort key kind + key path; ancestors are part of identity
Tenancy none a namespace on every key
Reads GetItem single, BatchGetItem batch lookup is batch by construction
Writes separate operations one commit carries a mutation list
Partial update UpdateItem expression none; update replaces the whole entity
Conditions a condition expression verb preconditions, baseVersion, or a transaction
Transactions out of scope in scope; the only conditional path
Integrity x-amz-crc32 nothing
Tables create/describe/list none; kinds are implicit
Throttling provisioned capacity contention, reported as ABORTED

A caller porting from nosql/dynamodb rewrites the write path and keeps the read path.

Values

Value is a proto3 oneof, so exactly one member is set. Zero is ErrEmptyValue and two is ErrAmbiguousValue; the wire carries one member, so there is no correct choice to make and picking one silently would encode something the caller did not mean.

datastore.String("s")            datastore.Blob([]byte{1, 2})
datastore.Int(42)                datastore.Time(t)
datastore.IntString("...")       datastore.KeyValue(k)
datastore.Float(1.5)             datastore.GeoPoint(35.6, 139.7)
datastore.Bool(true)             datastore.Nested(entity)
datastore.Null()                 datastore.Array(a, b)

datastore.Unindexed(v)           // composes with any of the above

Three distinctions the type keeps that a map[string]any could not:

  • Integer is text. proto3 JSON encodes int64 as a string, which is what keeps a 64-bit id from passing through float64. AsInt converts; AsNumber returns the stored text.
  • Integer and double are different types. AsFloat refuses an integerValue and AsInt refuses a doubleValue. Datastore stores them apart, and quietly widening one to the other is how a filter stops matching.
  • Null is not absent. A property set to null and a property that is missing are different things to a query filter, and both are representable.

Timestamps go out as RFC 3339. Datastore stores microseconds, so a finer value loses resolution on the server; the constructor does not hide that.

An embedded entity has no key. One that carries a key is rejected at encode time rather than silently stripped.

Int accepts every Go integer type that fits int64 on every platform. uint is not among them: on a 64-bit platform it holds values int64 does not, and Int has no error to return, so admitting it meant Int(uint(math.MaxUint64)) storing -1 silently. A uint caller writes Int(int64(n)) when the value is known to fit, or IntString(strconv.FormatUint(n, 10)) when it is not — and the latter fails at encode time if it really is too wide.

Struct mapping

MarshalEntity and UnmarshalEntity map a struct to an entity. They are the only place this package uses reflection, and they live in their own file, so a program that never calls them does not link them.

type Task struct {
    Key      datastore.Key `datastore:"__key__"`
    Title    string        `datastore:"title"`
    Body     string        `datastore:"body,noindex"`
    Draft    string        `datastore:"draft,omitempty"`
    Internal string        `datastore:"-"`
}

e, err := datastore.MarshalEntity(t)
_, err = client.Put(ctx, e)

A field tagged __key__ carries the entity's own key and must be a Key or *Key; Datastore reserves that name for the key in queries, so no real property can collide. Without such a field the entity comes back with no key and the caller attaches one.

Supported: string, the integer and float kinds, bool, []byte, time.Time, Key, Value, slices, structs, and pointers to any of those. A nil pointer becomes null, which is a value and not an absence — use ,omitempty when absent is what you meant. Decoding mirrors that: an explicit null zeroes the field, an absent property leaves it alone.

Maps are refused. Datastore has no map type, so a map would become an embedded entity whose property names come from runtime data rather than from the struct — the one thing this mapping exists to avoid.

A uint64 above MaxInt64 is refused rather than wrapped: Datastore integers are signed 64-bit and have no representation for it.

The datastore tag is authoritative for this path only. A code generator over this driver reads its own tag, and a struct carrying both gets two field mappings that look interchangeable and disagree on every renamed property. If you generate a codec, treat a field carrying this tag but not yours as an error rather than as agreement.

Keys

datastore.NameKey("Task", "first")            // string name
datastore.IDKey("Task", 5001)                 // numeric id
datastore.IncompleteKey("Task")               // the server allocates
parent.Child(datastore.PathElement{Kind: "Task", Name: "sub"})
key.WithNamespace("tenant-a")

A key carries what identifies the entity and nothing else. The project and database are added by the client at encode time, so a Key stays portable inside a program.

That applies wherever a key appears, not just to an entity's own key: a KeyValue stored as a property, one inside an Array — which is what ref IN (...) needs — and one inside a nested entity all get the partition attached on the way out. Marshalling a Key or a Value yourself produces one without a partition, by design, because nothing below the client knows which project it is for.

An incomplete key is legal only in an Insert or AllocateIDs. Only the last path element may be incomplete: an ancestor without an identifier does not name anything.

Queries

q := datastore.NewQuery("Task").
    Ancestor(parent).
    Filter("done", datastore.Equal, datastore.Bool(false)).
    Filter("priority", datastore.GreaterThanEqual, datastore.Int(3)).
    Order("created").
    Limit(50)

for {
    batch, err := client.Run(ctx, q)
    // ... use batch.Entities
    if !batch.HasMore() {
        break
    }
    q = q.Start(batch.EndCursor)
}

Every builder method returns a new Query, so a partially built query can be shared without one caller's additions reaching another.

AND and OR

Repeated Filter calls combine with AND. For a disjunction, build a condition tree and attach it with Where:

q := datastore.NewQuery("Task").
    Filter("owner", datastore.Equal, datastore.String("me")).
    Where(datastore.Or(
        datastore.Prop("state", datastore.Equal, datastore.String("new")),
        datastore.And(
            datastore.Prop("starred", datastore.Equal, datastore.Bool(true)),
            datastore.Prop("done", datastore.Equal, datastore.Bool(false)),
        ),
    ))

Prop, And and Or nest freely. AncestorOf(key) is a condition too, so an ancestor restriction can sit inside a disjunction.

Narrowing what comes back
q = q.Project("title", "priority")   // only these properties, read from an index
q = q.KeysOnly()                     // keys alone, the cheapest read there is
q = q.DistinctOn("owner")            // collapse results sharing these properties
q = q.Offset(20)                     // skip, but see below

Every projected property must be indexed, because a projection query reads from the index rather than from the entity.

Offset skips results the server has already read and billed you for. Batch.SkippedResults reports how many, which is the number to look at when deciding to move to a cursor instead — Start resumes without paying for what came before.

Projection and distinct constraints are the service's and they are subtle, so they are passed through unvalidated: a client-side check that was wrong would refuse a query that works.

Repeated Where calls, and a Where alongside a Filter, combine with AND — so an Or belongs inside one call, not spread across two.

A query is limited to MaxDisjunctions (30) once its filter is put in disjunctive normal form, so nesting Or inside And multiplies rather than adds. That bound is enforced by the service, not here: the expansion rule is the service's, and a client-side count that disagreed would refuse a query that works.

There is no iterator hiding round trips. EndCursor feeds Start explicitly, the same shape as storage/s3 and nosql/dynamodb.

Aggregations
n, err := client.Count(ctx, q)                    // int64
total, err := client.Sum(ctx, q, "celsius")       // Value: integer or double
mean, err := client.Avg(ctx, q, "celsius")        // Value: double, or null

All three use runAggregationQuery, and all three exist for the same reason: the paging alternative costs a read per entity. Counting by paging can at least be done keys-only; summing by paging cannot, because every entity has to come back in full for the caller to add one property up. So paging to sum is strictly more expensive than paging to count.

Sum and Avg return a Value rather than a Go number. A sum is an integer when every summed value was an integer and a double otherwise, and an average over nothing is null — where zero would be a different claim. Flattening either would erase the integer-versus-double distinction the rest of this package keeps.

They are available inside a transaction too, via Tx.Count, Tx.Sum and Tx.Avg.

Sum and Avg shipped on 2026-08-04, and the "not in scope" list below still excluded them for a day afterwards. A consumer read that line and wrote the paging loop including them was meant to prevent. A test now fails if that list names anything this package exports, which is why the list is only a list.

Conditional writes

There is no condition expression language here. What the wire offers is:

  • Insert fails with ErrAlreadyExists if the key is taken — put-if-absent
  • Update fails with ErrNoSuchEntity if it is absent — put-if-present
  • WithBaseVersion(v) / WithUpdateTime(t) — optimistic concurrency, from a previous read
  • everything else needs a transaction
err := client.RunInTransaction(ctx, func(tx *datastore.Tx) error {
    current, err := tx.Get(ctx, key)
    if err != nil {
        return err
    }
    n, _ := current.Properties["count"].AsInt()
    if n >= limit {
        return errTooMany            // nothing is written
    }
    current.Properties["count"] = datastore.Int(n + 1)
    tx.Put(*current)
    return nil
})

The predicate runs in Go, between a read and a commit that share a snapshot. That is why the transaction is required and not optional: a client-side check against a value read outside one is a race with a confident-looking API.

Mutations are queued and sent with the commit, so a closure that returns an error writes nothing.

RunReadOnly is the same shape without the writes: several reads over one consistent snapshot, taking no write locks. A closure that queues a mutation inside one is refused rather than silently dropped.

A transaction costs one fewer round trip than it looks. There is no separate beginTransaction: the first read asks to start one and its reply carries the handle, and a closure that never reads folds its transaction into the commit.

closure round trips
one read, then commit 2
N reads, then commit N+1
writes only, no read 1
neither 0

The last row is not a trick: no handle was ever taken, so there is nothing to release. None of this restricts what a closure may do — a second read simply uses the handle the first brought back.

The closure can run more than once. Datastore reports contention as ABORTED, and the right response is to re-run the whole closure — the reads it decided on are stale. So it must have no side effects outside the transaction. That cannot be enforced, only stated.

Limits and chunking

Exported, because a caller batching work has to chunk against them and a number copied out of Google's documentation into every consumer drifts silently when the service changes it.

Constant Value
MaxLookupKeys 1000 keys per lookup — GetMulti checks this before sending
MaxRequestBytes 10 MiB
MaxTransactionBytes 10 MiB
MaxEntityBytes 1 MiB − 4
MaxKeyBytes 6 KiB
MaxIndexedStringBytes 1500; a longer string is stored but not indexed
MaxNestingDepth 20

There is no maximum-mutations-per-commit constant, and that is not an oversight. Google documents no count limit on a commit — the bound is bytes, MaxRequestBytes and, inside a transaction, MaxTransactionBytes. The only documented count of 500 is property transformations per entity, which this package excludes. So chunk a batch write by size, not by count.

Client.MutationSize gives that size without a throwaway marshal, and Client.CommitOverheadBytes gives the request built around the mutations:

batch, used := []datastore.Mutation{}, 0
for _, m := range mutations {
    n, err := client.MutationSize(m)
    if err != nil {
        return err
    }
    if used+n+client.CommitOverheadBytes(len(batch)+1) > datastore.MaxRequestBytes {
        client.Mutate(ctx, batch)
        batch, used = batch[:0], 0
    }
    batch, used = append(batch, m), used+n
}

The two account for the whole body exactly:

CommitOverheadBytes(len(ms)) + Σ MutationSize(m) == bytes sent to :commit

MutationSize is a method on Client rather than on Entity because it includes the key with its project, database and namespace attached, and only the client knows those. An Entity-level figure would understate every mutation by exactly the part the caller cannot see.

CommitOverheadBytes is the rest of the request: the mode, the array holding the mutations, the comma between each pair, and the databaseId when the client has one. Summing MutationSize alone undercounts by 42 + n bytes for n mutations, plus the length of "databaseId":"<name>", when a database is named. It takes a count rather than the mutations so chunking stays a running total — the question at each step is whether one more fits, and a whole-batch measure would re-encode everything to answer it.

A named database is counted twice, and both are real: every key carries the partition, so it is inside each mutation, and the request carries it once more at the top level.

Inside a transaction, use Tx.CommitOverheadBytes and chunk against MaxTransactionBytes. That commit also carries either the handle its first read returned or a singleUseTransaction block, which are different sizes, and only the transaction knows which it is in.

Composite indexes

Single-property indexes are automatic. A composite index is required when a query combines an equality filter with an inequality on a different property, orders on a property it also filters on inequality, or otherwise needs more than one property considered together. Without one the query fails at runtime with FAILED_PRECONDITION, on code that compiled cleanly.

Index describes one, so a tool that can see the need at build time has somewhere to put it:

idx := datastore.Index{
    Kind: "Task",
    Properties: []datastore.IndexProperty{
        {Name: "done"},
        {Name: "priority", Direction: datastore.Descending},
    },
}
yaml, err := datastore.MarshalIndexYAML([]datastore.Index{idx})
// feed to: gcloud datastore indexes create index.yaml

The output is sorted, so a tool that regenerates it produces a stable diff.

This is a description, not a request. Applying an index is an admin-API operation and stays out of scope; the shape of an index is a property of the service rather than of any one tool, which is why the type lives here instead of being reinvented by every generator.

There is deliberately no RequiredIndex(*Query). The rule for when a composite index is needed is subtle, and a derivation that is quietly wrong is worse than none — it would name an index that does not fix the query.

TTL

TTL is not expressible on this wire. It is a policy over an ordinary timestamp property, configured out of band:

gcloud firestore fields ttls update expiresAt --collection-group=Task --enable-ttl

So an expiring entity needs nothing special from this package: write a datastore.Time(...) property and point a policy at it. The property must be a timestamp; leaving it absent or null disables expiry for that entity, which is the per-entity opt-out. One property per kind may be a TTL property, and a database may hold at most 500 TTL policies. Deletion happens within about 24 hours of expiry, so it is a retention mechanism and not a correctness one — a read may still return an expired entity, and application code has to check.

Datastore mode additionally cannot use TTL with a concurrency mode of Optimistic With Entity Groups.

Errors

Match on the sentinel, never on the HTTP code:

if errors.Is(err, datastore.ErrAlreadyExists) { ... }

ABORTED and ALREADY_EXISTS are both HTTP 409 and mean opposite things, one retryable and one terminal. Classification keys on the canonical status string in the error body; a reply with no body falls back to the code, and 409 falls back to ALREADY_EXISTS because guessing wrong in the retryable direction would retry a duplicate insert forever.

Retries and contention

Status Behaviour
UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED retried, 3 attempts, 25 ms base, 1 s cap, full jitter
INTERNAL retried exactly once, per Google's documented guidance
UNAUTHENTICATED token refreshed once and resent; not charged to the retry budget
ABORTED inside a transaction the closure re-runs
ABORTED outside one terminal; there is nothing to re-run
everything else terminal

There is no x-amz-crc32 equivalent, so unlike nosql/dynamodb there is no response-integrity layer. TLS is the only guarantee on this path. That is a real difference, not an oversight.

Beneath this client, the native transport replays a request once when a pooled connection turns out to have been closed by the peer, so the honest worst case is attempts × 2 deliveries. The mutation verbs are idempotent by construction — Insert repeats as ALREADY_EXISTS, deleting an absent key succeeds, and Update replaces rather than accumulates — so that is harmless for them. There is no server-side arithmetic on this wire to be doubled, which is why the UpdateItem-with-ADD hazard from nosql/dynamodb has no counterpart here. A replayed transactional commit fails rather than double-writing, because the handle is consumed.

WithRetry(0, 0) disables retrying. A cancelled context stops it immediately.

Connections

Every request goes to one host, so the per-host cap is the whole pool. The default is 4 idle connections; WithMaxIdleConns(n) should be set to the concurrency the application runs.

Close is required rather than cosmetic: pooled native TLS handles outlive the last request otherwise. A client built with WithHTTPClient is left alone by Close, since its owner may still be using it.

The measured numbers live in the https README, which owns the transport.

Configuration

Option Default
WithEndpoint DATASTORE_EMULATOR_HOST, else https://datastore.googleapis.com
WithDatabase the project's default database
WithNamespace empty
WithCredentials / WithTokenSource GOOGLE_APPLICATION_CREDENTIALS
WithTimeout 10 s
WithMaxIdleConns 4
WithRetry 3 attempts, 25 ms base

WithReadTime reads as of a past instant. Within the past hour any microsecond-granularity instant is legal; from one hour to seven days back only whole-minute timestamps are, and only with point-in-time recovery enabled. Truncate to a whole minute yourself for the older window — the client does not, because that would change the instant you asked for, and the service refuses an untruncated one as "read_time is too old", naming the age when the precision was the problem.

A value with no scheme is taken as http, which is what DATASTORE_EMULATOR_HOST carries. When the emulator variable is set and no endpoint is given, the client sends no Authorization header at all: the emulator ignores it, and minting a token it will not read would be pretending to test something.

Not in scope

GQL, reserveIds, the admin API (index management, import, export), auto-pagination, and Firestore native mode's listeners, which Datastore mode does not have.

Property transformations — server-side increment and array-append — are excluded deliberately: they exist on the wire only inside commit, and they would reintroduce exactly the non-idempotent-retry hazard the rest of this design avoids.

Testing

go test ./nosql/datastore/
go test -tags force_tinygo_logic ./nosql/datastore/

Both commands run the same tests against a stub server that records what it was sent, so request shapes, retry counts, and transaction sequencing are pinned offline.

For a real server:

gcloud beta emulators datastore start --host-port=127.0.0.1:8081
DATASTORE_EMULATOR_HOST=127.0.0.1:8081 DATASTORE_PROJECT_ID=demo \
    tinygo run ./examples/datastoredemo

The emulator covers the codec, queries, transactions, and error paths. It does not cover authentication, because it ignores Authorization entirely — a sharper gap than DynamoDB Local's, where the signature is at least required to be present and well formed. The token path needs one manual run against a real project.

Documentation

Index

Constants

View Source
const (
	// MaxLookupKeys is the most keys one lookup accepts. GetMulti checks this
	// before sending.
	MaxLookupKeys = 1000

	// MaxRequestBytes bounds one API request.
	MaxRequestBytes = 10 << 20

	// MaxTransactionBytes bounds everything one transaction writes.
	MaxTransactionBytes = 10 << 20

	// MaxEntityBytes is the largest a single entity may be.
	MaxEntityBytes = 1<<20 - 4

	// MaxKeyBytes is the largest a single key may be.
	MaxKeyBytes = 6 << 10

	// MaxIndexedStringBytes is where a string property stops being indexed.
	// A longer value is still stored; it just cannot be filtered or ordered on.
	MaxIndexedStringBytes = 1500

	// MaxNestingDepth is how deep entity values may nest.
	MaxNestingDepth = 20
)

Service limits, from the published quotas. They are exported because a caller batching work has to chunk against them, and a number copied out of the documentation into every consumer drifts silently when the service changes it.

There is deliberately no maximum-mutations-per-commit constant: Google documents no count limit on a commit. A commit is bounded in bytes, by MaxRequestBytes and, inside a transaction, MaxTransactionBytes. The only documented count of 500 is property transformations per entity, which requirement:datastore-client-scope excludes. So chunk a batch write by size, not by count.

View Source
const MaxDisjunctions = 30

MaxDisjunctions is how many disjunctions a query may expand to once its filter is put in disjunctive normal form. A query past it is rejected by the service, not here: the expansion rule is the service's and a client-side count that disagreed would refuse a query that works.

Variables

View Source
var (
	ErrNoProject     = errors.New("datastore: no project configured")
	ErrNoCredentials = errors.New("datastore: no credentials configured")
)

Configuration errors.

View Source
var (
	ErrNoSuchEntity       = errors.New("datastore: no such entity")
	ErrAlreadyExists      = errors.New("datastore: entity already exists")
	ErrAborted            = errors.New("datastore: transaction aborted by contention")
	ErrFailedPrecondition = errors.New("datastore: failed precondition")
	ErrInvalidArgument    = errors.New("datastore: invalid argument")
	ErrPermissionDenied   = errors.New("datastore: permission denied")
	ErrUnauthenticated    = errors.New("datastore: unauthenticated")
	ErrUnavailable        = errors.New("datastore: service unavailable")
	ErrDeadlineExceeded   = errors.New("datastore: deadline exceeded")
	ErrResourceExhausted  = errors.New("datastore: resource exhausted")
	ErrInternal           = errors.New("datastore: internal server error")
)

Sentinels for the canonical error codes Datastore returns. Match on these with errors.Is rather than on the HTTP status: ABORTED and ALREADY_EXISTS are both 409 and mean opposite things, one retryable and one terminal.

View Source
var (
	ErrEmptyKeyPath  = errors.New("datastore: key has no path elements")
	ErrAmbiguousID   = errors.New("datastore: path element has both an id and a name")
	ErrIncompleteKey = errors.New("datastore: key is incomplete")
)

Key errors.

View Source
var (
	ErrNoKind      = errors.New("datastore: query has no kind")
	ErrBadOperator = errors.New("datastore: unknown filter operator")
)

Query errors.

View Source
var (
	ErrEmptyValue     = errors.New("datastore: value has no member set")
	ErrAmbiguousValue = errors.New("datastore: value has more than one member set")
	ErrBadValue       = errors.New("datastore: malformed value on the wire")
)

Codec failures. A Value carries exactly one member on the wire, because it is a proto3 oneof, so neither zero nor two is something this package can encode on the caller's behalf.

View Source
var ErrEmptyCondition = errors.New("datastore: And or Or with no conditions")

ErrEmptyCondition is returned for And or Or with no operands, which has no meaning on the wire.

View Source
var (
	ErrEmptyIndex = errors.New("datastore: index has no kind or no properties")
)

Index errors.

View Source
var ErrTooManyKeys = errors.New("datastore: more than 1000 keys in one lookup")

ErrTooManyKeys is returned before a request the server would reject.

View Source
var ErrTxClosed = errors.New("datastore: transaction is no longer usable")

ErrTxClosed is returned by a Tx used after its closure returned.

Functions

func MarshalIndexYAML added in v1.1.5

func MarshalIndexYAML(indexes []Index) ([]byte, error)

MarshalIndexYAML renders indexes in the index.yaml form that `gcloud datastore indexes create` consumes.

The output is sorted by kind and then by property list, so a tool that regenerates it produces a stable diff rather than a reordering.

It is written by hand rather than through a YAML library. The shape is four keys deep and closed, and this module has no external dependencies — acquiring one for this would cost more than the feature.

func UnmarshalEntity added in v1.1.5

func UnmarshalEntity(e Entity, out any) error

UnmarshalEntity fills a struct from an Entity.

Properties with no matching field are ignored, and fields with no matching property are left alone: an entity of one kind need not carry the same properties as the next, so neither is an error.

A field tagged "__key__" receives the entity's key.

Types

type Batch

type Batch struct {
	Entities  []Entity
	EndCursor Cursor
	More      MoreResults

	// SkippedResults counts entities the offset stepped over. They were read
	// and billed.
	SkippedResults int32
}

Batch is one page of query results.

func (*Batch) HasMore

func (b *Batch) HasMore() bool

HasMore reports whether running the query again from EndCursor could return anything.

type Client

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

Client talks to one Datastore endpoint.

It is safe for concurrent use. Close releases pooled connections and the cached token; a client dropped without Close leaves native TLS handles alive until they idle out, which is the whole reason this repository exists.

func New

func New(projectID string, opts ...Option) (*Client, error)

New builds a client for one project.

Credentials resolve in this order: an explicit token source, explicit credentials, the emulator (which needs none), then GOOGLE_APPLICATION_CREDENTIALS.

func (*Client) AllocateIDs

func (c *Client) AllocateIDs(ctx context.Context, keys []Key) ([]Key, error)

AllocateIDs completes incomplete keys, for referring to an entity before writing it.

func (*Client) Avg added in v1.1.6

func (c *Client) Avg(ctx context.Context, q *Query, property string, opts ...ReadOption) (Value, error)

Avg averages one property across the entities the query matches.

The result is a double, or null when nothing matched — which is why this returns a Value too: zero would be a different claim from "no data".

func (*Client) Close

func (c *Client) Close() error

Close releases pooled connections and the signing key.

A client built with WithHTTPClient leaves that client alone, since its owner may still be using it.

func (*Client) CommitOverheadBytes added in v1.1.9

func (c *Client) CommitOverheadBytes(n int) int

CommitOverheadBytes reports how many bytes a commit of n mutations spends on everything that is not the mutations themselves: the mode, the array that holds them, the comma between each pair, and the databaseId when this client has one.

MutationSize measures a mutation; this measures the request built around them, which is the part a caller summing MutationSize cannot see. Together they account for the whole body:

c.CommitOverheadBytes(len(ms)) + Σ c.MutationSize(m) == bytes sent to :commit

It takes a count rather than the mutations so that chunking stays a running total. A caller asks whether one more fits by adding that one's MutationSize and re-reading the overhead for n+1, instead of re-measuring the batch on every step.

A named database is counted twice over, and both are real: once inside each mutation, because every key carries the partition, and once here for the request-level databaseId.

This is the non-transactional envelope. A commit inside a transaction also carries a handle or a singleUseTransaction block, and only the transaction knows which; use Tx.CommitOverheadBytes there.

func (*Client) Count

func (c *Client) Count(ctx context.Context, q *Query, opts ...ReadOption) (int64, error)

Count returns how many entities the query matches.

It exists because counting by paging through keys costs a read per entity, so leaving it out would push callers toward the expensive thing.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, k Key, opts ...WriteOption) error

Delete removes an entity. Deleting an absent key succeeds.

func (*Client) Endpoint

func (c *Client) Endpoint() string

Endpoint is the host this client sends to.

func (*Client) Get

func (c *Client) Get(ctx context.Context, key Key, opts ...ReadOption) (*Entity, error)

Get reads one entity, returning ErrNoSuchEntity when it is absent.

func (*Client) GetMulti

func (c *Client) GetMulti(ctx context.Context, keys []Key, opts ...ReadOption) (*LookupResult, error)

GetMulti reads several entities.

It returns found, missing and deferred rather than failing, because the server answers a lookup by partitioning the keys. Deferred keys are handed back, not retried inside the call.

func (*Client) Insert

func (c *Client) Insert(ctx context.Context, e Entity, opts ...WriteOption) (Key, error)

Insert writes an entity, failing with ErrAlreadyExists if the key is taken.

An incomplete key is allowed here and only here; the allocated key comes back in the result.

func (*Client) Mutate

func (c *Client) Mutate(ctx context.Context, ms []Mutation) (*CommitResult, error)

Mutate applies several mutations in one commit.

Without a transaction this is NON_TRANSACTIONAL, where the server requires that no two mutations touch the same entity and does not promise all-or-none. Inside RunInTransaction it is atomic.

func (*Client) MutationSize added in v1.1.6

func (c *Client) MutationSize(m Mutation) (int, error)

MutationSize reports how many bytes m contributes to a commit request.

It exists so a caller chunking a batch write against MaxRequestBytes does not have to marshal each entity itself and then hand it to this package to be marshalled again. Datastore publishes no per-commit mutation count, so size is the only bound there is to chunk against.

This is the encoded mutation, including the key with its project, database and namespace attached — which is why it is a method on Client rather than on Entity: only the client knows the partition, and an Entity-level figure would understate every mutation by exactly the part the caller cannot see.

func (*Client) Namespace

func (c *Client) Namespace() string

Namespace is the default namespace for keys that carry none.

func (*Client) ProjectID

func (c *Client) ProjectID() string

ProjectID is the project this client addresses.

func (*Client) Put

func (c *Client) Put(ctx context.Context, e Entity, opts ...WriteOption) (Key, error)

Put writes an entity, creating or replacing it. It sends an upsert.

Update replaces the whole entity: there is no partial update on this wire and no server-side arithmetic, which is also why a replayed Put cannot double anything.

func (*Client) Run

func (c *Client) Run(ctx context.Context, q *Query, opts ...ReadOption) (*Batch, error)

Run executes a query and returns one batch.

There is no iterator hiding round trips: feed EndCursor back through Query.Start to continue, the same shape the S3 and DynamoDB clients use.

func (*Client) RunInTransaction

func (c *Client) RunInTransaction(ctx context.Context, fn func(*Tx) error, opts ...TxOption) error

RunInTransaction runs fn inside a read-write transaction and commits what it queued.

The closure can run more than once. Datastore reports contention as ABORTED, and the right response is to re-run the whole closure, not to resend the commit: the reads it decided on are stale. So fn must have no side effects outside the transaction — that cannot be enforced, only stated.

This is also the only way to express a conditional write richer than the insert and update preconditions. The predicate runs in Go, between a read and a commit that share a snapshot, which is what makes it safe.

func (*Client) RunReadOnly

func (c *Client) RunReadOnly(ctx context.Context, fn func(*Tx) error, opts ...TxOption) error

RunReadOnly runs fn inside a read-only transaction, which gives several reads one consistent snapshot without taking write locks.

func (*Client) Sum added in v1.1.6

func (c *Client) Sum(ctx context.Context, q *Query, property string, opts ...ReadOption) (Value, error)

Sum totals one property across the entities the query matches.

The result is an integer when every summed value is an integer and a double otherwise, which is why this returns a Value rather than one Go type: flattening it would erase the same integer-versus-double distinction the rest of this package keeps. Values of other types are ignored by the service rather than failing the query.

It is here for the reason Count is, applied consistently. Counting by paging can be done keys-only; summing by paging cannot, because every entity has to come back in full for the caller to add one property up. So paging to sum is strictly more expensive than paging to count, and the argument that put Count in scope applies harder here. requirement:datastore-client-scope excluded these as "conveniences over data the caller can page" until a downstream reader pointed that out on 2026-08-04.

func (*Client) Update

func (c *Client) Update(ctx context.Context, e Entity, opts ...WriteOption) error

Update replaces an entity, failing with ErrNoSuchEntity if it is absent.

type CommitResult

type CommitResult struct {
	// Keys are the keys of the written entities, in mutation order. An insert
	// with an incomplete key comes back completed here.
	Keys []Key

	// Versions are the post-commit versions, in the same order.
	Versions []int64

	// IndexUpdates counts the index entries the commit touched, which is what
	// a write is billed on.
	IndexUpdates int

	CommitTime string
}

CommitResult reports what a commit did.

type Condition added in v1.1.6

type Condition interface {
	// contains filtered or unexported methods
}

Condition is a filter tree. Build one with Prop, And and Or, and attach it with Query.Where.

Datastore composes with AND and OR; the OR arm arrived with disjunctive queries and this package believed otherwise until 2026-08-04, when a downstream reader asked whether the AND-only comment was still true. It was not.

func AncestorOf added in v1.1.6

func AncestorOf(key Key) Condition

Ancestor restricts results to descendants of key. It is a filter on the key path rather than on a property, which is why it reads as a condition rather than as a query option.

func And added in v1.1.6

func And(conds ...Condition) Condition

And requires every condition.

func Or added in v1.1.6

func Or(conds ...Condition) Condition

Or requires at least one condition.

A query using it counts against MaxDisjunctions once the whole filter is put in disjunctive normal form, so nesting Or inside And multiplies rather than adds.

func Prop added in v1.1.6

func Prop(property string, op Operator, value Value) Condition

Prop compares one property.

type Cursor

type Cursor string

Cursor is an opaque position in a result set. It is fed back through Start, the same shape the S3 and DynamoDB clients use, rather than hidden inside an iterator that makes round trips the caller cannot see.

type Direction added in v1.1.5

type Direction string

Direction is the sort order of one indexed property.

const (
	Ascending  Direction = "asc"
	Descending Direction = "desc"
)

The index directions, spelled as index.yaml spells them.

type Entity

type Entity struct {
	Key        *Key
	Properties map[string]Value

	Version    int64
	UpdateTime string
	CreateTime string
}

Entity is a key and its properties. Datastore is schemaless, so two entities of one kind need not carry the same properties.

Version and UpdateTime come back from a read and feed the write preconditions in WithBaseVersion and WithUpdateTime. They are ignored on the way out.

func MarshalEntity added in v1.1.5

func MarshalEntity(v any) (Entity, error)

MarshalEntity converts a struct into an Entity.

Fields are named by their datastore tag, or by the field name when there is none. A tag of "-" skips the field, ",omitempty" skips it when it holds the zero value, and ",noindex" sets ExcludeFromIndexes.

A field tagged "__key__" carries the entity's key and must be a Key or *Key. Without one the returned Entity has no key, and the caller supplies it.

The supported types are string, the integer and float kinds, bool, []byte, time.Time, Key, Value, slices, structs, and pointers to any of those. A nil pointer becomes the null value, which is distinct from an absent property; use ",omitempty" when absent is what you meant.

Maps are deliberately unsupported. Datastore has no map type — a map would have to become an embedded entity, whose property names would then come from runtime data rather than from the struct, which is the one thing this mapping exists to avoid.

func NewEntity

func NewEntity(key Key) Entity

NewEntity builds an entity with a key and no properties.

func (Entity) Get

func (e Entity) Get(name string) (Value, bool)

Get returns a property. The second result distinguishes an absent property from one explicitly set to null, which are different things to a filter.

func (Entity) MarshalJSON

func (e Entity) MarshalJSON() ([]byte, error)

MarshalJSON emits the entity without a partition on its key; a Client fills that in on the way out.

func (Entity) Set

func (e Entity) Set(name string, v Value) Entity

Set stores a property, allocating the map on first use, and returns e so calls chain.

func (*Entity) UnmarshalJSON

func (e *Entity) UnmarshalJSON(b []byte) error

UnmarshalJSON reads an entity.

type Error

type Error struct {
	Op         string
	Kind       string
	StatusCode int
	Status     string
	Message    string
}

Error is a failure reported by the service.

Status is the canonical code name, which is the only reliable discriminator: the HTTP code is ambiguous by itself.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Retryable

func (e *Error) Retryable() bool

Retryable reports whether sending the same request again could work.

ABORTED is deliberately absent: outside a transaction it is terminal, and inside one the whole closure re-runs rather than the request. That decision belongs to RunInTransaction, not here.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap maps the status to a sentinel so errors.Is works.

type Index added in v1.1.5

type Index struct {
	Kind string

	// Ancestor reports whether the index serves ancestor queries.
	Ancestor bool

	Properties []IndexProperty
}

Index describes a composite index.

Single-property indexes are automatic and need none of this. A composite index is required when a query combines an equality filter with an inequality on a different property, orders on a property it also filters on inequality, or otherwise needs more than one property considered together. Without one, the query fails at runtime with FAILED_PRECONDITION on code that compiled cleanly, which is the failure this type exists to move earlier.

This is a description, not a request. Applying an index is an admin-API operation and requirement:datastore-client-scope excludes the admin API on purpose; the shape of an index, though, is a property of this service rather than of any one tool, so it belongs here instead of being reinvented by every generator and migration script.

idx := datastore.Index{
    Kind: "Task",
    Properties: []datastore.IndexProperty{
        {Name: "done"},
        {Name: "priority", Direction: datastore.Descending},
    },
}
yaml, _ := datastore.MarshalIndexYAML([]datastore.Index{idx})

func (Index) Equal added in v1.1.5

func (i Index) Equal(other Index) bool

Equal reports whether two indexes describe the same thing. Property order is significant; an empty direction equals Ascending.

func (Index) String added in v1.1.5

func (i Index) String() string

String renders the index compactly, for a diagnostic telling an author what to create. It is not index.yaml; see MarshalIndexYAML for that.

func (Index) Valid added in v1.1.5

func (i Index) Valid() error

Valid reports whether the index is complete enough to describe.

type IndexProperty added in v1.1.5

type IndexProperty struct {
	Name string

	// Direction defaults to Ascending when empty.
	Direction Direction
}

IndexProperty is one property of a composite index. Order matters: an index serves a query only if its properties appear in the order the query needs.

type Integer

type Integer interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint32
}

Integer is the set of Go integer types Int accepts.

Every member fits int64 on every platform, which is what makes Int total: it cannot be handed a value Datastore has no representation for.

~uint was removed on 2026-08-04. On a 64-bit platform uint holds values int64 does not, and Int converted through int64, so Int(uint(math.MaxUint64)) stored "-1" and reported no error — the one silent wrong write in a package that refuses out-of-range integer text, refuses widening a double to an integer, and refuses a uint64 in the struct mapper. Int returns no error, so the constraint is the only place this could be fixed without changing the signature.

A uint caller writes Int(int64(n)) when the value is known to fit, or IntString(strconv.FormatUint(n, 10)) when it is not — and the latter fails loudly at encode time if it really is too wide. 32-bit callers, where uint always fits, pay a conversion for that.

type Key

type Key struct {
	Namespace string
	Path      []PathElement
}

Key identifies an entity. The last path element is the entity itself; the ones before it are its ancestors, which is why ancestry is part of identity here rather than a property.

Namespace is a tenancy dimension with no DynamoDB equivalent. It is empty for the default namespace.

The project and database are not carried here. A Client adds them at encode time, so a Key stays portable inside a program.

func IDKey

func IDKey(kind string, id int64) Key

IDKey builds a one-element key with a numeric id.

func IncompleteKey

func IncompleteKey(kind string) Key

IncompleteKey builds a one-element key for the server to complete.

func NameKey

func NameKey(kind, name string) Key

NameKey builds a one-element key with a string name.

func (Key) Child

func (k Key) Child(e PathElement) Key

Child returns k with one more path element appended, making k its ancestor.

func (Key) Equal

func (k Key) Equal(other Key) bool

Equal reports whether two keys name the same entity.

func (Key) Incomplete

func (k Key) Incomplete() bool

Incomplete reports whether the last element still needs an identifier.

func (Key) Kind

func (k Key) Kind() string

Kind is the kind of the last path element, which is the entity's own kind.

func (Key) MarshalJSON

func (k Key) MarshalJSON() ([]byte, error)

MarshalJSON emits the key without a project. A Client fills the partition in on the way out, because only it knows which project and database the request is for; see (*Client).encodeKey.

func (Key) String

func (k Key) String() string

String renders the key for logs and error messages. It is not a wire format and nothing parses it back.

func (*Key) UnmarshalJSON

func (k *Key) UnmarshalJSON(b []byte) error

UnmarshalJSON reads a key, keeping only the namespace from the partition: the project and database are the client's, not the key's.

func (Key) Valid

func (k Key) Valid() error

Valid reports whether k can be sent. An incomplete key is valid; only insert and AllocateIDs accept one, which is checked where it matters.

func (Key) WithNamespace

func (k Key) WithNamespace(namespace string) Key

WithNamespace returns k in the given namespace.

type Kind

type Kind int

Kind identifies which member of a Value is set.

const (
	KindInvalid Kind = iota
	KindNull
	KindBool
	KindInteger
	KindDouble
	KindTimestamp
	KindKey
	KindString
	KindBlob
	KindGeoPoint
	KindEntity
	KindArray
)

The Value kinds, in the order the wire format lists them.

func (Kind) String

func (k Kind) String() string

String names the kind, for error messages.

type LatLng

type LatLng struct {
	Latitude  float64 `json:"latitude"`
	Longitude float64 `json:"longitude"`
}

LatLng is a geographical point.

type LookupResult

type LookupResult struct {
	Found []Entity

	// Missing are keys with no entity.
	Missing []Key

	// Deferred are keys the server did not read this time. They are handed back
	// rather than retried inside the call, because that is the caller's
	// decision about which reads still matter.
	Deferred []Key
}

LookupResult is what a batch read returns. All three lists matter: the server answers a lookup by partitioning the keys rather than by failing, and a caller batching a thousand keys needs to know which came back.

func (*LookupResult) HasDeferred

func (r *LookupResult) HasDeferred() bool

HasDeferred reports whether the server left keys unread.

type MoreResults

type MoreResults string

MoreResults says why a batch ended.

const (
	NotFinished            MoreResults = "NOT_FINISHED"
	MoreResultsAfterLimit  MoreResults = "MORE_RESULTS_AFTER_LIMIT"
	MoreResultsAfterCursor MoreResults = "MORE_RESULTS_AFTER_CURSOR"
	NoMoreResults          MoreResults = "NO_MORE_RESULTS"
)

The batch termination reasons.

type Mutation

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

Mutation is one change in a commit. Datastore carries the verb in the request rather than in the endpoint, so several verbs fit in one round trip.

func DeleteOp

func DeleteOp(k Key) Mutation

DeleteOp removes an entity. Deleting an absent key succeeds, which is what makes it replay-safe.

func InsertOp

func InsertOp(e Entity) Mutation

InsertOp fails if the key already exists. This is put-if-absent, and it is the closest thing on this wire to a condition expression.

func UpdateOp

func UpdateOp(e Entity) Mutation

UpdateOp fails if the key does not exist. This is put-if-present.

func UpsertOp

func UpsertOp(e Entity) Mutation

UpsertOp writes unconditionally.

func (Mutation) With

func (m Mutation) With(opts ...WriteOption) Mutation

With applies write options, so a Mutation inside Mutate can carry the same preconditions a single-entity call takes as arguments.

type Operator

type Operator string

Operator is a property filter comparison.

const (
	LessThan         Operator = "LESS_THAN"
	LessThanOrEqual  Operator = "LESS_THAN_OR_EQUAL"
	GreaterThan      Operator = "GREATER_THAN"
	GreaterThanEqual Operator = "GREATER_THAN_OR_EQUAL"
	Equal            Operator = "EQUAL"
	NotEqual         Operator = "NOT_EQUAL"
	HasAncestor      Operator = "HAS_ANCESTOR"
	In               Operator = "IN"
	NotIn            Operator = "NOT_IN"
)

The property filter operators. Composition is in condition.go: Datastore supports AND and OR.

type Option

type Option func(*config)

Option configures a Client.

func WithCredentials

func WithCredentials(creds google.Credentials) Option

WithCredentials supplies a service account key directly.

func WithDatabase

func WithDatabase(database string) Option

WithDatabase selects a named database. Empty means the project's default.

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint overrides the service endpoint, which is how a client is pointed at the emulator. A value with no scheme is taken as http, matching what DATASTORE_EMULATOR_HOST carries.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient supplies the HTTP client. Close leaves such a client alone, since its owner may still be using it.

func WithMaxIdleConns

func WithMaxIdleConns(n int) Option

WithMaxIdleConns sets how many idle connections are kept.

Every request goes to one host, so the per-host cap is the whole pool for this client. Set it to the concurrency the application runs.

func WithNamespace

func WithNamespace(namespace string) Option

WithNamespace sets the default namespace for keys that carry none.

func WithRetry

func WithRetry(attempts int, base time.Duration) Option

WithRetry configures the retry budget. Zero attempts disables retrying.

Retries multiply: the TinyGo transport replays a request once below this client when a pooled connection dies before any response byte arrives, so the worst case is attempts x 2 deliveries. The mutation verbs are idempotent by construction, so that is harmless for them; a commit carrying a transaction handle fails on the second delivery rather than writing twice.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds one request including reading the response body. The default is 10s, matching the DynamoDB client: these are small round trips.

func WithTokenSource

func WithTokenSource(ts google.TokenSource) Option

WithTokenSource supplies bearer tokens from somewhere else: a metadata server, a companion process, or a test.

A client built this way never signs anything, so on a TinyGo build it links no RSA code at all.

type PathElement

type PathElement struct {
	Kind string
	ID   int64
	Name string
}

PathElement is one step of a key path: a kind, plus either a numeric id or a string name. Neither set means an incomplete key, which the server completes on insert.

func (PathElement) Incomplete

func (p PathElement) Incomplete() bool

Incomplete reports whether the server still has to allocate an identifier.

type Query

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

Query selects entities of one kind. It is a value worth building once and running repeatedly, which is why it is a type rather than a pile of options.

Every method returns a new Query, so a partially built query can be shared without one caller's additions reaching another.

func NewQuery

func NewQuery(kind string) *Query

NewQuery starts a query over one kind.

func (*Query) Ancestor

func (q *Query) Ancestor(key Key) *Query

Ancestor restricts the query to descendants of key, which is a filter on the key path rather than on a property.

func (*Query) DistinctOn

func (q *Query) DistinctOn(properties ...string) *Query

DistinctOn collapses results that share the named properties.

func (*Query) End

func (q *Query) End(cursor Cursor) *Query

End stops at a cursor.

func (*Query) Filter

func (q *Query) Filter(property string, op Operator, value Value) *Query

Filter adds a property comparison. Repeated calls combine with AND.

It is sugar over Where(Prop(...)) and is kept because most filters are one comparison ANDed with the rest; reach for Where when a query needs OR.

func (*Query) KeysOnly

func (q *Query) KeysOnly() *Query

KeysOnly returns keys without properties, which is the cheapest read there is. It is a projection on the special __key__ property.

func (*Query) Limit

func (q *Query) Limit(n int32) *Query

Limit caps the batch size. Zero means the server decides.

func (*Query) Offset

func (q *Query) Offset(n int32) *Query

Offset skips results. The skipped entities are still read and still billed, so a cursor is the cheaper way to resume.

func (*Query) Order

func (q *Query) Order(property string) *Query

Order sorts ascending by property.

func (*Query) OrderDesc

func (q *Query) OrderDesc(property string) *Query

OrderDesc sorts descending by property.

func (*Query) Project

func (q *Query) Project(properties ...string) *Query

Project returns only the named properties. A projection query reads from an index, so every projected property must be indexed.

func (*Query) Start

func (q *Query) Start(cursor Cursor) *Query

Start resumes from a cursor returned by a previous batch.

func (*Query) Where added in v1.1.6

func (q *Query) Where(c Condition) *Query

Where adds a condition tree. Repeated calls, and a Where alongside a Filter, combine with AND — so Or belongs inside one call, not across two.

q.Where(datastore.Or(
    datastore.Prop("state", datastore.Equal, datastore.String("new")),
    datastore.Prop("priority", datastore.GreaterThanEqual, datastore.Int(8)),
))

type ReadOption

type ReadOption interface {
	// contains filtered or unexported methods
}

ReadOption configures a read.

func WithEventualConsistency

func WithEventualConsistency() ReadOption

WithEventualConsistency asks for a possibly stale read.

The default is strong, which on the Firestore backend applies to non-ancestor queries too. This exists for the cases where a stale answer is cheaper and good enough, not because staleness is ever the safer default.

func WithReadTime

func WithReadTime(at time.Time) ReadOption

WithReadTime reads the database as of a past instant.

Two windows, and which one you are in changes what a legal instant is:

  • Within the past hour, any microsecond-granularity instant, whether or not point-in-time recovery is enabled on the database.
  • From one hour to seven days back, whole-minute timestamps only, and only with point-in-time recovery enabled.

Neither window reaches before the database's earliestVersionTime, which on a young database can be later than both.

A read older than an hour must therefore be truncated by the caller:

at := time.Now().Add(-2 * time.Hour).Truncate(time.Minute)

Without that, the service refuses the read as "read_time is too old", which names the age when the precision is what was wrong. This does not truncate for you, because truncating would change the instant you asked for, and the boundary between the two windows moves while the request is in flight.

None of this is checked here. The client cannot see whether PITR is enabled or what earliestVersionTime is, so a local range check would refuse reads that work.

type Tx

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

Tx accumulates reads and mutations inside one transaction.

Mutations are queued and sent with the commit, so a closure that returns an error writes nothing and needs no rollback on the ordinary path.

The transaction starts lazily. handle is empty until a read or the commit starts it, which is what removes the separate beginTransaction round trip: the first read asks to start one and the reply carries the handle, and a closure that never reads folds its transaction into the commit instead. A Tx is used by one goroutine, the closure's, so this needs no lock.

func (*Tx) Avg added in v1.1.6

func (t *Tx) Avg(ctx context.Context, q *Query, property string) (Value, error)

Avg averages one property inside the transaction.

func (*Tx) CommitOverheadBytes added in v1.1.9

func (t *Tx) CommitOverheadBytes(n int) int

CommitOverheadBytes reports what this transaction's commit will spend on everything that is not the mutations, the same figure as Client.CommitOverheadBytes but for the request this Tx actually sends. Chunk against MaxTransactionBytes with it.

A transaction that has read carries the handle its first read brought back; a transaction that has not carries a singleUseTransaction block instead, and the two are different sizes. So the answer describes the transaction as it stands: asked before the first read it describes the single-use shape, and reading changes it, because reading is what changes the commit. Asked where a caller decides whether one more mutation fits, which is after whatever reads the closure does, it is exact.

func (*Tx) Count

func (t *Tx) Count(ctx context.Context, q *Query) (int64, error)

Count aggregates inside the transaction.

func (*Tx) Delete

func (t *Tx) Delete(k Key, opts ...WriteOption)

Delete queues a delete.

func (*Tx) Get

func (t *Tx) Get(ctx context.Context, key Key) (*Entity, error)

Get reads inside the transaction.

func (*Tx) GetMulti

func (t *Tx) GetMulti(ctx context.Context, keys []Key) (*LookupResult, error)

GetMulti reads several entities inside the transaction.

func (*Tx) Insert

func (t *Tx) Insert(e Entity, opts ...WriteOption)

Insert queues an insert.

func (*Tx) Mutate

func (t *Tx) Mutate(ms ...Mutation)

Mutate queues arbitrary mutations.

func (*Tx) Put

func (t *Tx) Put(e Entity, opts ...WriteOption)

Put queues an upsert.

func (*Tx) Run

func (t *Tx) Run(ctx context.Context, q *Query) (*Batch, error)

Run executes a query inside the transaction.

func (*Tx) Sum added in v1.1.6

func (t *Tx) Sum(ctx context.Context, q *Query, property string) (Value, error)

Sum totals one property inside the transaction.

func (*Tx) Update

func (t *Tx) Update(e Entity, opts ...WriteOption)

Update queues an update.

type TxOption

type TxOption func(*txConfig)

TxOption configures a transaction.

func WithTxRetries

func WithTxRetries(n int) TxOption

WithTxRetries caps how many times the closure re-runs on ABORTED. The default is 3.

type Value

type Value struct {
	Null      bool
	Bool      *bool
	Integer   *string
	Double    *float64
	Timestamp *time.Time
	Key       *Key
	String    *string
	Blob      []byte
	GeoPoint  *LatLng
	Entity    *Entity
	Array     []Value

	// ExcludeFromIndexes is not part of the union. It rides alongside whichever
	// member is set.
	ExcludeFromIndexes bool
}

Value is one property in its wire form. Exactly one member is set; a slice member counts as set when it is non-nil, so an empty array and an absent one are different values.

Integer is text because proto3 JSON encodes int64 as a string, which is also what keeps a 64-bit id from passing through float64 on the way in. Double is a real JSON number: Datastore stores the two as different types, and collapsing them would change sort order and equality filters.

func Array

func Array(vs ...Value) Value

Array builds a list value. An array of zero values is legal and is distinct from an absent property.

func Blob

func Blob(v []byte) Value

Blob builds a byte-string value, base64 on the wire.

func Bool

func Bool(v bool) Value

Bool builds a boolean value.

func Float

func Float(v float64) Value

Float builds a double value.

func GeoPoint

func GeoPoint(lat, lng float64) Value

GeoPoint builds a geographical point value.

func Int

func Int[T Integer](v T) Value

Int builds an integer value from any Go integer type.

func IntString

func IntString(v string) Value

IntString builds an integer value from its decimal text, for a uint64 beyond int64 or to avoid a conversion the caller does not want.

func KeyValue

func KeyValue(k Key) Value

KeyValue builds a value referring to another entity.

func Nested

func Nested(e Entity) Value

Nested builds a value holding an embedded entity.

An embedded entity has no key. One that carries a key is rejected at encode time rather than silently stripped.

func Null

func Null() Value

Null builds the null value, which is distinct from an absent property.

func String

func String(v string) Value

String builds a string value.

func Time

func Time(v time.Time) Value

Time builds a timestamp value.

Datastore stores microseconds, so a value with finer resolution loses it on the round trip. That truncation happens on the server and is not hidden here.

func Unindexed

func Unindexed(v Value) Value

Unindexed returns v with ExcludeFromIndexes set. It composes with every constructor, because indexing is not one of the union members.

func (Value) AsArray

func (v Value) AsArray() ([]Value, bool)

AsArray returns the list value.

func (Value) AsBool

func (v Value) AsBool() (bool, bool)

AsBool returns the boolean value.

func (Value) AsBytes

func (v Value) AsBytes() ([]byte, bool)

AsBytes returns the blob value.

func (Value) AsEntity

func (v Value) AsEntity() (Entity, bool)

AsEntity returns the embedded entity value.

func (Value) AsFloat

func (v Value) AsFloat() (float64, bool)

AsFloat returns the double value.

It does not accept an integer value. The two are distinct types to Datastore, and quietly widening one to the other is how a filter stops matching.

func (Value) AsGeoPoint

func (v Value) AsGeoPoint() (LatLng, bool)

AsGeoPoint returns the geographical point value.

func (Value) AsInt

func (v Value) AsInt() (int64, bool)

AsInt parses the integer value.

func (Value) AsKey

func (v Value) AsKey() (Key, bool)

AsKey returns the key value.

func (Value) AsNumber

func (v Value) AsNumber() (string, bool)

AsNumber returns the integer value as stored text, without a conversion.

func (Value) AsString

func (v Value) AsString() (string, bool)

AsString returns the string value.

func (Value) AsTime

func (v Value) AsTime() (time.Time, bool)

AsTime returns the timestamp value.

func (Value) IsNull

func (v Value) IsNull() bool

IsNull reports whether this is the null value, which is not the same as an absent property.

func (Value) Kind

func (v Value) Kind() Kind

Kind reports which member is set, or KindInvalid when zero or several are.

func (Value) MarshalJSON

func (v Value) MarshalJSON() ([]byte, error)

MarshalJSON emits exactly one union member, plus excludeFromIndexes when set.

func (*Value) UnmarshalJSON

func (v *Value) UnmarshalJSON(b []byte) error

UnmarshalJSON reads exactly one union member.

It decodes to a member map first rather than to a struct of pointers. A struct cannot see nullValue at all: encoding/json resolves a JSON null by setting the pointer field to nil, so the one member whose value is literally null becomes indistinguishable from an absent one. Counting keys also makes an unknown member an error instead of silently nothing.

type WriteOption

type WriteOption interface {
	// contains filtered or unexported methods
}

WriteOption configures a write.

func WithBaseVersion

func WithBaseVersion(version int64) WriteOption

WithBaseVersion applies the write only if the stored entity is still at this version, which comes from a previous read.

This is optimistic concurrency, and that is the honest name for it. It is strictly stronger than a client-side check, because it also catches a concurrent write the caller never read.

func WithUpdateTime

func WithUpdateTime(at string) WriteOption

WithUpdateTime is WithBaseVersion keyed on a timestamp instead, taken from Entity.UpdateTime.

Jump to

Keyboard shortcuts

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