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

electric

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package electric proxies live-sync shape requests.

A shape is a filtered view of one table that a client subscribes to and keeps up to date. The sync service serves it; this package stands in front, because the filter is not the client's to choose.

That is the whole design. Everything about which rows exist — the tenant, the soft-delete predicate, the snapshot predicate, and whatever the application adds — is decided here and sent as a parameterized filter. Everything about where the client is in the stream — its offset, its handle, its cursor — is forwarded untouched, because that genuinely is the client's business. A parameter that is neither is dropped rather than passed along, since a request that could set `table` could read any table there is.

The rest of the package is what happens when the sync service is not there: a Shape.Fallback answers from the application's own read in the protocol's own format, and a circuit stops asking a service that has stopped answering so that the requests behind an outage are not each paying to discover it.

Index

Examples

Constants

View Source
const DefaultBreakerCooldown = retryAfterSeconds * time.Second

DefaultBreakerCooldown is how long a proxy goes on not asking when Config.BreakerCooldown says nothing.

The same five seconds a subscriber holding a snapshot is asked to wait, so a service that comes back is found on the poll that was coming anyway.

View Source
const DefaultBreakerThreshold = 5

DefaultBreakerThreshold is how many failures in a row stop a proxy asking when Config.BreakerThreshold says nothing.

Five, because a shape route is not the only thing that fails: one refused connection is a restart or a redeploy, and answering the next subscriber from a fallback because of it would be trading live sync for nothing.

View Source
const DefaultInitialTimeout = 10 * time.Second

DefaultInitialTimeout is how long a shape read from the beginning waits for the sync service when Config.InitialTimeout says nothing.

View Source
const DefaultMaxSnapshotRows = 20_000

DefaultMaxSnapshotRows is how large a fallback snapshot may be when Config.MaxSnapshotRows says nothing.

View Source
const DefaultSnapshotTimeout = 5 * time.Second

DefaultSnapshotTimeout is how long one read of Config.DB may take when Config.SnapshotTimeout says nothing.

Shorter than DefaultInitialTimeout, because by the time it applies the subscriber has already spent that waiting for the sync service: the two are consecutive on the same request, not alternatives.

View Source
const PollDeadline = 5 * time.Minute

PollDeadline is how long Proxy.Serve gives itself to write a response, replacing the one deadline the server set for every route at once.

The other end of DefaultInitialTimeout: that one bounds the wait for an answer, this one bounds the sending of it. A shape route needs both because it is the one route in an application that is slow on purpose.

Five minutes is past every legitimate case rather than tuned to one, which is why it is a constant and not a Config field. Note what it does not bound: a sync service that hangs forever leaves this handler blocked in the upstream call, and no write deadline unblocks that — what ends such a poll is the client hanging up or Proxy.Drain. So being generous here costs one stalled reader's socket for as long as this, per request, never accumulating.

Not "no deadline at all", though the wait above is deliberately unbounded. A write blocked on a reader that stopped reading is unblocked by nothing else: cancelling the request's context does not abort a write already in flight. Finite and generous is the honest form of unbounded.

View Source
const Role = "electric"

Role is the Postgres role rig's electric/foundation set creates, and the one SetRolePassword will set a password on.

Stated here because two things read it and they are in different modules: the SQL that creates the role, and the caller that hands its connection string to SetRolePassword.

Variables

View Source
var ProtocolParams = []string{"offset", "handle", "live", "cursor", "replica"}

ProtocolParams are the sync protocol's own query parameters, forwarded as the client sent them. They say where in the stream a request resumes; none of them can widen what the stream contains.

Exported because the proxy is not the only thing that has to name them: a specification describing a shape route documents the same five, and a second copy of the list is a second thing to forget when the protocol grows a sixth.

Functions

func DateOnly

func DateOnly(v any) any

DateOnly renders a date column: the day, with no time and no zone, which is what Postgres prints and what the same column looks like over the stream.

It exists because a date and a timestamp are both a time.Time by the time they reach here, and Value cannot tell them apart. Something upstream can: the read Config.DB answers with asks the column's own type, and a hand-written Fallback knows which of its fields is which.

func RowKey

func RowKey(table string, key ...string) string

RowKey builds the key the sync service gives a row: the schema-qualified table, then the primary key, each part quoted.

The table may arrive qualified or not, the way Shape.Table may; an unqualified one is public, which is what the sync service resolves it to. More than one key part is a composite primary key, in the order the table declares them.

func SetRolePassword added in v0.3.0

func SetRolePassword(ctx context.Context, db DB, dsn, role string) (bool, error)

SetRolePassword gives role the password carried in dsn, and reports whether it did.

An empty dsn does nothing and returns false, which is every environment without a sync service — a laptop, and any deployment that has not grown one. Absent means absent rather than broken, the same discipline the proxy's own URL follows.

role is a parameter rather than Role read directly, and the check that dsn's username equals it is not fussiness. A connection string naming the database's master user would otherwise reset the master password, which is a way to lock a deployment out of its own database from a line that looks like configuration.

It is deliberately not an ALTER ROLE with the password in it. A managed Postgres commonly logs DDL and captures statement text for performance insight, and Postgres redacts a PASSWORD literal in neither — which is why psql has \password. So what goes over the wire and into those logs is a SCRAM-SHA-256 verifier, which the server stores verbatim when a PASSWORD literal already parses as one. Unlike an MD5 hash a SCRAM verifier is not password-equivalent: it holds StoredKey, which lets a server verify a client's proof but not produce one, so it cannot be replayed to log in. What is left is an offline attack on 4096 rounds of PBKDF2 against whatever generated the password.

No error returned from here carries the DSN, the password or the verifier.

There is no logger, because this package has none — see Config.OnError. The bool is what a caller logs off: true is worth one line at boot, false is worth silence.

func TimeOnly

func TimeOnly(v any) any

TimeOnly renders a time-of-day column, for the reason DateOnly exists — for a hand-written Fallback only, since Postgres prints a time in one form whatever the session says and the read Config.DB answers with takes it as it comes.

With the fraction, because a time column keeps microseconds and Postgres prints them: the sync service sends "10:06:07.5" for a column holding half a second past the minute, and time.TimeOnly would have sent "10:06:07". The layout trims trailing zeros, which is what Postgres does too, so a whole second is still "10:06:07".

func Value

func Value(v any) any

Value renders one column of a Row the way the sync service renders it.

Every value in a shape response is the text Postgres prints for it, or JSON null, and a subscriber decides how to read it from the column's type in Snapshot.Schema rather than from the JSON. So this returns a string or nil, never a number or a bool — a value that arrived typed would be decoded twice and disagree with the same column read over the stream.

It takes any because it is called on whatever a row's field happens to be, including a nil pointer, which is the null.

Two callers. A hand-written Fallback renders its rows with this, because Go values are what it has. The read Config.DB answers with takes most columns as the text Postgres prints and needs no renderer for them, and calls this for the four it decodes instead — the unexported binaryFormats says why those four are not read as text.

Types

type Config

type Config struct {
	// URL is the sync service, for example http://electric:3000.
	URL string

	// Client makes the upstream request.
	//
	// It must not have a Timeout: a live request is a long poll that
	// deliberately hangs until something changes, and a client timeout would
	// cut every subscription at the same interval. Cancellation comes from the
	// request's context instead, so a client that goes away takes its upstream
	// request with it.
	//
	// This is not the only clock a poll runs under, and it is the only one this
	// field decides. The server's own WriteTimeout would cut the answer on the
	// way out however patient this client was; [Proxy.Serve] replaces that one
	// per request with [PollDeadline].
	Client *http.Client

	// Extra are query parameters added to every upstream request, for the
	// credentials a hosted sync service needs.
	Extra url.Values

	// Headers are added to every upstream request, for the same reason.
	Headers http.Header

	// Secret authorises this proxy to the sync service.
	//
	// It is why the sync service can stay off the load balancer. Electric reads it from
	// the query string, so whatever holds it has to be something a browser is not —
	// which is this proxy, and is the arrangement Electric's own documentation asks
	// for. A client's own credential is separate and is never forwarded; see
	// [Proxy.Serve].
	//
	// Empty leaves it off the request entirely rather than sending an empty one, which
	// a secured sync service reads as a wrong one. That is the service `rig db up`
	// starts, which runs without a secret at all.
	//
	// A field of its own rather than an entry in [Config.Extra], and the reason is not
	// tidiness: this package has to know the value in order to take it back out of what
	// it reports. The credential is on the URL of every upstream request, and a
	// transport failure is an *url.Error, which renders that URL — so without this
	// field the secret reaches [Config.OnError] and then a log. Naming it here is what
	// lets every error this proxy reports have it removed.
	//
	// Setting both this and an Extra["secret"] is refused by [New]: two answers to one
	// question, one of which would be silently dropped.
	//
	// A secret stated only in Extra — which is where one had to go before this field
	// existed — is sent from there and redacted all the same. The redaction is not
	// what this field is for, and a project doing that is the one already logging its
	// credential, so it would be the wrong place to hold the line.
	Secret string

	// DB answers a shape when the sync service cannot be reached, by running the
	// shape's own filter against the database the sync service reads.
	//
	// Setting it gives every shape a fallback. Nil is the older behaviour: a
	// sync outage is a 502 and a subscriber with no rows.
	//
	// A [Shape] is a SELECT — a table, a projection, and a parameterized filter
	// — so there is nothing to write per shape and nothing to keep in step. That
	// is the point. A hand-written fallback is a read this package cannot see
	// inside, so a shape scoped to less than its table with a fallback that is
	// not shows a subscriber rows the subscription would have withheld; the read
	// built from [Shape.Where] is the same predicate the sync service was sent,
	// so it cannot diverge from it.
	//
	// Three things it is worth knowing:
	//
	// The cost lands all at once. Every subscriber falls back at the same
	// moment, because what they have in common is the sync service being gone —
	// so an outage is a read per subscriber per shape against the database the
	// sync service was shielding. [Config.MaxSnapshotRows] bounds each one and
	// [Config.SnapshotTimeout] bounds how long it may take.
	//
	// [Where.Raw] now reaches this database. It was always the one place in this
	// package where the caller is responsible for what it writes; until now what
	// it wrote was parsed by the sync service in a grammar of its own, and from
	// here it is concatenated into a statement sent to Postgres.
	//
	// Postgres accepts filters the sync service refuses, which inverts the usual
	// reassurance that a fallback is the narrower of the two. [Where.In] on a
	// column whose type is a Postgres enum is a 400 from the sync service — see
	// [Where.EqText], which exists for that reason — and runs here without
	// complaint. So a scope with that mistake in it fails loudly while the sync
	// service is up and answers with rows while it is down.
	DB DB

	// InitialTimeout bounds the wait on the one request that is not a long poll:
	// a subscriber reading a shape from the beginning.
	//
	// It exists so that a sync service which is running and not answering is
	// treated as unreachable rather than waited on, which is the outage a
	// [Shape.Fallback] is most useful for and the one a transport error does not
	// catch. A live poll's wait is deliberately left unbounded — see Client;
	// what bounds its answer on the way out is [PollDeadline].
	//
	// The wait only: once the answer has started arriving it is copied out
	// whole, up to [PollDeadline]. A large shape read over a slow connection is
	// a transfer rather than an outage, and half of one is worse than either.
	//
	// Required; [DefaultInitialTimeout] is what to write. A negative value is no
	// timeout, which is
	// what a project whose sync service legitimately takes a while to begin
	// answering should set.
	InitialTimeout time.Duration

	// MaxSnapshotRows bounds a fallback snapshot, and refuses past the bound
	// rather than sending part of one.
	//
	// A snapshot is built whole, in memory, per subscriber — and every subscriber
	// falls back at once, because what they have in common is the sync service
	// being gone. Truncating instead would be worse than refusing: a subscriber
	// cannot tell a short answer from a complete one, so a collection quietly
	// missing half its rows would look like a table that had lost them.
	//
	// Required; [DefaultMaxSnapshotRows] is what to write. A negative value is no
	// bound.
	MaxSnapshotRows int

	// SnapshotTimeout bounds one read of [Config.DB].
	//
	// It exists for the reason [Config.InitialTimeout] does, one layer down.
	// Every subscriber falls back at the same moment, so an outage is every one
	// of them queueing on the same pool — and without this, a database that has
	// become slow because of that queue is a request that waits rather than one
	// that gives up and lets the next through.
	//
	// Required; [DefaultSnapshotTimeout] is what to write. A negative value is no
	// timeout, and the
	// request's own context is then the only bound.
	SnapshotTimeout time.Duration

	// BreakerThreshold is how many failures in a row stop this proxy asking.
	//
	// An outage is one process being unreachable, and every request that goes on
	// asking it pays [InitialTimeout] to learn what the request before it already
	// learned — a held goroutine, a held connection, and a subscriber watching a
	// spinner for ten seconds before it is given the snapshot it could have had
	// at once. Past this many consecutive failures the proxy stops asking and
	// answers from here, until [BreakerCooldown] has passed and one request is
	// let through to find out whether the service is back.
	//
	// The trade is a shape whose sync service is fine and whose network is
	// briefly not: it is answered from a fallback, or refused, for as long as one
	// cooldown — where before it would have waited and then usually succeeded. In
	// a row rather than a rate, so a single failure among successes never counts.
	//
	// Required; [DefaultBreakerThreshold] is what to write. A negative value
	// never opens the
	// circuit, which is every request asking however long the outage lasts.
	BreakerThreshold int

	// BreakerCooldown is how long the proxy goes on not asking before it lets one
	// request through to test the sync service.
	//
	// One request, not all of them: the rest are answered from here while it
	// finds out. Nothing polls in the background, so a service that comes back is
	// noticed by the next subscriber through the door rather than a moment
	// earlier by a goroutine of this package's own.
	//
	// Required; [DefaultBreakerCooldown] is what to write. There is no negative
	// answer here to carry, so zero and below are refused alike.
	BreakerCooldown time.Duration

	// OnError is told about a failure that was answered rather than returned:
	// the sync service being unreachable, and a Fallback that refused. And one
	// that could not be answered at all, because the answer had already begun —
	// a response cut short on its way out.
	//
	// It exists because those are the failures a shape endpoint hides. The
	// proxy writes a status and a short line, and without this the cause reaches
	// nobody — there is no logger in this package, for the reason there is none
	// anywhere else in runtime.
	//
	// A request the circuit refused to make is not one of them: it attempted
	// nothing, so it learned nothing worth a second line. What is worth a line is
	// the circuit opening, and that is [OnSyncState].
	//
	// Every error names the [Shape.Table] it came from. This takes a context and
	// an error and nothing else, so a handler cannot add the route afterwards —
	// and without it an application serving four shapes gets four identical lines
	// during one outage.
	OnError func(context.Context, error)

	// OnSyncState is told when the answer to whether the sync service is there
	// changes: false when the circuit opens, true when a request through it
	// succeeds.
	//
	// Twice per outage rather than once per request, which is what makes it the
	// thing to alert on. [Proxy.SyncReachable] is the same answer for a health
	// endpoint or a banner, asked rather than pushed.
	OnSyncState func(ctx context.Context, reachable bool)
}

Config builds a proxy.

func (Config) Defaults added in v0.3.0

func (c Config) Defaults() Config

Defaults fills in the five values New will not invent, from the Default constants this package documents as the answers to write, and returns the Config.

New refusing them is deliberate and stands: each governs what a subscriber sees while the sync service is away, and a value this package chose for a caller who said nothing would be a value nobody chose, found the first time a sync service goes away. Calling this is choosing them — explicitly, in one line, at the call site, where a reader can see that the answer is "whatever rig recommends" rather than a considered number.

So it is not a default. It is the documented answer, applied on request:

proxy, err := electric.New(electric.Config{
	URL: cfg.ElectricURL,
	DB:  pool,
}.Defaults())

Anything already set is left alone, including a negative — each of the five documents what its negative means, and this is not the place that decides one was a mistake. So a caller can take the recommendations and override one:

electric.Config{URL: u, MaxSnapshotRows: 200}.Defaults()

type DB

type DB = dbx.Conn

DB is the database a shape is read from when the sync service cannot be reached: the same one the sync service reads.

dbx.Conn and not dbx.Pool, because nothing here opens a transaction. One statement answers a shape, which is the whole reason this works — a Shape is a SELECT.

type Fallback

type Fallback func(ctx context.Context) (Snapshot, error)

Fallback answers a shape from somewhere other than the sync service.

It is called when the sync service cannot be reached and a subscriber is asking for the shape from the beginning — never mid-subscription, and never on a live poll. What it returns is sent in the sync protocol's own format, so a client that already speaks the protocol needs to know nothing about it.

Once per such request, so a snapshot is as fresh as the request that asked for it: what does not update is one subscriber's copy of the rows, not the rows. An outage takes the sync service and not the API, so writes during one commit and the next read from the beginning has them — which is why a reload sees a change that the tab it was made in does not. A front end that renders a write optimistically and waits for the stream to confirm it is waiting for something that cannot arrive until the sync service is back.

It is the escape hatch rather than the usual way. Config.DB answers a shape from the shape's own filter, which is the same predicate the sync service was sent and therefore cannot narrow differently; this is for a shape that is not one table's rows — one answered from a materialized view, a cache, or a second database — and it wins where both are set.

A shape with neither answers a sync outage the way this package always did: 502, and a subscriber with no rows.

type Proxy

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

Proxy forwards shape requests.

func New

func New(cfg Config) (*Proxy, error)

New builds a proxy.

func (*Proxy) Drain

func (p *Proxy) Drain(ctx context.Context) error

Drain ends the polls this proxy is holding open and stops it starting more.

Register it as the first step of a shutdown, which is what the step is for:

app.DrainWithin("shapes", 5*time.Second, proxy.Drain)

A live subscription is a request that is deliberately not answering yet. It is still an in-flight request, so http.Server.Shutdown waits for it — and waits, because nothing in the poll is late and Shutdown does not cancel a request's context. One open browser tab is then a shutdown that spends its budget waiting for the sync service to have news, and a sync service that hangs rather than refuses is a shutdown that spends all of it. What goes without in either case is everything registered after: the flush, the sweep, and the pass of the notification engine whose claims are only handed back if its close step runs.

So they are ended from this side instead, at the point in the sequence where readiness is already false and there is nothing to be gained from holding a subscription open. A subscriber gets a 503 with a Retry-After and resumes from the same offset against whichever replica is still serving; nothing is lost, because a poll that had not answered had nothing in it yet.

It returns when every poll has gone, or when ctx says to stop waiting. Calling it twice is safe, and a proxy that never served anything returns at once.

func (*Proxy) Health added in v0.2.0

func (p *Proxy) Health(ctx context.Context) error

Health asks the sync service whether it is serving.

One request to its own health endpoint, bounded by ctx and nothing else — the way everything in this package is bounded by the request that caused it. A nil error means the service is there and has finished connecting to its database; any other means it is not, and says which of those two it failed at.

Answering HTTP is deliberately not the whole test. The sync service comes up before it has connected to Postgres and reports its own state in the body, and a shape request sent during that gap hangs rather than fails — which is the worst of the three answers, because a startup check that hangs is indistinguishable from a startup that is merely slow. So a 200 whose body does not say active is a failure here.

It does not touch the circuit, in either direction. This is a probe somebody asked for rather than a subscription that failed: a monitoring page polling every few seconds must not be what stops this proxy serving live shapes, and a probe that succeeded must not put a subscriber back on a service it has not tried itself. Proxy.SyncReachable is the other question — what the requests that did happen found — and the two are worth having separately.

func (*Proxy) Serve

func (p *Proxy) Serve(w http.ResponseWriter, r *http.Request, s Shape)

Serve forwards one shape request and streams the answer back.

It writes the response, including on failure. A caller that wants its own error rendering should check the shape before calling.

When the sync service cannot be reached and the shape has a Fallback, a subscriber reading from the beginning is answered from there instead. Nothing about the request says which of the two it got, and nothing has to: the answer is in the same format either way.

func (*Proxy) SyncReachable

func (p *Proxy) SyncReachable() bool

SyncReachable reports whether the sync service is answering.

It is the circuit's state and not a probe: false once enough requests in a row have failed that this proxy stopped asking, true again as soon as one has succeeded. So it costs nothing to ask, and a health endpoint or a banner can ask it per request.

True is also what it says when the circuit has been turned off with a negative Config.BreakerThreshold, because then nothing is counting.

type Row

type Row struct {
	// Key identifies the row within the shape. [RowKey] builds one.
	Key string

	// Value is the row: the shape's columns, each in the text form Postgres
	// prints, or nil for NULL. A column the shape does not carry does not belong
	// here — the projection is the same promise on this path as on the other one.
	Value map[string]any
}

Row is one row of a snapshot.

type Shape

type Shape struct {
	// Table is the table to stream, optionally schema-qualified.
	Table string
	// Where is the filter, with $1-style placeholders bound by Params.
	Where string
	// Params are the placeholder values.
	Params []string
	// Columns narrows the projection. Empty streams every column.
	//
	// It is worth setting: a shape carries every column it names to every
	// subscriber, forever, and a password hash in a live-sync stream is a
	// password hash on somebody's laptop.
	Columns []string

	// Key are the table's primary key columns, in the order the table declares
	// them.
	//
	// They identify a row within the shape — see [RowKey] — and are what
	// [Config.DB] needs to answer this shape without being told anything else
	// about it. A key column outside Columns is still read, and still left out
	// of the row: the projection is the promise on that path too.
	//
	// Required for that read, and refused rather than defaulted where it is
	// missing: a snapshot whose rows are all named after the table is one row as
	// far as a subscriber is concerned. A generated shape always carries it.
	Key []string

	// Schema is the electric-schema document for Columns: a JSON object mapping
	// each column to its Postgres type. See [Snapshot.Schema], which is where it
	// ends up.
	//
	// It describes Columns, so a shape that narrows the projection and does not
	// narrow this describes a different set than it sends.
	Schema string

	// NoFallback answers a sync outage with a 502 even where this proxy could
	// have answered from the database.
	//
	// It is for a shape whose value is its freshness rather than its rows. A
	// snapshot of who is looking at this page, that then stops updating, is
	// worth less than an empty list — so presence sets this, and it is a
	// decision rather than an omission.
	NoFallback bool

	// Fallback answers this shape from somewhere other than the database this
	// package would otherwise read.
	//
	// It is the escape hatch, and it wins: a default that overrode it would not
	// be one. Setting it is for a shape that is not a single table's rows — one
	// answered from a materialised view, a cache, or a second database.
	//
	// Whatever narrows Where has to narrow this too. Where is a filter this
	// package sends and can therefore promise; a Fallback is a read it cannot
	// see inside, so a shape scoped to less than its table with a fallback that
	// is not is a subscriber shown rows the subscription would have withheld.
	// The read built from Where has no such gap, which is why it is the default.
	Fallback Fallback
	// contains filtered or unexported fields
}

Shape is what to subscribe to.

type Snapshot

type Snapshot struct {
	// Rows are the rows the shape's filter admits. Order does not matter.
	Rows []Row

	// Schema is the electric-schema header: a JSON object mapping each column to
	// its Postgres type, which is how a client picks a parser per column.
	//
	// Sending it is not optional in practice. A subscriber that does not get it
	// leaves every value as the string it arrived as, so a timestamp stays text
	// and a count stays text — decoded differently from the same column read
	// over the API, which is the one thing a generated client exists to prevent.
	Schema string
}

Snapshot is a shape's rows at one moment, and enough type information for a client to decode them.

type Where

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

Where builds a shape's filter.

Conditions are added through methods rather than written as text, and every value becomes a numbered parameter the sync service binds. That is not politeness: a shape's filter is assembled from a tenant identifier and whatever an application's scoping function adds, and the moment either is interpolated into SQL, a live-sync endpoint becomes an injection point with a streaming response attached.

Example

A shape's filter is assembled here and sent to the sync service as a parameterized WHERE. The zero value is ready to use, which is what a generated scoping function returns before adding the application's own conditions to it.

Column names are quoted and values are numbered parameters — never interpolated. A shape's filter is built from a tenant identifier and whatever an application's scoping function adds, and interpolating either would make a live-sync endpoint an injection point with a streaming response attached.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/electric"
)

func main() {
	w := &electric.Where{}
	w.Eq("tenant_id", "018f2c9e-4a1b-7c3d-9e5f-000000000001")
	w.IsNull("deleted_at")
	w.In("status", "draft", "review")

	fmt.Println(w.SQL())
	fmt.Println(w.Params())

}
Output:
"tenant_id" = $1 AND "deleted_at" IS NULL AND "status" IN ($2, $3)
[018f2c9e-4a1b-7c3d-9e5f-000000000001 draft review]

func (*Where) Bind

func (w *Where) Bind(value string) string

Bind adds a parameter without a condition and returns its placeholder, for a Where.Raw that needs one.

func (*Where) Empty

func (w *Where) Empty() bool

Empty reports whether nothing has been added.

func (*Where) Eq

func (w *Where) Eq(column string, value string) *Where

Eq adds `column = $n`.

func (*Where) EqText

func (w *Where) EqText(column string, value string) *Where

EqText adds `column::text = $n`, for a column whose Postgres type is an enum.

The cast is not decoration. The sync service types a filter's parameters from the column they compare against, and a user-defined enum is a type its parser does not accept a value for — `version_type = 'Original'` is refused whether the value is bound or literal. Comparing the text form is the same filter, and text is a type every value has.

func (*Where) Gt

func (w *Where) Gt(column, value string) *Where

Gt adds `column > $n`.

func (*Where) Gte

func (w *Where) Gte(column, value string) *Where

Gte adds `column >= $n`.

func (*Where) In

func (w *Where) In(column string, values ...string) *Where

In adds `column IN ($a, $b, ...)`.

An empty set adds `false`, because "in nothing" matches nothing — and omitting the condition instead would widen the shape to everything, which is the opposite of what the caller asked for.

Example (Empty)

An empty set adds `false`, because "in nothing" matches nothing. Omitting the condition instead would widen the shape to everything — the opposite of what the caller asked for, on an endpoint that streams.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/electric"
)

func main() {
	w := &electric.Where{}
	w.Eq("tenant_id", "t-1")
	w.In("status")

	fmt.Println(w.SQL())
	fmt.Println(w.Params())

}
Output:
"tenant_id" = $1 AND (false)
[t-1]

func (*Where) IsNull

func (w *Where) IsNull(column string) *Where

IsNull adds `column IS NULL`.

func (*Where) Lt

func (w *Where) Lt(column, value string) *Where

Lt adds `column < $n`.

func (*Where) Lte

func (w *Where) Lte(column, value string) *Where

Lte adds `column <= $n`.

func (*Where) NotEq

func (w *Where) NotEq(column string, value string) *Where

NotEq adds `column <> $n`.

func (*Where) NotNull

func (w *Where) NotNull(column string) *Where

NotNull adds `column IS NOT NULL`.

func (*Where) Params

func (w *Where) Params() []string

Params are the bound values, in placeholder order.

func (*Where) Raw

func (w *Where) Raw(condition string) *Where

Raw adds a condition verbatim.

It is the escape hatch for something the methods cannot express, and the one place in this package where the caller is responsible for what reaches the database. Never build the string from a request; use the other methods, which bind their values.

Two databases, now. What is written here is parsed by the sync service in a grammar of its own, and — where Config.DB is set — concatenated into a statement this package sends to Postgres. The second is the stricter test of the sentence above.

func (*Where) SQL

func (w *Where) SQL() string

SQL renders the conditions, joined with AND.

Directories

Path Synopsis
Package foundation carries the Postgres role the sync service connects as.
Package foundation carries the Postgres role the sync service connects as.

Jump to

Keyboard shortcuts

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