keel

package module
v0.4.5 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 35 Imported by: 0

README

keel

ci

A typed, multi-tenant data layer for Go and PostgreSQL.

You write plain structs. keel gen writes a typed package for them. Every query, filter, relation load and write goes through that package, so the compiler checks your columns and values, and every statement is scoped to the caller's tenant before it reaches Postgres.

s := fleetdb.Shipments
m := s.Moves()

page, err := s.
    Where(
        s.Status.In(fleet.StatusNew, fleet.StatusAssigned),
        keel.When(search != "", s.Customer().Name.ContainsFold(search)),
        m.Any(m.Status.Eq(fleet.StatusInTransit)),
    ).
    Load(s.Customer(), m.Order(m.Sequence.Asc()), m.Stops().Location()).
    Order(s.CreatedAt.Desc()).
    Page(ctx, db, keel.After(cursor, 50).Total(10_000))

Outside a transaction that is one round trip: the page, its total, the joined customer and every level of moves, stops and locations, all filtered to the tenant on the context.

Why "keel"? A keel is the beam along the bottom of a ship's hull. Everything else is built on it, and it is what keeps the ship upright and stops it drifting sideways. keel does that job under an application's data: it is the part every query rests on, and it keeps each tenant's work from drifting into another's, so the application stays on an even keel as it grows.

Why keel

  • Typed end to end. Columns, relations, values, assignments and results are checked by the compiler. s.Status.In(...) accepts only fleet.Status values; a misspelled column does not compile. Raw SQL exists, but it is explicit (keel.Raw) and greppable.
  • Tenancy by construction. The tenant comes from the context and is applied to every statement on a tenant table: reads, joins, relation loads, subqueries, updates, deletes and inserts. A statement without a scope fails. Crossing tenants takes a system scope with a reason and an explicit AllTenants().
  • Fewer round trips. Transactions begin with their first statement. A lone statement that needs a transaction (a row-level security set_config) is one pipelined BEGIN; ...; COMMIT. A page and its total share a round trip, so do the independent reads of a keel.Batch, and a relation tree loads inside the read's own round trip wherever the read sees one snapshot.
  • Prepared statements, always. Values are bind parameters, so a statement's text depends only on its shape and Postgres skips parsing and, usually, planning. In binds one array: 3 IDs and 3,000 IDs are the same statement.
  • Built for many tenants on one database. Per-tenant concurrency limits, query budgets that catch N+1 loops, a result cache invalidated by data versions, read replicas with lag tracking, and reports of slow waits that name who is holding what.
  • Bounded by default. List needs a limit, pagination is keyset-based, bulk inserts send one array per column, and every wait for a slot or connection can be capped.
  • No reflection on the hot path. Generated accessors decode rows straight from pgx's buffers into struct fields.

keel is Postgres-only and uses pgx v5, never database/sql.

Is keel right for you?

keel is not a general-purpose ORM. It is the data layer of one kind of application, built with strong opinions, and it carries machinery many projects will never want. Read this before the quick start.

What it is for

A backend that serves many organizations from one PostgreSQL database: every tenant table carries tenant columns, the same code serves every tenant, and the data is relational enough that most screens read a record with several levels of related rows. Think of a transportation management system (keel is built alongside Trenova), a CRM, a helpdesk, an ERP or a marketplace back office: B2B SaaS with a large schema, deep detail pages, long lists, bulk imports and reports.

What it does very well
  • Keeping tenants apart. You cannot forget the tenant filter: keel adds it to every read, join, relation load, subquery and write, and fails a statement that has no scope. Work across tenants is written down, with a reason:

    ctx := withSystem(ctx, "purge canceled shipments") // your Config.System reads this
    n, err := s.AllTenants().
        Where(s.Status.Eq(fleet.StatusCanceled), s.CanceledAt.Lt(cutoff)).
        Delete(ctx, db)
    
  • Detail pages and lists in one round trip. A shipment with its customer, moves, stops and locations, or a page of fifty with a total count, is one exchange with Postgres outside a transaction. The saving grows with the distance to the database.

  • Dashboards. Independent reads of a screen go in one round trip with keel.Batch, and reads that ask for the result cache come back from memory until a table they read changes:

    err = keel.Batch(ctx, db,
        keel.ListInto(s.Where(s.Hazmat.IsTrue()).Order(s.CreatedAt.Desc()).Limit(50).Cache(), &recent),
        keel.CountInto(s.Where(s.Status.Eq(fleet.StatusNew)).Cache(), &unassigned),
        keel.OneInto(c.Where(c.Code.Eq("ACME")).Cache(), &cust),
    )
    
  • Imports and bulk changes. CreateMany sends one array per column, Copy streams binary COPY with IDs, tenant columns and hooks applied, and upserts and bulk updates are one statement each.

  • Staying up when one tenant misbehaves. Per-tenant concurrency limits, query budgets that fail an N+1 loop, a ceiling on waits, and slow-wait reports that name the code holding the slots.

  • Reports users build themselves. keel/analytics turns a report definition (columns across relations, filters, grouping, pivots, formulas) into tenant-scoped SQL, with exports and rollups.

  • Living with a large schema. Typed columns and relations, labels and descriptions derived from the models, keel.Verify to catch drift between models and migrations, golden SQL snapshots and index assertions in tests.

When to choose something else
  • You need another database, or database portability. keel is PostgreSQL only and leans on it: arrays, unnest, COPY, pipelining, LISTEN/NOTIFY, advisory locks, row-level security.
  • Your application is single-tenant and small. keel works without tenant columns, but you would pay for code generation, a large API and opinionated runtime machinery to get a query builder. sqlc or plain pgx will serve you better.
  • You want to write SQL first. keel builds SQL from typed Go. If you would rather keep queries in .sql files and generate Go from them, use sqlc.
  • You want the schema generated from your models. keel never writes migrations from structs; the schema lives in SQL migrations you write. GORM's AutoMigrate and ent's schema migrations do the opposite.
  • You expect classic ORM behaviour. There is no lazy loading, no identity map, no unit of work, no built-in soft delete and no polymorphic associations. Every load is asked for explicitly (an interceptor can add a soft-delete filter if you want one).
  • You need database/sql. keel runs on pgx v5 and nothing else, so libraries that want a *sql.DB cannot share its connections.
  • You want a small dependency graph. keel is one module with its CLI, migrations, seeds and analytics. Go compiles only the packages you import, but go.mod lists their dependencies (Charm's terminal libraries, excelize, Arrow) all the same.
  • You need a stable API today. keel is pre-1.0 and shaped by one application's needs; minor versions can still change the API.

docs/design.md has the full design and its non-goals.

What's in the box

Package What it does
keel queries, relation loads, pagination, writes, transactions, tenancy, hooks, the result cache
cmd/keel, gen keel gen: the typed package for each model package, and -check for CI
migrate SQL and Go migrations under an advisory lock, with lock-timeout retries, squash and templates for tests
seed reference data and fixtures as declared seeds that re-run when their inputs change
keelcli the command line an application's database binary is made of: up, migrate, seed, a live console
keeltest golden snapshots of rendered SQL and assertions that an index serves a query
keellog a readable statement log for development, with values, caller and outcome
cache/rediscache the shared tier of the result cache on Redis, Valkey, KeyDB or Dragonfly
vector, postgis pgvector distances and HNSW settings; PostGIS points, geometries and spatial SQL
numeric shopspring/decimal values read and bound in binary, 2 to 3 times as fast as through their text
analytics a semantic layer for user-built reports: definitions, formulas, a compiler to tenant-scoped SQL, exports, rollups

Install

go get github.com/emoss08/keel

keel needs Go 1.26 and PostgreSQL (tested on 18).

Quick start

The examples use the models of keel's own tests, in internal/testmodels/fleet. The snippets in this README and in docs/guide.md have compiled counterparts in example_test.go, which keeps them in step with the API.

1. Write a model

A model embeds keel.Table and tags every stored field with keel. Abridged from fleet.go:

package fleet

// Customer is a tenant's customer.
type Customer struct {
    keel.Table `keel:"customers,report=Sales"`

    ID             ids.ID `keel:"id,pk,autoid:cus_"`
    OrganizationID ids.ID `keel:"organization_id,pk,tenant"`
    BusinessUnitID ids.ID `keel:"business_unit_id,pk,tenant"`
    // Name is the customer's trading name.
    Name      string `keel:"name"`
    Code      string `keel:"code"`
    Version   int64  `keel:"version,version"`
    CreatedAt int64  `keel:"created_at,autocreate"`
    UpdatedAt int64  `keel:"updated_at,autoupdate"`
}

// Shipment is a load moved for a customer.
type Shipment struct {
    keel.Table `keel:"shipments,report=Operations"`

    ID             ids.ID `keel:"id,pk,autoid:shp_"`
    OrganizationID ids.ID `keel:"organization_id,pk,tenant"`
    BusinessUnitID ids.ID `keel:"business_unit_id,pk,tenant"`
    CustomerID     ids.ID `keel:"customer_id"`
    // ProNumber is the carrier's progressive number, unique per tenant.
    ProNumber string `keel:"pro_number"`
    Status    Status `keel:"status"`
    Weight    *int64 `keel:"weight,format=weight"`
    Version   int64  `keel:"version,version"`
    CreatedAt int64  `keel:"created_at,autocreate"`
    UpdatedAt int64  `keel:"updated_at,autoupdate"`

    Customer   *Customer `keel:"belongs:customer_id"`
    Moves      []*Move   `keel:"many:shipment_id"`
    OriginStop *Stop     `keel:"pick:moves.stops,first=moves.sequence|stops.sequence"`
}

autoid fills the key from your ID generator, tenant columns come from the scope, autocreate and autoupdate are stamped, and version gives Save optimistic locking. OriginStop picks the first stop of the first move, and keel loads and filters it like any to-one relation. The schema itself lives in SQL migrations. Every tag option is checked by keel gen, which also derives labels (CustomerID → "Customer ID", ProNumber → "PRO Number" with -acronyms PRO), descriptions from doc comments, Status's enum values and, for models tagged report, how reports present each column; see the tag reference.

2. Generate
go run github.com/emoss08/keel/cmd/keel gen ./internal/...

For each package with models, this writes a package next to it (fleet → fleet/fleetdb) with a handle per table (fleetdb.Shipments), a typed field per column (fleetdb.Shipments.Status), a method per relation (fleetdb.Shipments.Customer()) and Get by primary key. Run it with -check in CI.

3. Open a database
type tenantCtxKey struct{}

db, err := keel.Open(ctx, keel.Config{
    DSN:           os.Getenv("DATABASE_URL"),
    TenantColumns: []string{"organization_id", "business_unit_id"},
    Tenant: func(ctx context.Context) ([]any, bool) {
        t, ok := ctx.Value(tenantCtxKey{}).([2]ids.ID)
        if !ok {
            return nil, false
        }
        return []any{t[0], t[1]}, true
    },
    NewID: newID, // your generator: func(prefix string) string
})
if err != nil {
    return err
}
defer db.Close()

ctx = context.WithValue(ctx, tenantCtxKey{}, [2]ids.ID{"org_acme", "bu_dallas"})
4. Create, query, update
c, s := fleetdb.Customers, fleetdb.Shipments

cust := &fleet.Customer{Code: "ACME", Name: "Acme Freight"}
if err := c.Create(ctx, db, cust); err != nil { // ID, tenant columns and timestamps filled
    return err
}
sh := &fleet.Shipment{CustomerID: cust.ID, ProNumber: "PRO-1001", Status: fleet.StatusNew}
if err := s.Create(ctx, db, sh); err != nil {
    return err
}

open, err := s.
    Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned), s.Customer().Code.Eq("ACME")).
    Load(s.Customer()).
    Order(s.CreatedAt.Desc()).
    Limit(50).
    List(ctx, db)

sh.Status = fleet.StatusAssigned
if err := s.Save(ctx, db, sh); errors.Is(err, keel.ErrConflict) {
    // someone else saved this shipment first
}
5. Use a transaction

The transaction travels on the context; every call made with that context joins it.

err = keel.Write(ctx, db, func(ctx context.Context) error {
    mv := &fleet.Move{ShipmentID: sh.ID, Sequence: 1, Status: fleet.StatusAssigned}
    if err := fleetdb.Moves.Create(ctx, db, mv); err != nil {
        return err
    }
    _, err := s.Where(s.ID.Eq(sh.ID)).Update(ctx, db, s.Status.To(fleet.StatusInTransit))
    return err
})

Feature tour

The tour uses the full models in fleet.go, with s := fleetdb.Shipments and c := fleetdb.Customers.

Filters

Each column has the operators of its type; keel.When makes a filter optional.

rows, err := s.Where(
    keel.When(f.Search != "", s.ProNumber.ContainsFold(f.Search)),
    keel.When(len(f.Statuses) > 0, s.Status.In(f.Statuses...)),
    s.Weight.Between(10_000, 40_000),
    s.Labels.Has("expedite"),
    s.Attributes.Text("trailer", "type").Eq("reefer"),
    s.SearchVector.Match("dallas reefer"),
    keel.Or(s.BillToID.IsNull(), s.BillTo().Code.Ne("ACME")),
).Order(s.CreatedAt.Desc()).Limit(50).List(ctx, db)
Relations and loads

To-one paths join; to-many paths filter with Any and None and load one statement per level, with optional per-parent limits, all in the read's round trip: each level selects its parents' keys with a subquery over the parent level's statement.

m := s.Moves()
cu := s.Customer()

rows, err := s.
    Where(m.Any(m.Status.Eq(fleet.StatusInTransit), m.Loaded.IsTrue())).
    Load(
        cu.Pick(cu.Name, cu.Code),
        m.Where(m.Status.Ne(fleet.StatusCanceled)).Order(m.Sequence.Asc()).Load(m.Stops().Location()),
        s.Tags(),
    ).
    Limit(50).
    List(ctx, db)

latest, err := s.Load(m.Order(m.Sequence.Desc()).Limit(1)).Limit(50).List(ctx, db) // last move each
Pagination

Keyset pages with an opaque cursor and an optional total in the same round trip, exact up to a limit and the planner's estimate above it with Estimate(); Each walks a large set at constant memory.

page, err := s.Where(s.Hazmat.IsTrue()).Order(s.CreatedAt.Desc()).
    Page(ctx, db, keel.After(cursor, 50).Total(10_000).Estimate())
// page.Items, page.Next ("" on the last page), page.Total, page.TotalExact

err = s.Where(s.Hazmat.IsTrue()).Each(ctx, db, 500, func(ctx context.Context, batch []*fleet.Shipment) error {
    return nil
})
Batching reads

keel.Batch sends independent reads in one round trip and fills typed destinations only when every read succeeded.

var rows []*fleet.Shipment
var total int64
var cust *fleet.Customer
err = keel.Batch(ctx, db,
    keel.ListInto(s.Where(s.Hazmat.IsTrue()).Order(s.CreatedAt.Desc()).Limit(50), &rows),
    keel.CountInto(s.Where(s.Hazmat.IsTrue()), &total),
    keel.OneInto(c.Where(c.Code.Eq("ACME")), &cust),
)
Projections and aggregates
type statusTotal struct {
    Status  fleet.Status
    Loads   int64
    Hazmat  int64
    Revenue fleet.Money
}
totals, err := keel.Into[statusTotal](ctx, db, s.
    Group(s.Status).
    Pick(s.Status, keel.Count(), keel.Count().Filter(s.Hazmat.IsTrue()), s.Total.Sum()).
    Having(keel.Count().Gt(10)))

Window functions (keel.RowNumber().Over(...)), DistinctOn, joins and self-joins with As, lateral joins and correlated subqueries are covered in the guide.

CTEs and set operations
busy := keel.CTE("busy", []string{"customer_id", "loads"},
    s.Group(s.CustomerID).Pick(s.CustomerID, keel.Count()).Where(s.Status.Eq(fleet.StatusInTransit)))

customers, err := c.With(busy).
    Join(busy, busy.Col("customer_id").EqCol(c.ID)).
    Where(busy.Col("loads").Gte(10)).
    All(ctx, db)

codes, err := keel.Into[string](ctx, db, keel.UnionAll(
    s.Pick(s.ProNumber).Where(s.Hazmat.IsTrue()),
    c.Pick(c.Code).Where(c.Email.IsNull()),
))

keel.RecursiveCTE walks hierarchies; keel.Union, Intersect and Except combine queries.

Upserts and bulk writes
err = fleetdb.Stops.CreateMany(ctx, db, stops) // one array per column through unnest, one round trip

n, err := s.Copy(ctx, db, imported) // binary COPY for large loads; IDs, tenant columns and hooks as in Create

inserted, err := c.Upsert(ctx, db, rows,
    keel.OnConflict(c.OrganizationID, c.BusinessUnitID, c.Code).Update(c.Name))

inserted, err = c.Upsert(ctx, db, rows, keel.OnConflict(c.OrganizationID, c.BusinessUnitID, c.Code).Set(
    c.Name.ToExpr(c.Name.Excluded()),                                         // the proposed name
    c.Email.ToExpr(keel.Raw("coalesce(?, ?)", c.Email, c.Email.Excluded())), // keep a stored email
))

n, err = c.CreateOrSkip(ctx, db, rows, c.OrganizationID, c.BusinessUnitID, c.Code)

n, err = s.Where(s.Status.Eq(fleet.StatusNew), s.Customer().Code.Eq("ACME")).
    Update(ctx, db, s.Status.To(fleet.StatusCanceled), s.Priority.Increment(1))
Hooks

Model hooks are methods on the model (BeforeSave, AfterLoad, …). Table hooks run before a write, after it inside the transaction, or after commit, and can ask for the previous values of columns:

keel.OnUpdate(db, s, keel.AfterWrite, func(ctx context.Context, m *keel.Mutation[*fleet.Shipment]) error {
    if m.Old == nil || !m.Changed(s.Status) || m.Old.Status == m.Row.Status {
        return nil
    }
    log.Printf("shipment %s: %s -> %s", m.Row.ID, m.Old.Status, m.Row.Status)
    return nil
}, keel.Needs(s.Status))

Interceptors (db.Intercept) add predicates to the reads, bulk writes, Save and DeleteRow of a table (never inserts), so a soft-delete or ownership filter holds for single-row writes too. Observers (Config.Observers) see every statement; a span started in BeforeQuery travels on the statement's context to its execution and to AfterQuery.

Result cache

A read that asks for the cache is served from memory, with no round trip, until a table it read changes for the tenant. Validity never depends on the TTL: writes bump a per-tenant, per-table data version, pushed to every process with LISTEN/NOTIFY. A shared tier on Redis lets processes share entries.

cfg.DataVersions = keel.DataVersionsConfig{Enabled: true}
cfg.Cache = &keel.CacheConfig{Local: keel.MemoryCache(256 << 20)}

open, err := s.Where(s.Status.Eq(fleet.StatusNew)).
    Load(s.Customer(), s.Moves()).
    Order(s.CreatedAt.Desc()).Limit(50).
    Cache(keel.TTL(5 * time.Minute)).
    List(ctx, db)

A hit costs about 20 µs, against about 390 µs for the same read from the database on loopback. Concurrent misses of one entry wait for a single read.

Running many tenants on one database
cfg.TenantConcurrency = 8                // at most 8 terminals or transactions per tenant at once
cfg.MaxWait = time.Minute                // no wait for a slot or connection outlasts this
cfg.SlowWaits = keel.SlowWaitConfig{     // report waits past 5s, with who holds the slots
    After:  5 * time.Second,
    Report: func(ctx context.Context, w *keel.SlowWait) { log.Printf("slow %s wait: %+v", w.Kind, w) },
}

ctx = keel.Budget(ctx, keel.MaxRoundTrips(20), keel.MaxRepeats(3)) // per request: catches N+1 loops
  • Tenant concurrency stops one tenant's burst (an import, a dashboard of twenty widgets) from taking every connection. Work nested under a tenant's transaction shares its slot, so a bound of one cannot deadlock.
  • Slow waits turn a hang into a log line while it happens: the waiter's stack and, for a slot, every holder with how long it has held it and the stack that took it.
  • MaxWait ends a wait outright with keel.ErrWaitLimit, so work without a deadline (a background job) cannot wait forever behind a stuck request.
  • Budgets count round trips, statements and repeated statement texts under a context, and fail the call that goes over.
Dynamic queries

For queries whose columns are chosen at run time (a report builder, an export, an admin grid), keel.Registry resolves tables, columns and relation paths by name, with the same tenancy, interceptors and bind parameters as typed queries, and returns columnar results.

q := reg.Query("shipments")
customer := q.Path("customer", "name")
q.Select(q.Path("pro_number"), customer, q.Path("status")).
    Where(q.Path("status").In([]string{"New", "Assigned"}), customer.ContainsFold(search)).
    Limit(100)
res, err := q.Run(ctx, db)

keel/analytics builds a full report engine on top of this: report definitions, a formula language, pivots, charts, drill-through, Excel and CSV exports, and rollup tables (docs/analytics.md).

Replicas and data versions

Reads on a context marked with keel.OnReplica run on the least-loaded replica within Config.MaxReplicaLag, tracked once a second, and still see the context's own writes: a write reads its commit position in the COMMIT round trip. With Config.DataVersions on, keel keeps a version per tenant and table, bumped after commit by a debounced background writer, for caches keyed on the data they read.

ctx = keel.OnReplica(ctx)
page, err := s.Where(s.Status.Eq(fleet.StatusNew)).Order(s.CreatedAt.Desc()).Page(ctx, db, keel.After("", 50))

versions, err := db.DataVersions(ctx, db.TenantKey(ctx), "shipments", "customers")
Migrations

<version>_<name>.sql files and Go functions, applied in order under an advisory lock, each in its own transaction with a 5s lock timeout retried on contention; --keel:notx files run outside a transaction for CREATE INDEX CONCURRENTLY, and R_<name>.sql files re-run when they change.

keel migrate create add_shipment_index -kind notx
keel migrate plan -dsn "$KEEL_DSN"
keel up -dsn "$KEEL_DSN"                 # a live view in a terminal, one line per event elsewhere
keel migrate status -dsn "$KEEL_DSN"
keel migrate squash -yes -dump "docker exec pg pg_dump -U app -d {db}"
keel migrate stamp -out schema/version_gen.go
keel                                     # the console, in a terminal

An application's database binary is one keelcli.App naming its migrations, seeds and database configuration (keelcli).

From Go:

m, err := migrate.Open(ctx, dsn, migrate.Sources(migrate.Source{Name: "core", FS: dir}),
    migrate.Options{After: []string{"SELECT trenova_rls.reconcile()"}})
res, err := m.Up(ctx, migrate.UpOptions{})

and at server startup, migrate.RequireWait(ctx, pool, schema.Version, time.Second). Tests copy a migrated database from a template in milliseconds with migrate.Template.

Performance

bench/ runs the same operations through keel, sqlc, sqlx, raw pgx, Bob, ent, GORM, Jet and Bun, each written the way its documentation writes it, with the tenant filter added by hand where the library has no tenancy. Medians of 6 runs on a 4-vCPU Xeon VM, Postgres 18 in Docker, through a proxy that adds 1 ms to every round trip, as a database in the same cloud region would (recorded on 2026-10-26; fastest in bold):

Operation keel sqlc sqlx pgx Bob ent GORM Jet Bun
get one row by key 1.88 ms 1.87 ms 1.88 ms 1.80 ms 1.95 ms 1.93 ms 1.89 ms 1.99 ms 2.00 ms
list 100 rows 2.48 ms 2.76 ms 2.79 ms 2.89 ms 2.71 ms 2.76 ms 3.45 ms 3.59 ms 2.93 ms
list 1,000 rows 5.62 ms 6.42 ms 7.54 ms 5.96 ms 7.10 ms 7.74 ms 10.77 ms 12.96 ms 8.30 ms
50 rows with a to-one and a to-many relation 3.02 ms 6.47 ms 6.83 ms – 8.40 ms 6.96 ms 8.71 ms 7.25 ms 6.95 ms
read in a transaction with a set_config 1.98 ms 6.99 ms 7.00 ms – 7.31 ms 7.22 ms 7.07 ms 7.41 ms 7.34 ms
keyset page of 50 with its total 2.71 ms 4.47 ms 4.66 ms 4.69 ms 4.42 ms 4.72 ms 4.84 ms 5.05 ms 4.86 ms
insert one row 2.24 ms 2.22 ms 2.36 ms – 2.36 ms 2.39 ms 2.28 ms 2.41 ms 2.37 ms
insert 1,000 rows 15.97 ms (16.75 ms with Copy) 16.97 ms 20.02 ms – 22.70 ms 22.69 ms 19.01 ms 26.40 ms 24.72 ms
save with optimistic locking 2.19 ms 2.24 ms 2.23 ms – 2.43 ms 2.22 ms 2.32 ms 2.37 ms 2.46 ms
build a statement, no database 4.1 µs – – – 12.6 µs – 8.2 µs 22.2 µs 3.6 µs
  • keel wins where it saves round trips. A relation tree loads in the read's own round trip (the others take one per level), a scoped read pipelines BEGIN, the set_config, the read and COMMIT into one round trip (the others take four), and a page shares its round trip with its count. That makes keel 2.1 to 2.9 times as fast as every other library on relation loads, 3.5 to 3.7 times on scoped reads and 1.6 to 1.9 times on pages, and the gap grows with the distance to the database.
  • keel decodes rows fastest. Its lists lead here and with the database on the same host, where the next fastest list of 100 takes 1.3 times keel's time. It allocates 14 times a row on a list of 1,000, against 19 for raw pgx, 22 for sqlc and 34 to 73 for the ORMs.
  • Where an operation is one round trip, everyone is close. Gets, inserts and saves land within 12% of each other. Bun builds statements about 13% faster than keel.

docs/benchmarks.md has the same table with the database on the same host, how each library is written, allocations, the raw output and how to rerun everything.

Status

keel is pre-1.0. It is built alongside Trenova, a transportation management system designed for thousands of tenants on one database, and every Trenova query runs through it. Releases are tagged v0.x.y; until 1.0 a minor version may change the API, and its release notes say how.

Documentation

Development

docker run -d --name keelpg -e POSTGRES_USER=keel -e POSTGRES_PASSWORD=keel -e POSTGRES_DB=keel -p 55432:5432 postgres:18-alpine
go test ./...                      # integration tests skip when Postgres is unreachable
KEEL_TEST_REQUIRE=1 go test ./...  # fail instead of skipping
make gen                           # regenerate the test models' packages
make check                         # gofmt, vet, and generated code up to date
make lint                          # golangci-lint (v2, .golangci.yml) and actionlint
make test-replica                  # start a streaming replica of keelpg and run the replica tests

License

keel is released under the MIT License.

Documentation

Overview

Example (Metadata)
package main

import (
	"fmt"

	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/schema"
)

func main() {
	t := fleetdb.ShipmentsSchema()
	fmt.Println(t.Label, t.Plural, t.Report.Category, t.Doc)
	for _, c := range t.Columns {
		if c.Report == nil || !c.Report.Included {
			continue
		}
		fmt.Println(c.Label, c.Report.Kind == schema.Measure, c.Report.Format, c.Doc)
		for _, v := range c.Enum {
			fmt.Println("  ", v.Value, v.Label)
		}
	}
}
Example (Migrations)
package main

import (
	"context"
	"embed"
	"errors"
	"fmt"
	"io/fs"
	"log"
	"os"

	"github.com/emoss08/keel/migrate"
)

//go:embed testdata/migrations/*.sql
var migrationFiles embed.FS

func main() {
	ctx := context.Background()
	dir, err := fs.Sub(migrationFiles, "testdata/migrations")
	if err != nil {
		log.Fatal(err)
	}
	sources := migrate.Sources(migrate.Source{Name: "core", FS: dir})
	m, err := migrate.Open(ctx, os.Getenv("KEEL_DSN"), sources, migrate.Options{
		After: []string{"ANALYZE"},
		Observer: func(e migrate.Event) {
			switch e.Kind {
			case migrate.Retry:
				log.Printf("%s: lock timeout, attempt %d, blocked by %v", e.Migration.ID(), e.Attempt, e.Blockers)
			case migrate.MigrationEnd:
				log.Printf("%s in %s", e.Migration.ID(), e.Duration)
			}
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer m.Close()

	res, err := m.Up(ctx, migrate.UpOptions{})
	switch {
	case errors.Is(err, migrate.ErrChanged):
		log.Fatal("an applied migration was edited; add a new migration instead")
	case errors.Is(err, migrate.ErrOutOfOrder):
		log.Fatal("a migration is older than the newest applied one; give it a newer version")
	case err != nil:
		log.Fatal(err)
	}
	fmt.Println("applied", len(res.Applied))

	status, err := m.Status(ctx)
	if err != nil {
		log.Fatal(err)
	}
	for _, s := range status {
		fmt.Println(s.ID(), s.State, s.Directives.NoTx)
	}

	if err := migrate.Require(ctx, m.Conn(), sources.Version()); errors.Is(err, migrate.ErrBehind) {
		log.Fatal(err)
	}
}
Example (ModelHooks)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	w := fleetdb.Workers

	grace := &fleet.Worker{FirstName: "  Grace ", LastName: "Hopper"}
	if err := w.Create(ctx, db, grace); err != nil {
		log.Fatal(err)
	}
	fmt.Println(grace.FirstName, grace.WholeName, grace.HiredAt)

	got, err := w.Get(ctx, db, grace.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(got.Initials)
}
Example (NullZero)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Contacts
	contact := &fleet.Contact{Name: "Dana"}
	if err := c.Create(ctx, db, contact); err != nil {
		log.Fatal(err)
	}
	unassigned, err := c.Where(c.CustomerID.Eq("")).Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	_, err = c.Where(c.Name.Eq("Dana")).Update(ctx, db, c.Phone.To(""))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(unassigned)
}
Example (Operators)
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	since := time.Now().Add(-24 * time.Hour)

	n, err := s.Where(
		s.Status.In(fleet.StatusNew, fleet.StatusAssigned),
		s.Priority.Gte(2),
		s.Weight.Between(10_000, 40_000),
		s.BillToID.IsNull(),
		s.ProNumber.HasPrefix("PRO-"),
		s.Hazmat.IsFalse(),
		s.Total.Gt(fleet.Money{Cents: 50_000}),
		s.Labels.Has("expedite"),
		s.Attributes.Contains(map[string]any{"temperature": "frozen"}),
		s.Attributes.Text("trailer", "type").Eq("reefer"),
		s.SearchVector.Match("dallas reefer"),
		keel.Or(s.CanceledAt.IsNull(), s.CanceledAt.Gt(since.Unix())),
		keel.Not(s.Customer().Name.ContainsFold("test")),
	).Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	st := fleetdb.Stops
	late, err := st.Where(st.ArrivedAt.Gte(since), st.Kind.Eq("delivery")).Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, late)
}
Example (QuickStart)
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"log"
	"os"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

type tenantCtxKey struct{}

func main() {
	ctx := context.Background()
	db, err := keel.Open(ctx, keel.Config{
		DSN:           os.Getenv("DATABASE_URL"),
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			t, ok := ctx.Value(tenantCtxKey{}).([2]ids.ID)
			if !ok {
				return nil, false
			}
			return []any{t[0], t[1]}, true
		},
		NewID: newID,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	ctx = context.WithValue(ctx, tenantCtxKey{}, [2]ids.ID{"org_acme", "bu_dallas"})
	c, s := fleetdb.Customers, fleetdb.Shipments

	cust := &fleet.Customer{Code: "ACME", Name: "Acme Freight"}
	if err := c.Create(ctx, db, cust); err != nil {
		log.Fatal(err)
	}
	sh := &fleet.Shipment{CustomerID: cust.ID, ProNumber: "PRO-1001", Status: fleet.StatusNew}
	if err := s.Create(ctx, db, sh); err != nil {
		log.Fatal(err)
	}

	open, err := s.
		Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned), s.Customer().Code.Eq("ACME")).
		Load(s.Customer()).
		Order(s.CreatedAt.Desc()).
		Limit(50).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(open), open[0].Customer.Name)

	sh.Status = fleet.StatusAssigned
	if err := s.Save(ctx, db, sh); errors.Is(err, keel.ErrConflict) {
		fmt.Println("someone else saved this shipment first")
	} else if err != nil {
		log.Fatal(err)
	}

	err = keel.Write(ctx, db, func(ctx context.Context) error {
		mv := &fleet.Move{ShipmentID: sh.ID, Sequence: 1, Status: fleet.StatusAssigned}
		if err := fleetdb.Moves.Create(ctx, db, mv); err != nil {
			return err
		}
		_, err := s.Where(s.ID.Eq(sh.ID)).Update(ctx, db, s.Status.To(fleet.StatusInTransit))
		return err
	})
	if err != nil {
		log.Fatal(err)
	}
}
Example (RelationFilters)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	m := s.Moves()
	st := m.Stops()

	rows, err := s.Where(
		s.Customer().Code.Eq("ACME"),
		keel.Or(s.BillToID.IsNull(), s.BillTo().Code.Ne("ACME")),
		m.Any(m.Status.Eq(fleet.StatusInTransit), m.Loaded.IsTrue()),
		m.Any(st.Any(st.Kind.Eq("delivery"), st.Location().City.Eq("Dallas"))),
		s.Tags().None(s.Tags().Name.Eq("on-hold")),
	).Limit(100).List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}
Example (RelationKeys)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	l := fleetdb.Leaves
	e := l.Entries()
	leaves, err := l.
		Where(e.Any(e.Hours.Gte(8))).
		Load(e.Order(e.PostedAt.Asc()), l.LastEntry()).
		Order(l.ID.Asc()).
		Limit(100).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, lv := range leaves {
		fmt.Println(lv.ID, len(lv.Entries), lv.LastEntry != nil)
	}
}
Example (SharedParameters)
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	type monthly struct {
		Month   time.Time
		Revenue fleet.Money
	}
	month := keel.Raw("date_trunc(?, to_timestamp(?))", "month", s.CreatedAt)
	rows, err := keel.Into[monthly](ctx, db, s.
		Pick(month, s.Total.Sum()).
		Group(month).
		Order(keel.Asc(month)))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}
Example (TenantErrors)
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

func withSystem(ctx context.Context, reason string) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{SystemReason: reason})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	db := appDB
	s := fleetdb.Shipments

	_, err := s.Query().Limit(10).List(context.Background(), db)
	fmt.Println(errors.Is(err, keel.ErrNoScope))

	sys := withSystem(context.Background(), "report")
	_, err = s.Query().Limit(10).List(sys, db)
	fmt.Println(errors.Is(err, keel.ErrAllTenantsRequired))

	_, err = s.AllTenants().Limit(10).List(requestContext(), db)
	fmt.Println(errors.Is(err, keel.ErrSystemScopeRequired))

	err = fleetdb.Customers.Create(requestContext(), db,
		&fleet.Customer{OrganizationID: "org_other", BusinessUnitID: "bu_other", Code: "X", Name: "X"})
	fmt.Println(errors.Is(err, keel.ErrTenantMismatch))

	plant, err := fleetdb.Plants.Get(context.Background(), db, "DAL1")
	fmt.Println(plant, err)
}

Index

Examples

Constants

View Source
const (
	ForUpdate      LockStrength = "FOR UPDATE"
	ForNoKeyUpdate LockStrength = "FOR NO KEY UPDATE"
	ForShare       LockStrength = "FOR SHARE"
	ForKeyShare    LockStrength = "FOR KEY SHARE"

	Wait       LockWait = ""
	NoWait     LockWait = " NOWAIT"
	SkipLocked LockWait = " SKIP LOCKED"
)
View Source
const DefaultCacheBytes = 64 << 20

DefaultCacheBytes is the size of the MemoryCache the result cache keeps when CacheConfig.Local is nil.

Variables

View Source
var (
	// UnboundedPreceding is the first row of the partition.
	UnboundedPreceding = FrameBound{/* contains filtered or unexported fields */}
	// CurrentRow is the current row (in Range and Groups frames, its first or last peer).
	CurrentRow = FrameBound{/* contains filtered or unexported fields */}
	// UnboundedFollowing is the last row of the partition.
	UnboundedFollowing = FrameBound{/* contains filtered or unexported fields */}
)
View Source
var (
	EmptyJSONObject = []byte("{}")
	EmptyJSONArray  = []byte("[]")
)

EmptyJSONObject and EmptyJSONArray are written for a nil map or slice in a non-pointer jsonb field, so the column holds {} or [] rather than NULL or JSON null.

View Source
var (
	// ErrNotFound is returned by One, First and Get when no row matches.
	ErrNotFound = errors.New("keel: no rows")
	// ErrNotSingular is returned by One when more than one row matches.
	ErrNotSingular = errors.New("keel: more than one row")
	// ErrConflict is matched by a *ConflictError: Save found a different version.
	ErrConflict = errors.New("keel: version conflict")
	// ErrNoScope is returned for a statement on a tenant table without a tenant or system scope.
	ErrNoScope = errors.New("keel: no tenant or system scope on the context")
	// ErrAllTenantsRequired is returned for a statement on a tenant table under a system scope
	// that did not declare AllTenants.
	ErrAllTenantsRequired = errors.New("keel: system scope statement on a tenant table must declare AllTenants")
	// ErrSystemScopeRequired is returned when AllTenants is used without a system scope.
	ErrSystemScopeRequired = errors.New("keel: AllTenants requires a system scope")
	// ErrTenantMismatch is returned when a row's tenant columns differ from the scope's.
	ErrTenantMismatch = errors.New("keel: row belongs to a different tenant than the scope")
	// ErrUnbounded is returned by List without a Limit.
	ErrUnbounded = errors.New("keel: List needs a Limit; use Page, or All for a set known to be small")
	// ErrReadOnly is returned by a write inside a read-only transaction.
	ErrReadOnly = errors.New("keel: write inside a read-only transaction")
	// ErrScopeMismatch is returned by a statement, Read, Write or Savepoint that would join the
	// transaction on the context under another scope than the one the transaction began with. Run
	// such work in its own transaction with Detached.
	ErrScopeMismatch = errors.New("keel: the context's scope differs from its transaction's; use keel.Detached for a transaction of its own")
	// ErrWaitLimit is returned by work that waited Config.MaxWait for a tenant slot or a pool
	// connection without getting one.
	ErrWaitLimit = errors.New("keel: waited longer than Config.MaxWait")
)
View Source
var ErrBudgetExceeded = errors.New("keel: budget exceeded")

ErrBudgetExceeded is matched by a *BudgetError: work under a Budget went past one of its limits.

Functions

func AfterCommit

func AfterCommit(ctx context.Context, fn func(ctx context.Context))

AfterCommit runs fn after the transaction on ctx commits; it is dropped if the transaction rolls back. Outside a transaction fn runs at once.

func Alias

func Alias[H any](ref *Ref, alias string, build func(*Ref) H) H

Alias returns a handle that references the same table as ref under its own alias, for self-joins and correlated subqueries. Generated As methods call it. Aliases are memoised, so alias names should be constants.

func Batch

func Batch(ctx context.Context, db *DB, items ...BatchItem) error

Batch runs independent reads in one round trip. The main statements of every item are sent together, in the transaction on the context, else in an implicit transaction when Config.OnBegin is set (BEGIN, the OnBegin statements, the items' statements and, when no relation loads follow, COMMIT in one round trip), else outside a transaction. The to-many relation loads of all items follow, one round trip per level.

Each item keeps its terminal's checks: scope errors, interceptors, AfterLoad hooks, ErrUnbounded for a list without a Limit, ErrNotFound and ErrNotSingular for a single-row item. Batch returns the first error, and fills the destinations only when every item succeeded. The shortest Timeout among the items bounds the whole batch.

var rows []*shipment.Shipment
var total int64
var cust *customer.Customer
err := keel.Batch(ctx, db,
    keel.ListInto(s.Where(open).Order(s.CreatedAt.Desc()).Limit(50), &rows),
    keel.CountInto(s.Where(open), &total),
    keel.OneInto(c.Where(c.ID.Eq(id)), &cust),
)
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	type statusCount struct {
		Status fleet.Status
		Loads  int64
	}
	s, c := fleetdb.Shipments, fleetdb.Customers
	open := s.Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned))

	var rows []*fleet.Shipment
	var total int64
	var byStatus []statusCount
	var cust *fleet.Customer
	err := keel.Batch(ctx, db,
		keel.ListInto(open.Clone().Order(s.CreatedAt.Desc()).Limit(50), &rows),
		keel.CountInto(open.Clone(), &total),
		keel.IntoSlice(s.Group(s.Status).Pick(s.Status, keel.Count()), &byStatus),
		keel.OneInto(c.Where(c.ID.Eq("cus_01")), &cust),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows), total, len(byStatus), cust.Name)
}

func Budget

func Budget(ctx context.Context, opts ...BudgetOption) context.Context

Budget returns a context that counts the round trips, statements and repeated statement texts of the work keel does under it, every terminal, transaction and goroutine included, and limits them as opts say. A budget under another one counts against both. Without limits it only counts, for BudgetReportFrom. A statement text counts once per round trip it is sent in, so the chunks of one CreateMany, which share a text and a round trip, are not repeats. It is safe for concurrent use.

func Code

func Code(err error) string

Code returns the SQLSTATE of a Postgres error, or "".

func ColumnOf

func ColumnOf(e Expr) (table *schema.Table, index int32, ok bool)

ColumnOf returns the table and the column index of a generated column handle, such as fleetdb.Shipments.CreatedAt, for packages that take columns in declarations. ok is false for an expression that is not a column, and for a column reached through a relation path (fleetdb.Shipments.Customer.Name), which belongs to a related row rather than to the table. A column of an alias made with As is the table's own column.

func ConstraintName

func ConstraintName(err error) string

ConstraintName returns the constraint a Postgres error names, or "".

func Exec

func Exec(ctx context.Context, db *DB, sql string, args ...any) (int64, error)

Exec runs one SQL statement that keel does not render, such as DDL or a write to a bookkeeping table without a model, and returns the number of rows it affected. It joins the transaction on the context, so the statement commits or rolls back with the rest of it; outside a transaction it runs as any terminal does, in an implicit transaction when Config.OnBegin is set. Inside a Read, Postgres refuses a statement that writes.

Observers see the statement with the Op "exec". keel cannot see what the statement touches, so it bypasses scope checks, interceptors and hooks, always runs on the primary, records no commit position for OnReplica outside a Write, and changes no data version: call MarkChanged with the tables it writes. Bind values as $1, $2, … arguments; never format them into sql.

func ExecAll

func ExecAll(ctx context.Context, db *DB, sql string, args ...any) (int64, error)

ExecAll runs one statement keel does not render on the primary and on every replica, outside any transaction on the context, and returns the rows it affected on all of them together. It is for statements about the servers rather than the data, which must reach whichever server a backend runs on: cancelling the backends tagged with an application_name, for one. Every server is tried; the errors of those that fail are joined. Observers see one statement per server with the Op "exec"; the replica's events carry its name.

func ExecBatch

func ExecBatch(ctx context.Context, db *DB, stmts ...Statement) ([]int64, error)

ExecBatch runs statements keel does not render in one round trip, in order, as Exec runs one: joining the transaction on the context, else in an implicit transaction when Config.OnBegin is set. It returns the rows each affected. The first failing statement aborts the round trip; inside a transaction the transaction is then failed, and outside one nothing commits. Observers see each statement with the Op "exec".

func InTx

func InTx(ctx context.Context, db *DB) bool

InTx reports whether ctx carries a transaction of db.

func Int16s

func Int16s[S ~int16](vs []S) []int16

Int16s converts a slice of an int16 type to []int16.

func Int32s

func Int32s[S ~int32](vs []S) []int32

Int32s converts a slice of an int32 type to []int32.

func Int64s

func Int64s[S ~int64](vs []S) []int64

Int64s converts a slice of an int64 type to []int64.

func Into

func Into[R any](ctx context.Context, db *DB, p Projection) ([]R, error)

Into runs a projection and decodes each row into R: the picked expressions fill R's exported fields in order. R may also be a single scalar type when one expression is picked.

type statusCount struct {
    Status shipment.Status
    Total  int64
}
rows, err := keel.Into[statusCount](ctx, db, s.Group(s.Status).Pick(s.Status, keel.Count()))
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	type statusTotal struct {
		Status  fleet.Status
		Loads   int64
		Hazmat  int64
		Revenue fleet.Money
	}
	rows, err := keel.Into[statusTotal](ctx, db, s.
		Group(s.Status).
		Pick(s.Status, keel.Count(), keel.Count().Filter(s.Hazmat.IsTrue()), s.Total.Sum()).
		Having(keel.Count().Gt(10)).
		Order(keel.Count().Desc()))
	if err != nil {
		log.Fatal(err)
	}

	heaviest, err := keel.IntoOne[*int64](ctx, db, s.Pick(s.Weight.Max()).Where(s.Status.Eq(fleet.StatusNew)))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(rows, heaviest)
}

func IntoOne

func IntoOne[R any](ctx context.Context, db *DB, p Projection) (R, error)

IntoOne is Into for a projection that returns exactly one row, such as an aggregate.

func IsCheck

func IsCheck(err error, constraints ...string) bool

IsCheck reports a check constraint violation, optionally on one of the named constraints.

func IsExclusion

func IsExclusion(err error, constraints ...string) bool

IsExclusion reports an exclusion constraint violation.

func IsForeignKey

func IsForeignKey(err error, constraints ...string) bool

IsForeignKey reports a foreign key violation, optionally on one of the named constraints.

func IsNotNull

func IsNotNull(err error) bool

IsNotNull reports a not-null violation.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports a serialization failure or deadlock, which a retried transaction can resolve.

func IsUnique

func IsUnique(err error, constraints ...string) bool

IsUnique reports a unique violation, optionally on one of the named constraints.

Example
package main

import (
	"context"
	"fmt"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	err := fleetdb.Shipments.Create(ctx, db, &fleet.Shipment{
		CustomerID: "cus_01", ProNumber: "PRO-1001", Status: fleet.StatusNew,
	})
	switch {
	case keel.IsUnique(err, "uq_shipments_pro"):
		fmt.Println("PRO number already used")
	case keel.IsForeignKey(err, "fk_shipments_customer"):
		fmt.Println("no such customer")
	case err != nil:
		fmt.Println("SQLSTATE", keel.Code(err), "constraint", keel.ConstraintName(err))
	}
}

func JoinTx

func JoinTx(ctx context.Context, db *DB, conn *pgx.Conn) (context.Context, func(committed bool))

JoinTx makes keel run inside a transaction another data layer began on conn, for an application moving to keel while it still writes through another library over the same pgx connection (Bun on pgx's stdlib driver, plain pgx). Every keel operation on the returned context runs on conn, in that transaction: keel sends no BEGIN, COMMIT or Config.OnBegin statements (the owner began the transaction and set its scope), Read and Write join it, Savepoint nests in it, and replicas are not used. Scope checks, tenancy, interceptors, read filters, hooks and observers apply as in a transaction keel began. The transaction's scope is ctx's: keel work under another scope returns ErrScopeMismatch, and a Detached Read or Write runs on keel's own pool instead.

The owner calls done once the transaction ends: done(true) after COMMIT runs the Committed hooks and AfterCommit callbacks and hands the tables the transaction changed to the data versions; done(false) after ROLLBACK drops them. Calls after the first do nothing, and keel refuses to run on the context afterwards.

tx, err := bunDB.BeginTx(ctx, nil) // on a pgx connection
ctx, done := keel.JoinTx(ctx, db, conn)
err = keel.Write(ctx, db, func(ctx context.Context) error { … })
… tx.Commit() …
done(err == nil)

conn is used by one goroutine at a time, as any transaction is.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	conn, err := db.Pool().Acquire(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Release()
	tx, err := conn.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}
	kctx, done := keel.JoinTx(ctx, db, conn.Conn())
	err = fleetdb.Customers.Create(kctx, db, &fleet.Customer{Code: "ACME", Name: "Acme"})
	if err == nil {
		_, err = tx.Exec(ctx, "UPDATE plants SET name = upper(name)")
	}
	if err != nil {
		_ = tx.Rollback(ctx)
		done(false)
		log.Fatal(err)
	}
	err = tx.Commit(ctx)
	done(err == nil)
	if err != nil {
		log.Fatal(err)
	}
}

func LockKey

func LockKey(ctx context.Context, db *DB, key string) error

LockKey takes a transaction-scoped advisory lock on key, waiting for it, so work without a row to lock is serialised across processes. It needs a transaction; the lock is released when the transaction ends.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		if err := keel.LockKey(ctx, db, "invoice-run:org_acme"); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	err = keel.Write(ctx, db, func(ctx context.Context) error {
		got, err := keel.TryLockKey(ctx, db, "rate-refresh")
		if err != nil {
			return err
		}
		if !got {
			fmt.Println("another process is refreshing rates")
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}
}

func MarkChanged

func MarkChanged(ctx context.Context, db *DB, tables ...string)

MarkChanged records that tables changed, for writes keel cannot see into: Exec, statements sent through Pool, triggers. Under a tenant scope the pairs are the tenant's; under a system scope or without a scope they are recorded for no tenant (""), which moves the tables' version for every tenant. Inside a transaction they are written after it commits. It does nothing when data versions are off.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		_, err := keel.Exec(ctx, db, `UPDATE shipments SET priority = priority + 1
		 WHERE organization_id = $1 AND business_unit_id = $2 AND status = 'New'`, "org_acme", "bu_dallas")
		if err != nil {
			return err
		}
		keel.MarkChanged(ctx, db, "shipments")
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}
}

func OnCreate

func OnCreate[T any](db *DB, t tabler[T], phase Phase,
	fn func(ctx context.Context, m *Mutation[T]) error, opts ...HookOption,
)

OnCreate registers a hook for creates of T's table.

Example
package main

import (
	"context"
	"errors"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

func main() {
	db := appDB
	s := fleetdb.Shipments

	keel.OnCreate(db, s, keel.BeforeWrite, func(ctx context.Context, m *keel.Mutation[*fleet.Shipment]) error {
		if m.Row.ProNumber == "" {
			return errors.New("shipment needs a PRO number")
		}
		if m.Row.Attributes == nil {
			m.Row.Attributes = map[string]any{}
		}
		return nil
	})
}

func OnDelete

func OnDelete[T any](db *DB, t tabler[T], phase Phase,
	fn func(ctx context.Context, m *Mutation[T]) error, opts ...HookOption,
)

OnDelete registers a hook for deletes of T's table.

func OnReplica

func OnReplica(ctx context.Context) context.Context

OnReplica marks ctx so that read terminals (One, First, List, All, Page, Count, CountUpTo, CountEstimate, Exists, Each, Into, IntoOne, Batch) called with it outside a transaction run on a replica within Config.MaxReplicaLag, the least loaded relative to its weight, and on the primary when none qualifies. Explicit transactions keep using the primary unless started with ReplicaTx.

Reads on the context see its own writes: a write made with ctx, or a context derived from it, records the primary's WAL position after its commit, in the commit's round trip, and later replica reads on ctx run only where that position has been replayed. A replica the lag tracker has seen replay it is used as is; one it has not yet seen is used when the write is older than the replica's lag, with a check of its replay position sent first in the read's own round trip, and the read moves to the primary when the check fails. A context that is already marked is returned unchanged, so the positions are shared with it.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := keel.OnReplica(requestContext()), appDB
	s := fleetdb.Shipments

	page, err := s.Where(s.Status.Eq(fleet.StatusNew)).
		Order(s.CreatedAt.Desc()).
		Page(ctx, db, keel.After("", 50))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(page.Items))

	sh := &fleet.Shipment{CustomerID: "cus_01", ProNumber: "PRO-3001", Status: fleet.StatusNew}
	if err := s.Create(ctx, db, sh); err != nil {
		log.Fatal(err)
	}
	got, err := s.Get(ctx, db, sh.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(got.ProNumber)
}

func OnReplicaAfter

func OnReplicaAfter(ctx context.Context, db *DB, pos Position) context.Context

OnReplicaAfter is OnReplica for reads that must see everything db committed up to pos, such as a position an earlier request returned from CommitPosition and the client sent back.

func OnUpdate

func OnUpdate[T any](db *DB, t tabler[T], phase Phase,
	fn func(ctx context.Context, m *Mutation[T]) error, opts ...HookOption,
)

OnUpdate registers a hook for updates of T's table: Save, Upsert conflicts and bulk updates.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

func main() {
	db := appDB
	s := fleetdb.Shipments

	keel.OnUpdate(db, s, keel.AfterWrite, func(ctx context.Context, m *keel.Mutation[*fleet.Shipment]) error {
		if m.Old == nil || !m.Changed(s.Status) || m.Old.Status == m.Row.Status {
			return nil
		}
		log.Printf("shipment %s: %s -> %s", m.Row.ID, m.Old.Status, m.Row.Status)
		return nil
	}, keel.Needs(s.Status))
}

func OnWrite

func OnWrite[T any](db *DB, t tabler[T], phase Phase,
	fn func(ctx context.Context, m *Mutation[T]) error, opts ...HookOption,
)

OnWrite registers a hook for every write of T's table.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

func main() {
	db := appDB
	c := fleetdb.Customers

	keel.OnWrite(db, c, keel.Committed, func(ctx context.Context, m *keel.Mutation[*fleet.Customer]) error {
		log.Printf("customer %s %s; refresh the search index", m.Row.ID, m.Op)
		return nil
	})
}

func Path

func Path[H any](parent *Ref, rel int32, build func(*Ref) H) H

Path returns the handle for relation rel of the table referenced by parent, building it with build on first use. Generated relation methods call it. Every handle type built for one relation of one parent references the same Ref, so their columns resolve to the same join.

func Pointers

func Pointers[T any](vs []T) []*T

Pointers returns pointers to the elements of vs, for a custom type whose driver.Valuer has a pointer receiver: the driver encodes each element through it.

func QueryAll

func QueryAll[T any](ctx context.Context, db *DB, to pgx.RowToFunc[T], sql string, args ...any) ([]T, error)

QueryAll runs one SQL statement keel does not render, as QueryRows does, and returns every row it returns converted by to, such as pgx.RowTo[int64] or pgx.RowToStructByName[T].

func QueryRows

func QueryRows(ctx context.Context, db *DB, fn func(row pgx.CollectableRow) error, sql string,
	args ...any,
) error

QueryRows runs one SQL statement keel does not render and calls fn for each row it returns, in order, with the rows positioned on that row: read it with Scan, Values or a pgx row function such as pgx.RowToStructByName. It joins the transaction on the context, Read or Write, so a read inside a Read with ReplicaTx runs on that replica; outside a transaction it runs on the primary, in an implicit transaction when Config.OnBegin is set. An error from fn stops the rows and is returned. fn must not run statements of its own on the same transaction: the connection is busy until the rows are read.

Observers see the statement with the Op "query". As with Exec, keel cannot see what the statement touches: scope checks, interceptors, read filters and hooks do not apply, and a write through it changes no data version. Bind values as $1, $2, … arguments.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/ids"
	"github.com/jackc/pgx/v5"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		n, err := keel.QueryValue[int64](ctx, db, "SELECT count(*) FROM keel_rollup_dirty WHERE rollup = $1", "shipments_daily")
		if err != nil {
			return err
		}
		fmt.Println("pending slices:", n)
		return keel.QueryRows(ctx, db, func(row pgx.CollectableRow) error {
			var code string
			var loads int64
			if err := row.Scan(&code, &loads); err != nil {
				return err
			}
			fmt.Println(code, loads)
			return nil
		}, `SELECT c.code, count(*) FROM shipments s JOIN customers c ON c.id = s.customer_id
			WHERE s.status = $1 GROUP BY c.code`, fleet.StatusDelivered)
	})
	if err != nil {
		log.Fatal(err)
	}
}

func QueryValue

func QueryValue[T any](ctx context.Context, db *DB, sql string, args ...any) (T, error)

QueryValue runs one SQL statement keel does not render, as QueryRows does, and scans the first column of its first row into a T. It returns ErrNotFound when the statement returns no row.

func Read

func Read(ctx context.Context, db *DB, fn func(ctx context.Context) error, opts ...TxOption) error

Read runs fn in a read-only transaction, on the primary unless ReplicaTx is given, at the isolation level an option gives, else Config.ReadIsolation. A Read or Write already on the context is joined, unless Detached is given; joining it under another scope than the one it began with returns ErrScopeMismatch.

When fn returns, keel sends COMMIT (ROLLBACK when fn failed) and returns without waiting for the reply, which a goroutine reads before the connection goes back to the pool; a connection whose reply fails is closed. A read-only transaction has nothing left to commit, so the reply carries no information the caller needs. Serializable reads, whose COMMIT can report a serialization failure, and Config.AwaitReadCommit wait as writes do.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	var open, late int64

	err := keel.Read(ctx, db, func(ctx context.Context) error {
		var err error
		if open, err = s.Where(s.Status.Ne(fleet.StatusDelivered)).Count(ctx, db); err != nil {
			return err
		}
		late, err = s.Where(s.Status.Eq(fleet.StatusInTransit), s.Priority.Gte(2)).Count(ctx, db)
		return err
	}, keel.RepeatableRead)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(open, late)
}

func ReadValue

func ReadValue[T any](ctx context.Context, db *DB, fn func(ctx context.Context) (T, error),
	opts ...TxOption,
) (T, error)

ReadValue is Read for a function that returns a value.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	sh, err := keel.ReadValue(ctx, db, func(ctx context.Context) (*fleet.Shipment, error) {
		return s.Where(s.ProNumber.Eq("PRO-1001")).Load(s.Customer(), s.Moves()).One(ctx, db)
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(sh.Customer.Name)
}

func Savepoint

func Savepoint(ctx context.Context, db *DB, fn func(ctx context.Context) error) error

Savepoint runs fn inside a savepoint of the transaction on ctx: an error from fn rolls back to the savepoint, drops the Committed hooks and AfterCommit callbacks queued inside it, and is returned, leaving the transaction usable. Outside a transaction it runs fn in a new Write. Under another scope than the transaction's it returns ErrScopeMismatch.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Customers

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		for _, code := range []string{"ACME", "GLOBEX", "ACME"} {
			err := keel.Savepoint(ctx, db, func(ctx context.Context) error {
				return c.Create(ctx, db, &fleet.Customer{Code: code, Name: code})
			})
			if keel.IsUnique(err, "uq_customers_code") {
				continue
			}
			if err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}
}
func SetLinks[K any](ctx context.Context, db *DB, rel interface{ TableRef() *Ref }, parent K,
	targets []K,
) error

SetLinks makes the rows the parent row with key parent links to through the many-to-many relation rel exactly the rows with keys targets: one statement deletes the parent's join rows to every other target and inserts the missing ones (ON CONFLICT DO NOTHING, so the join table needs a primary key or unique index over its columns). rel is the relation's handle, as a generated relation method returns it (shipmentdb.Shipments.Tags()). An empty targets unlinks every row.

Keys are strings or integers, or named types of them. A join table between two tenant tables is written under the scope's tenant: its tenant columns take the scope's values and the delete is limited to them, so a tenant scope is required; a system scope is refused, since the parent's tenant is not known. Observers see the statement with the Op "links"; the join table has no model, so no hook or interceptor runs, and its data version moves for the scope's tenant.

func Strings

func Strings[S ~string](vs []S) []string

Strings converts a slice of a string type to []string.

func TenantKeyOf

func TenantKeyOf(values ...any) string

TenantKeyOf returns the data-versions tenant key of tenant values, given in the order of Config.TenantColumns: the values as text joined with "/", with "\" and "/" inside a value escaped with "\". Values of named types are keyed as their underlying type.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withSystem(ctx context.Context, reason string) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{SystemReason: reason})
}

func main() {
	ctx, db := withSystem(context.Background(), "warm report caches"), appDB

	tenant := keel.TenantKeyOf(ids.ID("org_acme"), ids.ID("bu_dallas"))
	versions, err := db.DataVersions(ctx, tenant, "shipments")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(tenant, versions["shipments"])
}

func TryLockKey

func TryLockKey(ctx context.Context, db *DB, key string) (bool, error)

TryLockKey is LockKey without waiting: it reports whether the lock was taken.

func WithReadFilter

func WithReadFilter(ctx context.Context, f ReadFilter) context.Context

WithReadFilter returns a context whose read statements apply f to every table reference they make: the base table, joined relations, picks and each table a pick walks, EXISTS, subqueries, CTE bodies and pre-aggregations. Interceptors cover only a statement's base table; a read filter covers every table, so a joined row the filter excludes reads as absent (its columns NULL) and a related row it excludes does not count. f is called each time a statement references a table, so it should return quickly, from a cache when the predicates cost anything to build. A ReadFilter already on ctx is replaced.

func Write

func Write(ctx context.Context, db *DB, fn func(ctx context.Context) error, opts ...TxOption) error

Write runs fn in a read-write transaction on the primary. A Write already on the context is joined, unless Detached is given; a Read is not, and returns ErrReadOnly. Joining a transaction under another scope than the one it began with returns ErrScopeMismatch. On a context marked with OnReplica, COMMIT reads the commit position in its round trip, for the context's replica reads.

Example
package main

import (
	"context"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		sh, err := s.Where(s.ID.Eq("shp_01")).Lock(keel.ForUpdate, keel.NoWait).One(ctx, db)
		if err != nil {
			return err
		}
		sh.Status = fleet.StatusAssigned
		if err := s.Save(ctx, db, sh, s.Status); err != nil {
			return err
		}
		mv := &fleet.Move{ShipmentID: sh.ID, Sequence: 1, Status: fleet.StatusAssigned}
		if err := fleetdb.Moves.Create(ctx, db, mv); err != nil {
			return err
		}
		keel.AfterCommit(ctx, func(ctx context.Context) {
			log.Printf("shipment %s assigned", sh.ID)
		})
		return nil
	}, keel.LockTimeout(2*time.Second))
	if err != nil {
		log.Fatal(err)
	}
}

func WriteValue

func WriteValue[T any](ctx context.Context, db *DB, fn func(ctx context.Context) (T, error),
	opts ...TxOption,
) (T, error)

WriteValue is Write for a function that returns a value.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB

	id, err := keel.WriteValue(ctx, db, func(ctx context.Context) (ids.ID, error) {
		c := &fleet.Customer{Code: "UMBRELLA", Name: "Umbrella Transport"}
		if err := fleetdb.Customers.Create(ctx, db, c); err != nil {
			return "", err
		}
		sh := &fleet.Shipment{CustomerID: c.ID, ProNumber: "PRO-2001", Status: fleet.StatusNew}
		if err := fleetdb.Shipments.Create(ctx, db, sh); err != nil {
			return "", err
		}
		return sh.ID, nil
	}, keel.Serializable, keel.Retry(3))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(id)
}

Types

type Agg

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

Agg is a computed value: an aggregate (count, sum, min, max, avg), a window function or a scalar function keel knows. It compares to values, orders, and filters or windows when it aggregates.

func ArrayAgg

func ArrayAgg(e Expr) Agg

ArrayAgg is array_agg(e). OrderBy orders the elements.

func Avg

func Avg(e Expr) Agg

Avg is avg(e).

func BoolAnd

func BoolAnd(p Pred) Agg

BoolAnd is the aggregate bool_and(p): whether p holds for every row, NULL over no rows.

func BoolOr

func BoolOr(p Pred) Agg

BoolOr is the aggregate bool_or(p): whether p holds for any row, NULL over no rows.

func Count

func Count() Agg

Count is count(*).

func CountOf

func CountOf(e Expr) Agg

CountOf is count(e): the rows where e is not NULL.

func CumeDist

func CumeDist() Agg

CumeDist is cume_dist(): the fraction of partition rows ordered before or tied with the row.

func DenseRank

func DenseRank() Agg

DenseRank is dense_rank(): rank without gaps.

func FirstValue

func FirstValue(e Expr) Agg

FirstValue is first_value(e): e in the first row of the window frame.

func Grouping

func Grouping(es ...Expr) Agg

Grouping is GROUPING(es...): a bit mask with a 1 for each of es that the row's grouping set does not group by. In a query with GroupingSets, Rollup or Cube it tells a subtotal or total row from a detail row; it may appear in Pick, Having and Order. Each of es must be written exactly as it is in the grouping.

func JSONBAgg

func JSONBAgg(e Expr) Agg

JSONBAgg is the aggregate jsonb_agg(e), the values of e as a jsonb array (NULL over no rows). OrderBy orders the elements.

func Lag

func Lag(e Expr, offset int, def any) Agg

Lag is lag(e, offset, def): e in the row offset rows before the current row in its partition, or def (a value bound as a parameter or an expression) when there is no such row. A nil def is NULL.

func LastValue

func LastValue(e Expr) Agg

LastValue is last_value(e): e in the last row of the window frame. The default frame ends at the current row's last peer; give the window a frame that reaches UnboundedFollowing to read the partition's last row.

func Lead

func Lead(e Expr, offset int, def any) Agg

Lead is lead(e, offset, def): e in the row offset rows after the current row.

func Max

func Max(e Expr) Agg

Max is max(e).

func Min

func Min(e Expr) Agg

Min is min(e).

func Mode

func Mode(order Order) Agg

Mode is mode() WITHIN GROUP (ORDER BY order): the group's most frequent value, the first in order on a tie.

func NthValue

func NthValue(e Expr, n int) Agg

NthValue is nth_value(e, n): e in the n-th row of the window frame, from 1.

func Ntile

func Ntile(n int) Agg

Ntile is ntile(n): the bucket, from 1 to n, of the row when the partition is split into n buckets as evenly as possible. n is bound as a parameter.

func PercentRank

func PercentRank() Agg

PercentRank is percent_rank(): (rank - 1) / (partition rows - 1), from 0 to 1.

func PercentileCont

func PercentileCont(fraction float64, order Order) Agg

PercentileCont is percentile_cont(fraction) WITHIN GROUP (ORDER BY order): the value at fraction (0 to 1) of the ordered group, interpolated between rows.

func PercentileDisc

func PercentileDisc(fraction float64, order Order) Agg

PercentileDisc is percentile_disc(fraction) WITHIN GROUP (ORDER BY order): the first value of the ordered group whose position reaches fraction.

func Rank

func Rank() Agg

Rank is rank(): the row's rank in its partition by the window's order, with gaps after ties.

func RowNumber

func RowNumber() Agg

RowNumber is row_number(): the row's position in its window partition, from 1. Give it a window with Over or OverWindow.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	type ranked struct {
		Customer ids.ID
		Pro      string
		Rank     int64
	}
	rows, err := keel.Into[ranked](ctx, db, s.
		Pick(s.CustomerID, s.ProNumber,
			keel.RowNumber().Over([]keel.Expr{s.CustomerID}, s.CreatedAt.Desc())).
		Where(s.Status.Eq(fleet.StatusDelivered)).
		Limit(500))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

func StringAgg

func StringAgg(e Expr, sep string) Agg

StringAgg is string_agg(e, sep). OrderBy orders the pieces.

func Sum

func Sum(e Expr) Agg

Sum is sum(e).

func (Agg) Asc

func (a Agg) Asc() Order

Asc orders by a ascending.

func (Agg) Desc

func (a Agg) Desc() Order

Desc orders by a descending.

func (Agg) Distinct

func (a Agg) Distinct() Agg

Distinct aggregates the distinct values of the argument: array_agg(DISTINCT e).

func (Agg) Eq

func (a Agg) Eq(v any) Pred

Eq is a = v.

func (Agg) Filter

func (a Agg) Filter(p Pred) Agg

Filter restricts the rows an aggregate sees: count(*) FILTER (WHERE p).

func (Agg) Gt

func (a Agg) Gt(v any) Pred

Gt is a > v.

func (Agg) Gte

func (a Agg) Gte(v any) Pred

Gte is a >= v.

func (Agg) Lt

func (a Agg) Lt(v any) Pred

Lt is a < v.

func (Agg) Lte

func (a Agg) Lte(v any) Pred

Lte is a <= v.

func (Agg) Ne

func (a Agg) Ne(v any) Pred

Ne is a <> v.

func (Agg) OrderBy

func (a Agg) OrderBy(os ...Order) Agg

OrderBy orders the rows an aggregate reads, for an aggregate whose result depends on their order: array_agg(e ORDER BY …), string_agg(e, sep ORDER BY …).

func (Agg) Over

func (a Agg) Over(partition []Expr, order ...Order) Agg

Over turns the aggregate into a window function over the rows that share partition, ordered by order. It is OverWindow(PartitionBy(partition...).OrderBy(order...)).

func (Agg) OverWindow

func (a Agg) OverWindow(w Window) Agg

OverWindow turns the aggregate into a window function over w: a running or moving aggregate, a rank, an offset or a value in the window frame.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	byDay := keel.PartitionBy(s.CustomerID).OrderBy(s.CreatedAt.Asc())

	type trend struct {
		Pro     string
		Running fleet.Money
		Moving  float64
		Share   float64
		Rank    int64
		Change  *fleet.Money
	}
	rows, err := keel.Into[trend](ctx, db, s.Pick(
		s.ProNumber,
		keel.Sum(s.Total).OverWindow(byDay.Rows(keel.UnboundedPreceding, keel.CurrentRow)),
		keel.Avg(s.Total).OverWindow(byDay.Rows(keel.Preceding(2), keel.CurrentRow)),
		keel.Raw("? * 100.0 / ?", s.Total, keel.Sum(s.Total).OverWindow(keel.Window{})),
		keel.Rank().OverWindow(keel.PartitionBy(s.CustomerID).OrderBy(s.Total.Desc())),
		keel.Raw("? - ?", s.Total, keel.Lag(s.Total, 1, nil).OverWindow(byDay)),
	).Order(s.CustomerID.Asc(), s.CreatedAt.Asc()).Limit(1000))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

type AnyCol

type AnyCol struct{ Col }

AnyCol is a column of a type keel has no dedicated kind for (numeric through a decimal type, a custom sql.Scanner). Generated wrappers give it typed methods.

func (AnyCol) Add

func (c AnyCol) Add(v NumericValue) NumericExpr

Add is (col + v), an expression over a numeric (scanner) column; Increment is the assignment.

func (AnyCol) Avg

func (c AnyCol) Avg() Agg

func (AnyCol) Between

func (c AnyCol) Between(lo, hi any) Pred

func (AnyCol) Div

func (c AnyCol) Div(v NumericValue) NumericExpr

Div is (col / v).

func (AnyCol) Eq

func (c AnyCol) Eq(v any) Pred

func (AnyCol) Gt

func (c AnyCol) Gt(v any) Pred

func (AnyCol) Gte

func (c AnyCol) Gte(v any) Pred

func (AnyCol) In

func (c AnyCol) In(slice any) Pred

In takes a slice of values (for example []decimal.Decimal) bound as one array parameter.

func (AnyCol) InArray

func (c AnyCol) InArray(a NumericArrayValue) Pred

InArray is (col = ANY(a)).

func (AnyCol) Increment

func (c AnyCol) Increment(v any) Assignment

func (AnyCol) IsDistinctFrom

func (c AnyCol) IsDistinctFrom(v any) Pred

func (AnyCol) IsDistinctFromExpr

func (c AnyCol) IsDistinctFromExpr(o NumericValue) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (AnyCol) IsNotDistinctFrom

func (c AnyCol) IsNotDistinctFrom(v any) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (AnyCol) IsNotDistinctFromExpr

func (c AnyCol) IsNotDistinctFromExpr(o NumericValue) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (AnyCol) Lt

func (c AnyCol) Lt(v any) Pred

func (AnyCol) Lte

func (c AnyCol) Lte(v any) Pred

func (AnyCol) Mod

func (c AnyCol) Mod(v NumericValue) NumericExpr

Mod is (col % v).

func (AnyCol) Mul

func (c AnyCol) Mul(v NumericValue) NumericExpr

Mul is (col * v).

func (AnyCol) Ne

func (c AnyCol) Ne(v any) Pred

func (AnyCol) Neg

func (c AnyCol) Neg() NumericExpr

Neg is (-col).

func (AnyCol) NotIn

func (c AnyCol) NotIn(slice any) Pred

func (AnyCol) NotInArray

func (c AnyCol) NotInArray(a NumericArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (AnyCol) Sub

func (c AnyCol) Sub(v NumericValue) NumericExpr

Sub is (col - v).

func (AnyCol) Sum

func (c AnyCol) Sum() Agg

func (AnyCol) To

func (c AnyCol) To(v any) Assignment

type Assignment

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

Assignment is one SET term of an update, built by a column's To, ToNull or Add.

type BatchItem

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

BatchItem is one read of a Batch with its typed destination. ListInto, AllInto, OneInto, FirstInto, PageInto, CountInto, CountUpToInto, CountEstimateInto and ExistsInto read what the query terminal of the same name returns; IntoSlice and IntoValue read what Into and IntoOne return. An item holds no result, so it can run in more than one Batch.

func AllInto

func AllInto[T any](q *Query[T], dst *[]T) BatchItem

AllInto reads every matching row into dst, as All does.

func CountEstimateInto

func CountEstimateInto[T any](q *Query[T], exactUpTo int64, dst *RowCount) BatchItem

CountEstimateInto stores the number of matching rows, counted exactly up to exactUpTo and estimated by the planner above it, in dst, as CountEstimate does. Its count and EXPLAIN join the batch's round trip.

func CountInto

func CountInto[T any](q *Query[T], dst *int64) BatchItem

CountInto stores the number of matching rows in dst, as Count does.

func CountUpToInto

func CountUpToInto[T any](q *Query[T], limit int64, dst *int64) BatchItem

CountUpToInto stores the number of matching rows, counted up to limit, in dst, as CountUpTo does.

func DynCountEstimateInto

func DynCountEstimateInto(q *DynQuery, exactUpTo int64, dst *RowCount) BatchItem

DynCountEstimateInto stores the number of rows the dynamic query returns, counted exactly up to exactUpTo and estimated by the planner above it, in dst, as DynQuery.CountEstimate does.

func DynCountInto

func DynCountInto(q *DynQuery, dst *int64) BatchItem

DynCountInto stores the number of rows the dynamic query returns in dst: SELECT count(*) FROM (query).

func DynExistsInto

func DynExistsInto(q *DynQuery, dst *bool) BatchItem

DynExistsInto stores whether the dynamic query returns any row in dst: SELECT EXISTS (query), which stops at the first row. The query's scope, tenancy and interceptors apply as they do to its other terminals.

func DynInto

func DynInto(q *DynQuery, dst **DynResult) BatchItem

DynInto reads the rows of a dynamic query into dst, as Run does, as one item of a Batch: a report and its row count or dynamic pivot values in one round trip.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")
	q.Select(q.Path("status"), keel.DynCount().As("loads")).Group(q.Path("status"))

	statuses := reportTables.Query("shipments")
	statuses.Select(statuses.Path("status")).Distinct()

	var report, pivot *keel.DynResult
	var rows int64
	err := keel.Batch(ctx, db, keel.DynInto(q, &report), keel.DynInto(statuses, &pivot),
		keel.DynCountInto(q, &rows))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(report.Rows, pivot.Rows, rows)
}

func DynPageInto

func DynPageInto(q *DynQuery, p PageRequest, dst **DynResult) BatchItem

DynPageInto reads one keyset page of the dynamic query into dst, as Page does. A request with Total also counts the query's rows, and with Estimate reads the planner's estimate past the limit, in the same round trip.

func ExistsInto

func ExistsInto[T any](q *Query[T], dst *bool) BatchItem

ExistsInto stores whether any row matches in dst, as Exists does.

func FirstInto

func FirstInto[T any](q *Query[T], dst *T) BatchItem

FirstInto reads the first row in the query's order into dst, as First does: the batch fails with ErrNotFound when no row matches.

func IntoSlice

func IntoSlice[R any](p Projection, dst *[]R) BatchItem

IntoSlice decodes the rows of a projection into dst, as Into does.

func IntoValue

func IntoValue[R any](p Projection, dst *R) BatchItem

IntoValue decodes the only row of a projection into dst, as IntoOne does: the batch fails with ErrNotFound when the projection returns no row and ErrNotSingular when it returns more.

func ListInto

func ListInto[T any](q *Query[T], dst *[]T) BatchItem

ListInto reads the query's rows into dst, as List does. The query must have a Limit.

func OneInto

func OneInto[T any](q *Query[T], dst *T) BatchItem

OneInto reads the only matching row into dst, as One does: the batch fails with ErrNotFound when none matches and ErrNotSingular when more do.

func PageInto

func PageInto[T any](q *Query[T], p PageRequest, dst *Page[T]) BatchItem

PageInto reads one keyset page into dst, as Page does; a requested total (and its estimate) is counted in the same round trip.

type BoolArrayCase

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

BoolArrayCase builds a CASE whose result is boolean[]. Start it with CaseBoolArray.

func CaseBoolArray

func CaseBoolArray() BoolArrayCase

CaseBoolArray starts a CASE whose result is boolean[].

func (BoolArrayCase) Else

Else ends the CASE with ELSE v.

func (BoolArrayCase) End

func (b BoolArrayCase) End() BoolArrayExpr

End ends the CASE without ELSE.

func (BoolArrayCase) When

When adds WHEN cond THEN v.

type BoolArrayExpr

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

BoolArrayExpr is a computed boolean[] value, with the methods of StringArrayExpr.

func ArrayOfBool

func ArrayOfBool(es ...BoolValue) BoolArrayExpr

ArrayOfBool is ARRAY[es…] of boolean values.

func BoolArray

func BoolArray(vs []bool) BoolArrayExpr

BoolArray binds vs as $n::boolean[].

func BoolArrayOf

func BoolArrayOf(e Expr) BoolArrayExpr

BoolArrayOf types e as boolean[].

func (BoolArrayExpr) At

func (e BoolArrayExpr) At(i int) BoolExpr

At is (e)[i], the i-th element from 1.

func (BoolArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (BoolArrayExpr) ContainedByExpr

func (e BoolArrayExpr) ContainedByExpr(o BoolArrayValue) Pred

ContainedByExpr is (e <@ o).

func (BoolArrayExpr) ContainsAllExpr

func (e BoolArrayExpr) ContainsAllExpr(o BoolArrayValue) Pred

ContainsAllExpr is (e @> o).

func (BoolArrayExpr) IsNotNull

func (e BoolArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (BoolArrayExpr) IsNull

func (e BoolArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (BoolArrayExpr) Len

func (e BoolArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (BoolArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.BoolArray(nil)) turns an empty array into NULL.

func (BoolArrayExpr) OverlapsExpr

func (e BoolArrayExpr) OverlapsExpr(o BoolArrayValue) Pred

OverlapsExpr is (e && o).

func (BoolArrayExpr) Unnest

func (e BoolArrayExpr) Unnest() BoolExpr

Unnest is the set-returning unnest(e), for keel.Rows.

func (BoolArrayExpr) Without

func (e BoolArrayExpr) Without(v BoolValue) BoolArrayExpr

Without is array_remove(e, v).

type BoolArrayValue

type BoolArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

BoolArrayValue is a boolean[] value: a BoolArrayExpr.

type BoolCase

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

BoolCase builds a CASE whose result is boolean, itself a predicate. Start it with CaseBool.

func CaseBool

func CaseBool() BoolCase

CaseBool starts a CASE whose result is boolean.

func (BoolCase) Else

func (b BoolCase) Else(v BoolValue) BoolExpr

Else ends the CASE with ELSE v.

func (BoolCase) End

func (b BoolCase) End() BoolExpr

End ends the CASE without ELSE.

func (BoolCase) When

func (b BoolCase) When(cond Pred, v BoolValue) BoolCase

When adds WHEN cond THEN v.

type BoolCol

type BoolCol struct{ Col }

BoolCol is a boolean column.

func (BoolCol) Coalesce

func (c BoolCol) Coalesce(vs ...any) BoolExpr

Coalesce is coalesce(col, vs…), typed as boolean.

func (BoolCol) Eq

func (c BoolCol) Eq(v bool) Pred

func (BoolCol) IsDistinctFrom

func (c BoolCol) IsDistinctFrom(v bool) Pred

IsDistinctFrom is col IS DISTINCT FROM v.

func (BoolCol) IsDistinctFromExpr

func (c BoolCol) IsDistinctFromExpr(o BoolValue) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (BoolCol) IsFalse

func (c BoolCol) IsFalse() Pred

func (BoolCol) IsNotDistinctFrom

func (c BoolCol) IsNotDistinctFrom(v bool) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (BoolCol) IsNotDistinctFromExpr

func (c BoolCol) IsNotDistinctFromExpr(o BoolValue) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (BoolCol) IsTrue

func (c BoolCol) IsTrue() Pred

func (BoolCol) To

func (c BoolCol) To(v bool) Assignment

type BoolExpr

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

BoolExpr is a computed boolean value. It is a predicate, so Where and the combinators take it.

func Bool

func Bool(b bool) BoolExpr

Bool binds b as a boolean value, $n::boolean.

func BoolOf

func BoolOf(e Expr) BoolExpr

BoolOf types e as boolean without changing the SQL.

func BoolPtr

func BoolPtr(p *bool) BoolExpr

BoolPtr binds *p as $n::boolean, NULL when p is nil.

func CastBool

func CastBool(e Expr) BoolExpr

CastBool is CAST(e AS boolean): text 'true', 't', 'yes', 'on', '1' and their opposites, or an integer.

func ConstBool

func ConstBool(b bool) BoolExpr

ConstBool writes TRUE or FALSE into the SQL.

func NullBool

func NullBool() BoolExpr

NullBool is NULL::boolean.

func (BoolExpr) Asc

func (e BoolExpr) Asc() Order

func (BoolExpr) Coalesce

func (e BoolExpr) Coalesce(vs ...any) BoolExpr

Coalesce is coalesce(e, vs…).

func (BoolExpr) Desc

func (e BoolExpr) Desc() Order

func (BoolExpr) Eq

func (e BoolExpr) Eq(v bool) Pred

func (BoolExpr) EqExpr

func (e BoolExpr) EqExpr(o BoolValue) Pred

func (BoolExpr) IsDistinctFrom

func (e BoolExpr) IsDistinctFrom(v bool) Pred

IsDistinctFrom is e IS DISTINCT FROM v.

func (BoolExpr) IsDistinctFromExpr

func (e BoolExpr) IsDistinctFromExpr(o BoolValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (BoolExpr) IsFalse

func (e BoolExpr) IsFalse() Pred

func (BoolExpr) IsNotDistinctFrom

func (e BoolExpr) IsNotDistinctFrom(v bool) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (BoolExpr) IsNotDistinctFromExpr

func (e BoolExpr) IsNotDistinctFromExpr(o BoolValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (BoolExpr) IsNotNull

func (e BoolExpr) IsNotNull() Pred

func (BoolExpr) IsNull

func (e BoolExpr) IsNull() Pred

func (BoolExpr) IsTrue

func (e BoolExpr) IsTrue() Pred

type BoolValue

type BoolValue interface {
	Expr
	// contains filtered or unexported methods
}

BoolValue is a boolean value: a BoolCol or a BoolExpr.

type BudgetError

type BudgetError struct {
	// Limit names the limit crossed: "round trips", "statements" or "repeats".
	Limit string
	// Max is the limit's value.
	Max int
	// Report is the budget's account when the error was made.
	Report BudgetReport
}

BudgetError is the error of the terminal whose round trip took a Budget past a limit, returned after its statements ran, and of every later round trip under that budget, refused before it is sent. A write outside a transaction has committed when it returns a BudgetError.

func (*BudgetError) Error

func (e *BudgetError) Error() string

func (*BudgetError) Is

func (e *BudgetError) Is(target error) bool

Is makes errors.Is(err, ErrBudgetExceeded) true.

type BudgetOption

type BudgetOption func(b *budget)

BudgetOption configures a Budget.

func MaxRepeats

func MaxRepeats(n int) BudgetOption

MaxRepeats limits how many round trips one statement text may be sent in to n: a text sent in more than n crosses it. MaxRepeats(1) allows no repeated text at all.

func MaxRoundTrips

func MaxRoundTrips(n int) BudgetOption

MaxRoundTrips limits the budget to n round trips.

func MaxStatements

func MaxStatements(n int) BudgetOption

MaxStatements limits the budget to n statements.

func OnExceeded

func OnExceeded(fn func(ctx context.Context, r BudgetReport)) BudgetOption

OnExceeded makes crossing a limit call fn, once per budget, with the context of the terminal that crossed it and the account at that moment, instead of failing the terminal: for logging N+1 reads in production while the work goes on.

type BudgetReport

type BudgetReport struct {
	// RoundTrips counts the round trips that carried keel's statements, with the BEGIN keel sends
	// on its own before a COPY; the COMMIT or ROLLBACK that ends a transaction is not counted.
	RoundTrips int
	// Statements counts the statements sent: queries, relation loads, writes, COPY, savepoints,
	// advisory locks and raw SQL, not BEGIN, the Config.OnBegin statements or COMMIT.
	Statements int
	// Repeats lists the statement texts sent in more than one round trip, the most repeated first.
	Repeats []Repeat
}

BudgetReport is the account of the work under a Budget.

func BudgetReportFrom

func BudgetReportFrom(ctx context.Context) (BudgetReport, bool)

BudgetReportFrom returns the account of the innermost budget on ctx, and false when ctx carries none.

type BytesCol

type BytesCol struct{ Col }

BytesCol is a bytea column.

func (BytesCol) Eq

func (c BytesCol) Eq(v []byte) Pred

func (BytesCol) To

func (c BytesCol) To(v []byte) Assignment

type CTETable

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

CTETable is a named subquery (WITH name AS (…)) that queries declare with With and then join, filter on or select from like a table.

recent := keel.CTE("recent", []string{"customer_id", "last_at"},
    s.Group(s.CustomerID).Pick(s.CustomerID, s.CreatedAt.Max()))
rows, err := c.With(recent).
    Join(recent, recent.Col("customer_id").EqCol(c.ID)).
    Where(recent.Col("last_at").Lt(cutoff)).
    All(ctx, db)

func CTE

func CTE(name string, columns []string, body subquery) *CTETable

CTE declares a common table expression. columns name the body's output columns, in order.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s, c := fleetdb.Shipments, fleetdb.Customers

	busy := keel.CTE("busy", []string{"customer_id", "loads"},
		s.Group(s.CustomerID).Pick(s.CustomerID, keel.Count()).Where(s.Status.Eq(fleet.StatusInTransit)))

	customers, err := c.With(busy).
		Join(busy, busy.Col("customer_id").EqCol(c.ID)).
		Where(busy.Col("loads").Gte(10)).
		Order(c.Name.Asc()).
		All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	type load struct {
		Customer ids.ID
		Loads    int64
	}
	top, err := keel.Into[load](ctx, db, busy.Query().With(busy).
		Pick(busy.Col("customer_id"), busy.Col("loads")).
		Order(busy.Col("loads").Desc()).
		Limit(5))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(customers), top)
}

func RecursiveCTE

func RecursiveCTE(name string, columns []string, base subquery) *CTETable

RecursiveCTE declares WITH RECURSIVE name (columns) AS (base UNION ALL step). Give it its step with Recurse, which can join the CTE itself to walk a hierarchy:

chain := keel.RecursiveCTE("chain", []string{"id", "depth"}, w.Pick(w.ID, keel.Raw("0")).Where(w.ID.Eq(root)))
sub := w.As("sub")
chain.Recurse(sub.Pick(sub.ID, keel.Raw("? + 1", chain.Col("depth"))).
    Join(chain, chain.Col("id").EqCol(sub.ManagerID)))
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	w := fleetdb.Workers

	chain := keel.RecursiveCTE("chain", []string{"id", "depth"},
		w.Pick(w.ID, keel.Raw("0")).Where(w.ID.Eq("wrk_dispatch_lead")))
	sub := w.As("sub")
	chain.Recurse(sub.
		Pick(sub.ID, keel.Raw("? + 1", chain.Col("depth"))).
		Join(chain, chain.Col("id").EqCol(sub.ManagerID)))

	type node struct {
		ID    ids.ID
		Depth int32
	}
	tree, err := keel.Into[node](ctx, db, chain.Query().With(chain).
		Pick(chain.Col("id"), chain.Col("depth")).
		Order(chain.Col("depth").Asc()))
	if err != nil {
		log.Fatal(err)
	}

	team, err := w.With(chain).Where(w.ID.InQuery(chain.Query().Pick(chain.Col("id")))).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(tree), len(team))
}

func Unnest

func Unnest(name string, columns []string, arrays ...any) *CTETable

Unnest declares a CTE over Go slices, one per column, read row by row: WITH name (columns) AS (SELECT * FROM unnest($1::t1[], $2::t2[], …)). Each slice binds as one array parameter, so the statement text does not depend on the number of rows. It is the source of a bulk update that sets every row its own values, in one statement:

out := keel.Unnest("outcome", []string{"id", "status"}, ids, statuses)
n, err := t.With(out).Join(out, out.Col("id").EqCol(t.ID)).
    Update(ctx, db, t.Status.ToExpr(out.Col("status")))

Slices hold strings, integers, floats, bools, times or byte slices, or named types of those; they must be of equal length.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Contacts
	ids := []ids.ID{"con_01", "con_02"}
	phones := []string{"555-0101", "555-0102"}
	scores := []int64{10, 20}
	out := keel.Unnest("outcome", []string{"id", "phone", "score"}, ids, phones, scores)
	n, err := c.With(out).
		Join(out, out.Col("id").EqCol(c.ID)).
		Where(c.VIP.IsFalse()).
		Update(ctx, db, c.Phone.ToExpr(out.Col("phone")), c.Score.ToExpr(out.Col("score")))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}

func (*CTETable) Col

func (c *CTETable) Col(name string) AnyCol

Col references one of the CTE's columns.

func (*CTETable) Distinct

func (c *CTETable) Distinct() *CTETable

Distinct makes the recursion UNION instead of UNION ALL, so repeated rows end it.

func (*CTETable) Query

func (c *CTETable) Query() *Query[*Row]

Query starts a query over the CTE's rows. Pick what to read and run it with Into.

func (*CTETable) Recurse

func (c *CTETable) Recurse(step subquery) *CTETable

Recurse sets the recursive step: base UNION ALL step.

func (*CTETable) TableRef

func (c *CTETable) TableRef() *Ref

TableRef returns the CTE's table reference, so Join accepts it.

type CacheConfig

type CacheConfig struct {
	// Local is the in-process store; nil keeps a MemoryCache of DefaultCacheBytes.
	Local LocalCache
	// Shared, when set, is a store every process shares, such as Redis (keel/cache/rediscache). A
	// local miss reads it, and an entry read while this process had no unflushed writes to its
	// tables is written to it.
	Shared SharedCache
	// Key returns key material for what the scope does not capture but the rows depend on, such
	// as the row-level security settings Config.OnBegin sets (Trenova returns its encoded read
	// scope). It is required when Config.OnBegin is set. It runs on the terminal's context without
	// its transaction and with its tenant slot: a statement it sends runs in a transaction of its
	// own (on a second pool connection when the caller holds one), never joins or begins the one
	// the terminal reads in, and shares the terminal's slot rather than waiting for another.
	Key func(ctx context.Context) string
	// MaxEntryBytes caps an encoded entry; a larger result is not cached. Default 1 MiB.
	MaxEntryBytes int
	// OnError receives the errors of the shared store, the listener and the decoders; the cache
	// treats each as a miss or a bypass.
	OnError func(err error)
	// Listen is the connection the listener LISTENs for data versions on; nil uses the primary
	// pool's settings (and its BeforeConnect). LISTEN needs a session, so behind a
	// transaction-mode pooler (PgBouncer) point it at Postgres directly. Build it with
	// pgx.ParseConfig; it is used as given, without the pool's BeforeConnect.
	Listen *pgx.ConnConfig
}

CacheConfig configures the result cache (Config.Cache): query results kept by the statements that read them and served while the data versions of every table they read are unchanged, so a hit costs no round trip. It needs Config.DataVersions.Enabled.

type CacheOption

type CacheOption func(*cacheSpec)

CacheOption configures Query.Cache.

func AlsoReads

func AlsoReads(tables ...string) CacheOption

AlsoReads names tables the query reads that keel cannot see, such as a table only a keel.Raw fragment reads, so a change to them invalidates its cached results too.

func TTL

func TTL(d time.Duration) CacheOption

TTL keeps a cached result for at most d. Validity never depends on it: an entry stops being served as soon as a table it read changes. It bounds how long an unused entry holds memory. The default is one minute.

type CacheStats

type CacheStats struct {
	// Hits are terminals served from the cache, Misses terminals that asked for it and read the
	// database, Bypasses terminals that asked for it where it does not apply.
	Hits, Misses, Bypasses int64
	// Errors counts the failures reported to CacheConfig.OnError.
	Errors int64
	// Bytes is what the local store holds.
	Bytes int64
	// Versions is the number of (tenant, table) pairs whose versions the process holds.
	Versions int
	// Listening reports that the listener is connected; while it is not, the cache is bypassed.
	Listening bool
}

CacheStats counts the result cache's traffic and contents.

type Col

type Col struct {
	Ref   *Ref
	Index int32
}

Col references one column of a table. Generated code builds every column statically; the typed column kinds embed it.

func (Col) Asc

func (c Col) Asc() Order

Asc orders by the column ascending.

func (Col) Coalesce

func (c Col) Coalesce(vs ...any) Expr

Coalesce is coalesce(col, vs…): the first of col and vs that is not NULL, each v an expression or a Go value bound as a parameter of col's type.

func (Col) Count

func (c Col) Count() Agg

Count is count(col).

func (Col) CountDistinct

func (c Col) CountDistinct() Agg

CountDistinct is count(DISTINCT col).

func (Col) Desc

func (c Col) Desc() Order

Desc orders by the column descending.

func (Col) EqCol

func (c Col) EqCol(other Expr) Pred

EqCol is col = other, for joins and correlated subqueries.

func (Col) Excluded

func (c Col) Excluded() Expr

Excluded is the value the upsert proposed for the column, in the row that conflicted (EXCLUDED.col). It is valid only in a Conflict.Set assignment; anywhere else the statement fails.

func (Col) InQuery

func (c Col) InQuery(q subquery) Pred

InQuery is col IN (q). q must pick exactly one column.

func (Col) IsNotNull

func (c Col) IsNotNull() Pred

IsNotNull is col IS NOT NULL.

func (Col) IsNull

func (c Col) IsNull() Pred

IsNull is col IS NULL.

func (Col) Max

func (c Col) Max() Agg

Max is max(col).

func (Col) Min

func (c Col) Min() Agg

Min is min(col).

func (Col) NeCol

func (c Col) NeCol(other Expr) Pred

NeCol is col <> other.

func (Col) NotInQuery

func (c Col) NotInQuery(q subquery) Pred

NotInQuery is col NOT IN (q).

func (Col) ToDefault

func (c Col) ToDefault() Assignment

ToDefault sets the column to its database default.

func (Col) ToExpr

func (c Col) ToExpr(e Expr) Assignment

ToExpr sets the column to an expression.

When the column's tag declares its SQL type (sqltype=), e is cast to it, so a text expression can be assigned to an enum column.

func (Col) ToNull

func (c Col) ToNull() Assignment

ToNull sets the column to NULL.

type Column

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

Column is any generated column handle (StringCol, Int64Col, a generated enum column, ...). Save, SaveExcept, Omit, Compute, Conflict.Update and Needs take columns as Column, so a caller can build the list at run time: cols := []keel.Column{t.Status, t.Note}; t.Save(ctx, db, row, cols...).

type Compound

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

Compound is a UNION, UNION ALL, INTERSECT or EXCEPT of queries that pick the same number of expressions. Use it as a CTE body, a subquery, or read it with Into.

func Except

func Except(qs ...subquery) Compound

Except is q1 EXCEPT q2 ….

func Intersect

func Intersect(qs ...subquery) Compound

Intersect is q1 INTERSECT q2 ….

func Union

func Union(qs ...subquery) Compound

Union is q1 UNION q2 …: the distinct rows of every query.

func UnionAll

func UnionAll(qs ...subquery) Compound

UnionAll is q1 UNION ALL q2 …: every row of every query.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s, c := fleetdb.Shipments, fleetdb.Customers

	codes, err := keel.Into[string](ctx, db, keel.UnionAll(
		s.Pick(s.ProNumber).Where(s.Hazmat.IsTrue()),
		c.Pick(c.Code).Where(c.Email.IsNull()),
	))
	if err != nil {
		log.Fatal(err)
	}

	flagged := keel.CTE("flagged", []string{"customer_id"}, keel.Union(
		s.Pick(s.CustomerID).Where(s.Hazmat.IsTrue()),
		s.Pick(s.CustomerID).Where(s.Status.Eq(fleet.StatusCanceled)),
	))
	n, err := c.With(flagged).
		Where(c.ID.InQuery(flagged.Query().Pick(flagged.Col("customer_id")))).
		Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(codes), n)
}

type Config

type Config struct {
	// DSN is the connection string. Pool, when set, is used instead.
	DSN  string
	Pool *pgxpool.Config

	// TenantColumns names the tenant columns, in the order Tenant returns their values.
	TenantColumns []string
	// Tenant returns the tenant of the context.
	Tenant func(ctx context.Context) ([]any, bool)
	// System reports whether the context carries a system scope, and its reason.
	System func(ctx context.Context) (reason string, ok bool)

	// OnBegin returns statements to run at the start of every transaction, explicit or implicit,
	// for example a row-level security set_config. When it is set, terminals outside a transaction
	// run in an implicit one. It is called when the transaction begins, with its first statement,
	// whichever terminal or callback sends it, on the context the transaction was opened with,
	// without that transaction and with its tenant slot: a statement it sends runs in a transaction
	// of its own on a second pool connection, which calls OnBegin again, and shares the slot rather
	// than waiting for another.
	OnBegin func(ctx context.Context, tx TxInfo) ([]Statement, error)

	// NewID returns a new primary key for autoid columns.
	NewID func(prefix string) string
	// Now returns the time stamped into autocreate and autoupdate columns. Defaults to time.Now.
	Now func() time.Time

	// Observers see every statement.
	Observers []Observer
	// ObserveArgs hands observers each statement's bound values (QueryEvent.Values). The values
	// are the data itself, so turn it on only where a query log may hold it, in development.
	ObserveArgs bool

	// Tables are the generated tables (keeltables.All()) that constraint violations resolve
	// against: a *ConstraintError of a listed table carries the model's field names, labels and
	// the message its Constraints method declares. Open reads their constraints' columns from the
	// catalog in one query, so resolving a violation never waits for a connection.
	Tables []*schema.Table

	// AfterConnect runs on every new connection, after keel registers its types; use it to
	// register codecs (a decimal type, a faster JSON codec).
	AfterConnect func(ctx context.Context, conn *pgx.Conn) error
	// LoadTypes lists user-defined types (domains, composites) and their array types to register
	// on every connection, through pgx's LoadTypes, after the enums. Enums are registered without
	// being listed.
	LoadTypes []string
	// SkipEnums turns off registering every enum type of the database on each new connection.
	// keel reads them all from the catalog in one query, however many there are. Registered enums
	// bind as arrays and decode into one shared string per label, so enum columns cost no
	// allocation per row.
	SkipEnums bool
	// JSON encodes and decodes json and jsonb values. Each function left nil defaults to sonic in
	// its encoding/json-compatible mode, which encodes a value to the bytes encoding/json would;
	// set sonic.ConfigDefault's functions, which skip HTML escaping and map key sorting, for more
	// speed. Its UnmarshalNumber decodes the columns tagged jsonnumber.
	JSON *JSONCodec
	// PlanCache chooses how statements are prepared. The default, PlanCacheAuto, prepares every
	// statement shape once per connection.
	PlanCache PlanCache
	// StatementCacheCapacity is how many prepared statements (or, with PlanCacheOff, statement
	// descriptions) each connection keeps. Defaults to 512; the least recently used is
	// deallocated beyond it.
	StatementCacheCapacity int

	// Replicas are streaming replicas of the primary. Reads run on one only when their context is
	// marked with OnReplica, or inside a Read started with ReplicaTx. Every replica pool gets the
	// same connection setup as the primary's.
	Replicas []ReplicaConfig
	// MaxReplicaLag is how far behind the primary a replica may be and still serve reads.
	// Defaults to 2s.
	MaxReplicaLag time.Duration

	// DataVersions turns on the per-tenant, per-table change counters keel maintains from its own
	// write path.
	DataVersions DataVersionsConfig
	// Cache configures the result cache; nil turns it off.
	Cache *CacheConfig

	// ReadIsolation is the isolation level of explicit Read transactions that pass none of their
	// own. Zero leaves it to the server's default (READ COMMITTED unless the server says
	// otherwise); RepeatableRead gives every statement of a Read one snapshot. A ReplicaTx Read
	// runs REPEATABLE READ in place of Serializable, which a hot standby refuses.
	ReadIsolation Isolation
	// AwaitReadCommit makes read-only transactions wait for the reply to their COMMIT or ROLLBACK,
	// as writes do. By default keel sends it and returns, and a goroutine reads the reply before the
	// connection goes back to the pool.
	AwaitReadCommit bool
	// DeadlineTimeouts makes every transaction keel begins, explicit or implicit, set
	// statement_timeout to the time left before its context's deadline, in its BEGIN round trip,
	// so the server stops a statement the caller no longer waits for even when the cancel request
	// does not reach it. Statements outside a transaction rely on the context's cancellation alone.
	DeadlineTimeouts bool
	// TenantConcurrency bounds how many terminals and transactions of one tenant run at once on
	// the DB; zero turns the bound off. Work past it waits for a slot or for its context. A
	// transaction holds one slot from its start to its end and its statements take none; work
	// started under a context that carries a slot of the same tenant (a Detached transaction, a
	// write's hooks) shares it. System scopes and unscoped work take none. DB.TenantWaits counts
	// the waits.
	TenantConcurrency int
	// SlowWaits reports work that waits too long for a tenant slot, a pool connection or another
	// goroutine's read of a result cache entry (see SlowWaitConfig). Off unless both of its fields
	// are set.
	SlowWaits SlowWaitConfig
	// MaxWait bounds each wait for a tenant slot, a pool connection or another goroutine's read
	// of a result cache entry, whatever the context's deadline; zero (the default) leaves waits
	// to the context. A slot or connection wait that reaches it fails with ErrWaitLimit, and a
	// cache wait stops waiting and reads the database itself, so work without a deadline (a
	// background job) cannot wait forever behind a leaked slot or a stuck request.
	MaxWait time.Duration

	// LoadStrategy is how a read loads its to-many relations when the query does not choose
	// (Query.LoadStrategy). The default, LoadPipelined, sends the root statement and every level in
	// one round trip where every level can read one snapshot; LoadPerLevel sends a round trip per level.
	LoadStrategy LoadStrategy
}

Config configures a DB.

type Conflict

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

Conflict is the ON CONFLICT clause of an upsert. Its methods return a changed copy.

func OnConflict

func OnConflict(targets ...Expr) Conflict

OnConflict names what an upsert conflicts on, as a unique index declares it: columns of the table, or expressions for an index on expressions, such as keel.Raw("md5(?)", t.Endpoint) for ON CONFLICT (md5(endpoint)). Expressions must not bind values; columns in them are written without an alias, as the index names them.

func (Conflict) DoNothing

func (c Conflict) DoNothing() Conflict

DoNothing skips a conflicting row instead of updating it: ON CONFLICT … DO NOTHING.

func (Conflict) Set

func (c Conflict) Set(sets ...Assignment) Conflict

Set adds assignments for a conflicting row, built as for Query.Update: To binds a value, Increment adds to the stored value, ToNull, ToDefault and ToExpr set the rest. Inside an expression the table's columns read the stored row and a column's Excluded reads the proposed row:

keel.OnConflict(c.Code).Set(c.Name.ToExpr(c.Name.Excluded()), c.Hits.Increment(1))

Update and Set combine; a column must not be assigned twice. Unless Set assigns them, the autoupdate column takes the proposed row's stamp and the version column is incremented.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Customers
	rows := []*fleet.Customer{{Code: "ACME", Name: "Acme Freight"}}
	_, err := c.Upsert(ctx, db, rows,
		keel.OnConflict(c.OrganizationID, c.BusinessUnitID, c.Code).Set(
			c.Name.ToExpr(c.Name.Excluded()),
			c.Email.ToExpr(keel.Raw("coalesce(?, ?)", c.Email, c.Email.Excluded())),
			c.Version.Increment(10),
		))
	if err != nil {
		log.Fatal(err)
	}
}

func (Conflict) Update

func (c Conflict) Update(cols ...Column) Conflict

Update names the columns a conflicting row takes from the proposed row: SET col = EXCLUDED.col. Without Update or Set, a conflicting row takes every updatable column.

func (Conflict) UpdateWhere

func (c Conflict) UpdateWhere(ps ...Pred) Conflict

UpdateWhere updates a conflicting row only when ps hold: ON CONFLICT … DO UPDATE SET … WHERE …. The table's columns read the stored row and a column's Excluded the proposed row; values bind as anywhere else. A row the condition rejects is neither inserted nor updated, so UpsertFrom does not count it, which makes a capped counter one statement:

keel.OnConflict(u.Day).Set(u.Requests.Increment(1)).UpdateWhere(u.Requests.Lt(limit))

Upsert refuses it, since it reports every row as inserted or updated.

func (Conflict) Where

func (c Conflict) Where(ps ...Pred) Conflict

Where matches a partial unique index: ON CONFLICT (…) WHERE …. The predicates must not bind values, since Postgres matches them against the index predicate when it plans the statement.

type ConflictError

type ConflictError struct {
	Table   string
	Version int64
}

ConflictError reports a Save whose version no longer matched the stored row.

func (*ConflictError) Error

func (e *ConflictError) Error() string

func (*ConflictError) Is

func (e *ConflictError) Is(target error) bool

Is makes errors.Is(err, ErrConflict) true.

type Constraint

type Constraint = schema.Constraint

Constraint names a constraint of a model's table and the message its violation reports. A model declares the ones whose message matters with a Constraints method, which keel gen records in the generated table and keel.Verify checks against the database:

func (Location) Constraints() []keel.Constraint {
	return []keel.Constraint{
		{Name: "idx_locations_code", Message: "A location with this code already exists"},
	}
}

type ConstraintError

type ConstraintError struct {
	Kind ConstraintKind
	// Table is the violated table's name, and Model its generated schema when Config.Tables lists
	// it, else nil.
	Table string
	Model *schema.Table
	// Constraint is the violated constraint's name; empty for a NOT NULL violation.
	Constraint string
	// Columns are the constraint's columns, without the tenant columns: its key columns in order,
	// then the columns its index expressions and predicate read (lower(code), WHERE is_default).
	// Fields are their JSON names on the model and Labels their labels, empty when the model is
	// unknown; a Field the model declares for the constraint replaces them.
	Columns []string
	Fields  []string
	Labels  []string
	// Message is the message the model declares for the constraint, or "".
	Message string
	Err     *pgconn.PgError
}

ConstraintError is a statement's violation of a constraint, resolved against the model of the table: every keel statement returns one for a unique, foreign key, check, not-null or exclusion violation, wrapping the Postgres error. Read it with errors.As.

func (*ConstraintError) Error

func (e *ConstraintError) Error() string

func (*ConstraintError) Unwrap

func (e *ConstraintError) Unwrap() error

Unwrap returns the Postgres error, so Code, IsUnique and the other helpers see it.

type ConstraintKind

type ConstraintKind uint8

ConstraintKind is the kind of constraint a statement violated.

const (
	// ConstraintUnique is a unique constraint or unique index (23505).
	ConstraintUnique ConstraintKind = iota + 1
	// ConstraintForeignKey is a foreign key (23503).
	ConstraintForeignKey
	// ConstraintCheck is a check constraint (23514).
	ConstraintCheck
	// ConstraintNotNull is a NOT NULL column (23502).
	ConstraintNotNull
	// ConstraintExclusion is an exclusion constraint (23P01).
	ConstraintExclusion
)

func (ConstraintKind) String

func (k ConstraintKind) String() string

String returns the kind's name.

type DB

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

DB is a pool of connections and the configuration keel applies to them.

func Open

func Open(ctx context.Context, cfg Config) (*DB, error)

Open connects to Postgres. With Config.Replicas it also opens a pool per replica and starts a goroutine that tracks their lag; with Config.DataVersions.Enabled it starts the goroutine that writes data versions. Close stops both.

Example
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log"
	"os"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

// slowQueryLog is an Observer that logs statements slower than threshold.
type slowQueryLog struct{ threshold time.Duration }

func (slowQueryLog) BeforeQuery(ctx context.Context, _ *keel.QueryEvent) context.Context {
	return ctx
}

func (l slowQueryLog) AfterQuery(_ context.Context, e *keel.QueryEvent) {
	if e.Duration >= l.threshold {
		log.Printf("slow %s on %s (%d args, %d rows): %v: %s",
			e.Op, e.Table, e.Args, e.Rows, e.Duration, e.SQL)
	}
}

// appConfig is the configuration of a multi-tenant application.
func appConfig(dsn string) keel.Config {
	return keel.Config{
		DSN:           dsn,
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			if !ok || sc.SystemReason != "" {
				return nil, false
			}
			return []any{sc.OrganizationID, sc.BusinessUnitID}, true
		},
		System: func(ctx context.Context) (string, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			return sc.SystemReason, ok && sc.SystemReason != ""
		},
		OnBegin: func(ctx context.Context, tx keel.TxInfo) ([]keel.Statement, error) {
			sc, _ := ctx.Value(scopeKey{}).(scope)
			return []keel.Statement{{
				SQL:  "SELECT set_config('app.organization_id', $1, true)",
				Args: []any{string(sc.OrganizationID)},
			}}, nil
		},
		NewID:     newID,
		Observers: []keel.Observer{slowQueryLog{threshold: 200 * time.Millisecond}},
	}
}

func main() {
	ctx := context.Background()
	db, err := keel.Open(ctx, appConfig(os.Getenv("DATABASE_URL")))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	if err := db.Ping(ctx); err != nil {
		log.Fatal(err)
	}
}
Example (DataVersions)
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log"
	"os"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

// slowQueryLog is an Observer that logs statements slower than threshold.
type slowQueryLog struct{ threshold time.Duration }

func (slowQueryLog) BeforeQuery(ctx context.Context, _ *keel.QueryEvent) context.Context {
	return ctx
}

func (l slowQueryLog) AfterQuery(_ context.Context, e *keel.QueryEvent) {
	if e.Duration >= l.threshold {
		log.Printf("slow %s on %s (%d args, %d rows): %v: %s",
			e.Op, e.Table, e.Args, e.Rows, e.Duration, e.SQL)
	}
}

// appConfig is the configuration of a multi-tenant application.
func appConfig(dsn string) keel.Config {
	return keel.Config{
		DSN:           dsn,
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			if !ok || sc.SystemReason != "" {
				return nil, false
			}
			return []any{sc.OrganizationID, sc.BusinessUnitID}, true
		},
		System: func(ctx context.Context) (string, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			return sc.SystemReason, ok && sc.SystemReason != ""
		},
		OnBegin: func(ctx context.Context, tx keel.TxInfo) ([]keel.Statement, error) {
			sc, _ := ctx.Value(scopeKey{}).(scope)
			return []keel.Statement{{
				SQL:  "SELECT set_config('app.organization_id', $1, true)",
				Args: []any{string(sc.OrganizationID)},
			}}, nil
		},
		NewID:     newID,
		Observers: []keel.Observer{slowQueryLog{threshold: 200 * time.Millisecond}},
	}
}

func main() {
	ctx := context.Background()
	cfg := appConfig(os.Getenv("DATABASE_URL"))
	cfg.DataVersions = keel.DataVersionsConfig{
		Enabled:  true,
		Interval: time.Second,
		OnError:  func(err error) { log.Printf("data versions: %v", err) },
	}
	db, err := keel.Open(ctx, cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}
Example (Pool)
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log"
	"os"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
	"github.com/jackc/pgx/v5/pgxpool"
)

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

// slowQueryLog is an Observer that logs statements slower than threshold.
type slowQueryLog struct{ threshold time.Duration }

func (slowQueryLog) BeforeQuery(ctx context.Context, _ *keel.QueryEvent) context.Context {
	return ctx
}

func (l slowQueryLog) AfterQuery(_ context.Context, e *keel.QueryEvent) {
	if e.Duration >= l.threshold {
		log.Printf("slow %s on %s (%d args, %d rows): %v: %s",
			e.Op, e.Table, e.Args, e.Rows, e.Duration, e.SQL)
	}
}

// appConfig is the configuration of a multi-tenant application.
func appConfig(dsn string) keel.Config {
	return keel.Config{
		DSN:           dsn,
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			if !ok || sc.SystemReason != "" {
				return nil, false
			}
			return []any{sc.OrganizationID, sc.BusinessUnitID}, true
		},
		System: func(ctx context.Context) (string, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			return sc.SystemReason, ok && sc.SystemReason != ""
		},
		OnBegin: func(ctx context.Context, tx keel.TxInfo) ([]keel.Statement, error) {
			sc, _ := ctx.Value(scopeKey{}).(scope)
			return []keel.Statement{{
				SQL:  "SELECT set_config('app.organization_id', $1, true)",
				Args: []any{string(sc.OrganizationID)},
			}}, nil
		},
		NewID:     newID,
		Observers: []keel.Observer{slowQueryLog{threshold: 200 * time.Millisecond}},
	}
}

func main() {
	ctx := context.Background()
	pc, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	pc.MaxConns = 32
	pc.MaxConnIdleTime = 5 * time.Minute

	cfg := appConfig("")
	cfg.Pool = pc
	cfg.PlanCache = keel.PlanCacheCustom
	cfg.StatementCacheCapacity = 1024
	db, err := keel.Open(ctx, cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}
Example (Replicas)
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

// slowQueryLog is an Observer that logs statements slower than threshold.
type slowQueryLog struct{ threshold time.Duration }

func (slowQueryLog) BeforeQuery(ctx context.Context, _ *keel.QueryEvent) context.Context {
	return ctx
}

func (l slowQueryLog) AfterQuery(_ context.Context, e *keel.QueryEvent) {
	if e.Duration >= l.threshold {
		log.Printf("slow %s on %s (%d args, %d rows): %v: %s",
			e.Op, e.Table, e.Args, e.Rows, e.Duration, e.SQL)
	}
}

// appConfig is the configuration of a multi-tenant application.
func appConfig(dsn string) keel.Config {
	return keel.Config{
		DSN:           dsn,
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			if !ok || sc.SystemReason != "" {
				return nil, false
			}
			return []any{sc.OrganizationID, sc.BusinessUnitID}, true
		},
		System: func(ctx context.Context) (string, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			return sc.SystemReason, ok && sc.SystemReason != ""
		},
		OnBegin: func(ctx context.Context, tx keel.TxInfo) ([]keel.Statement, error) {
			sc, _ := ctx.Value(scopeKey{}).(scope)
			return []keel.Statement{{
				SQL:  "SELECT set_config('app.organization_id', $1, true)",
				Args: []any{string(sc.OrganizationID)},
			}}, nil
		},
		NewID:     newID,
		Observers: []keel.Observer{slowQueryLog{threshold: 200 * time.Millisecond}},
	}
}

func main() {
	ctx := context.Background()
	cfg := appConfig(os.Getenv("DATABASE_URL"))
	cfg.Replicas = []keel.ReplicaConfig{
		{DSN: os.Getenv("REPLICA_A_URL")},
		{DSN: os.Getenv("REPLICA_B_URL"), Weight: 2},
	}
	cfg.MaxReplicaLag = 2 * time.Second
	db, err := keel.Open(ctx, cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	for _, r := range db.Replicas() {
		fmt.Println(r.Name, r.Healthy, r.Lag, r.Position, r.InFlight, r.Reads, r.Err)
	}
	fmt.Println("replica reads that ran on the primary:", db.ReplicaFallbacks())
}

func (*DB) CacheStats

func (db *DB) CacheStats() CacheStats

CacheStats returns the result cache's counters; all zero without Config.Cache.

func (*DB) Close

func (db *DB) Close()

Close stops the replica lag tracker, writes the pending data versions, waits for the replies to read-only COMMITs keel did not wait for, and closes every connection, the replicas' included. Calls after the first do nothing.

func (*DB) DataVersions

func (db *DB) DataVersions(ctx context.Context, tenantKey string, tables ...string) (map[string]int64, error)

DataVersions returns the current version of each table for the tenant with key tenantKey (see TenantKey), in one query; a table never changed has version 0. A table's version also moves with the changes recorded for no tenant ("") and for each shorter prefix of the key, such as a table that holds only the first tenant column, so it changes whenever data the tenant can read in it changes. Versions are written at most Config.DataVersions.Interval after the write commits.

It reads the primary, joining the transaction on ctx. On a context marked with OnReplica it also raises the context's commit position to the primary's current one, in the same round trip, so data read from a replica afterwards is at least as new as the versions.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	tenant := db.TenantKey(ctx)

	versions, err := db.DataVersions(ctx, tenant, "shipments", "customers")
	if err != nil {
		log.Fatal(err)
	}
	cacheKey := fmt.Sprintf("open-shipments:%s:%d:%d", tenant, versions["shipments"], versions["customers"])
	fmt.Println(cacheKey)
}

func (*DB) FlushDataVersions

func (db *DB) FlushDataVersions(ctx context.Context) error

FlushDataVersions writes the pending data versions now instead of at the next interval, for tests and before a planned shutdown (Close also flushes). It does nothing when data versions are off.

func (*DB) ForgetColumnTypes

func (db *DB) ForgetColumnTypes()

ForgetColumnTypes drops the cached column types, so the next bulk write reads them again. Call it after a migration changes a table while the process runs.

func (*DB) Intercept

func (db *DB) Intercept(t interface{ TableRef() *Ref }, fn Interceptor)

Intercept registers fn for the statements whose base is t's table, as Interceptor describes: reads, Save, DeleteRow and bulk updates and deletes. Joined relations are not intercepted.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

type portalKey struct{}

func main() {
	db := appDB
	s := fleetdb.Shipments

	db.Intercept(s, func(ctx context.Context, q *keel.Intercepted) error {
		customers, ok := ctx.Value(portalKey{}).([]ids.ID)
		if !ok {
			return nil
		}
		if q.Op == keel.OpDelete {
			return errors.New("portal users cannot delete shipments")
		}
		q.Where(s.CustomerID.In(customers...))
		return nil
	})

	ctx := context.WithValue(requestContext(), portalKey{}, []ids.ID{"cus_01"})
	rows, err := s.Query().Limit(20).List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

func (*DB) InterceptAll

func (db *DB) InterceptAll(fn Interceptor)

InterceptAll registers fn for the statements of every table that Intercept would cover: reads, Save, DeleteRow and bulk updates and deletes, never inserts. It runs before the interceptors registered for one table.

func (*DB) JSON

func (db *DB) JSON() JSONCodec

JSON returns the codec db encodes and decodes json and jsonb values with: Config.JSON, with each function it leaves nil taken from the default, sonic in its encoding/json-compatible mode. Packages built on keel use it to read JSON the way the application configured.

func (*DB) Ping

func (db *DB) Ping(ctx context.Context) error

Ping checks that the database is reachable.

func (*DB) Pool

func (db *DB) Pool() *pgxpool.Pool

Pool returns the underlying pgx pool, for work keel does not cover (LISTEN, COPY of foreign data). Statements sent through it bypass scope checks and hooks.

func (*DB) ReadCommitFailures

func (db *DB) ReadCommitFailures() int64

ReadCommitFailures returns how many connections keel closed since Open because the reply to a read-only COMMIT or ROLLBACK it did not wait for failed.

func (*DB) ReplicaFallbacks

func (db *DB) ReplicaFallbacks() uint64

ReplicaFallbacks counts the reads that asked for a replica and ran on the primary because no replica was healthy, within the lag bound and past the context's commit position.

func (*DB) Replicas

func (db *DB) Replicas() []ReplicaStats

Replicas returns the state of db's replicas, in Config.Replicas order.

func (*DB) TenantColumns

func (db *DB) TenantColumns() []string

TenantColumns returns Config.TenantColumns: the tenant columns, in the order the values of a tenant (Config.Tenant) and of a tenant key (TenantKey) come in. The slice is a copy.

func (*DB) TenantKey

func (db *DB) TenantKey(ctx context.Context) string

TenantKey returns the data-versions tenant key of ctx's scope: TenantKeyOf the tenant values, or "" under a system scope or without a scope.

func (*DB) TenantWaits

func (db *DB) TenantWaits() int64

TenantWaits returns how many terminals and transactions have waited for a tenant slot (Config.TenantConcurrency) since Open.

type DataVersionsConfig

type DataVersionsConfig struct {
	// Enabled turns data versions on. They are off by default: each changed (tenant, table) pair
	// costs a row upsert per Interval.
	Enabled bool
	// Interval is how often pending pairs are written; a pair changed many times within it is
	// bumped once. Defaults to 1s.
	Interval time.Duration
	// OnError receives the errors of background writes; the pairs of a failed write are retried
	// at the next interval.
	OnError func(err error)
}

DataVersionsConfig configures data versions: a counter per tenant and table that keel bumps after a write to the table commits, for caches keyed on the data they read. keel keeps them in keel_data_versions, which it creates on first use under an advisory lock.

type Decimal

type Decimal string

Decimal is an exact decimal number in the text form Postgres writes for numeric: "-12.3400", "0.5", "NaN", "Infinity", "-Infinity". It keeps every digit and the scale, compares exactly as text after normalisation by the application, binds as a numeric parameter, and goes to CSV, JSON and spreadsheets without conversion.

func (Decimal) Float64

func (d Decimal) Float64() (float64, error)

Float64 returns the nearest float64, for charts and approximate arithmetic.

func (Decimal) String

func (d Decimal) String() string

String returns the decimal's text.

type DeclaredFunc

type DeclaredFunc interface {
	Decl() *FuncDecl
}

DeclaredFunc is a typed function declaration, accepted by VerifyFuncs: a Func0, Func1, Func2 or Func3.

type Drift

type Drift struct {
	Kind DriftKind `json:"kind"`
	// Table is the table the difference is in: the model's, a join table, or a relation's target;
	// for a function drift, the declared function's name.
	Table string `json:"table"`
	// Column is the column, empty for a whole table.
	Column string `json:"column,omitempty"`
	// Field is the model field or relation (Shipment.Weight), empty for a whole table.
	Field string `json:"field,omitempty"`
	// Detail says what differs.
	Detail string `json:"detail"`
	// Fix suggests how to make them agree.
	Fix string `json:"fix"`
}

Drift is one difference between a model and the live database.

func Verify

func Verify(ctx context.Context, db *DB, tables []*schema.Table) ([]Drift, error)

Verify compares the metadata of tables (keeltables.All(), say) with the live catalog and returns every difference that would make keel's statements fail or decode wrongly: missing tables, views and columns, primary keys that differ from the models' pk columns, nullable columns read into fields that cannot hold NULL, generated columns on fields keel writes, jsonb fields on columns that are not json, Go kinds that cannot hold the column's type, many-to-many join tables without their key or shared tenant columns, and relation keys that do not exist. Drifts come in the order of tables, then columns; none means the models and the database agree.

Verify reads the catalog in one statement on a pool connection of its own, outside any transaction and scope, resolving names through the connection's search_path as keel's statements do. Views and materialized views are checked for their columns and types only: Postgres records neither a primary key nor nullability for them. A noselect column is not checked for NULL, since it is read only when a query picks it. A field of a scanner or other custom type (schema.KindOther) is not type-checked, since its Scan method decides what it accepts.

func VerifyFuncs

func VerifyFuncs(ctx context.Context, db *DB, fns ...DeclaredFunc) ([]Drift, error)

VerifyFuncs checks typed function declarations against the live catalog: each declared function must exist as a plain function (not a procedure or an aggregate), resolved through the connection's search_path when its name carries no schema, with an overload that takes the declared number of arguments (counting defaults and a variadic last argument) and returns a type the declared result kind decodes (the rules Verify applies to columns; polymorphic results are not checked). It reads the catalog in one statement on a pool connection, outside any transaction and scope, and returns one Drift per mismatch in the order of fns; none means every call the declarations make resolves.

func (Drift) String

func (d Drift) String() string

String writes the drift on one line: table.column (Field): detail; fix.

type DriftKind

type DriftKind string

DriftKind names what a Drift found.

const (
	// DriftMissingTable is a model whose table or view does not exist.
	DriftMissingTable DriftKind = "missing_table"
	// DriftMissingColumn is a stored field whose column does not exist.
	DriftMissingColumn DriftKind = "missing_column"
	// DriftPrimaryKey is a model whose pk columns are not the table's primary key.
	DriftPrimaryKey DriftKind = "primary_key"
	// DriftNullable is a nullable column read into a field that cannot hold NULL.
	DriftNullable DriftKind = "nullable"
	// DriftGenerated is a generated or always-identity column on a field keel writes.
	DriftGenerated DriftKind = "generated"
	// DriftJSON is a jsonb field on a column that is neither json nor jsonb.
	DriftJSON DriftKind = "json"
	// DriftType is a field whose Go kind cannot hold the column's type.
	DriftType DriftKind = "type"
	// DriftJoinTable is a many-to-many join table that is missing, or lacks a key or tenant column.
	DriftJoinTable DriftKind = "join_table"
	// DriftRelationKey is a relation whose key column or target table does not exist.
	DriftRelationKey DriftKind = "relation_key"
	// DriftConstraint is a constraint a model's Constraints method names that the table lacks.
	DriftConstraint DriftKind = "constraint"
	// DriftMissingFunction is a declared database function (DeclareFunc0 … DeclareFunc3) that does
	// not exist, or exists only as a procedure, aggregate or window function.
	DriftMissingFunction DriftKind = "missing_function"
	// DriftFunctionArity is a declared function none of whose overloads takes the declared number
	// of arguments.
	DriftFunctionArity DriftKind = "function_arity"
	// DriftFunctionResult is a declared function whose overloads of the declared arity return a
	// type the declared result kind cannot hold.
	DriftFunctionResult DriftKind = "function_result"
)

type DynCTE

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

DynCTE is a dynamic query declared on another with With: WITH keel_cte (…) AS (sub). A statement names its CTEs keel_cte, keel_cte_2, … in the order it uses them, across every query nested in it, so no name shadows another. Its columns are sub's selected expressions, addressed by label.

func (*DynCTE) Col

func (c *DynCTE) Col(label string) DynExpr

Col returns the CTE's column labelled label.

func (*DynCTE) Labels

func (c *DynCTE) Labels() []string

Labels returns the labels of the CTE's columns.

type DynCaseBuilder

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

DynCaseBuilder builds CASE WHEN … THEN … [ELSE …] END. Start it with DynCase.

func DynCase

func DynCase() DynCaseBuilder

DynCase starts a CASE expression.

func (DynCaseBuilder) Else

func (b DynCaseBuilder) Else(v any) DynExpr

Else ends the CASE with ELSE v.

func (DynCaseBuilder) End

func (b DynCaseBuilder) End() DynExpr

End ends the CASE without ELSE: rows no WHEN matches are NULL.

func (DynCaseBuilder) When

func (b DynCaseBuilder) When(cond Pred, then any) DynCaseBuilder

When adds WHEN cond THEN then; then is a DynExpr, an Expr or a value bound as a parameter.

type DynColumn

type DynColumn struct {
	Name string
	Type ValueType
	// OID is the column's Postgres type.
	OID uint32
	// Nulls is a bitmap with bit i%64 of word i/64 set when row i is NULL; it is shorter than the
	// rows when the last rows are not NULL, and empty when none is.
	Nulls []uint64

	String  []string
	Int64   []int64
	Float64 []float64
	Numeric []Decimal
	Bool    []bool
	Time    []time.Time
	Bytes   [][]byte
	JSON    [][]byte
	// contains filtered or unexported fields
}

DynColumn is one column of a dynamic result: its label, its value type and one typed vector, the one Type selects, with a zero value at each NULL row. Strings, decimals, bytes and JSON share the result's memory instead of allocating per value.

func (*DynColumn) IsNull

func (c *DynColumn) IsNull(i int) bool

IsNull reports whether row i is NULL.

func (*DynColumn) Value

func (c *DynColumn) Value(i int) any

Value returns row i boxed: nil for NULL, else int64, float64, Decimal, string, bool, time.Time or []byte (bytes and JSON). The vectors are the fast path; Value is for code that handles a few values generically.

type DynExpr

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

DynExpr is an expression of a dynamic query, built at run time: a column or relation path resolved by name against the schema, a literal bound as a parameter, or an operator, function, CASE, CAST, aggregate or window over other expressions. It is a Pred, so Where, Having, And, Or and Filter take it, and an Expr, so it mixes with keel's typed expressions.

A name that does not resolve, a function outside the allow-list or a misplaced Filter makes an expression that carries an error: Err reports it, and a statement that uses the expression fails with it. Identifiers in the SQL only ever come from the schema; every value is a bound parameter.

func DynArray

func DynArray(vs ...any) DynExpr

DynArray is the array ARRAY[vs…]: DynExprs, Exprs or values bound as parameters. Cast an element when Postgres cannot infer the array's type from its elements alone.

func DynConst

func DynConst(v any) DynExpr

DynConst writes v into the SQL text as a constant instead of binding it: an integer, a float, a bool or a string (a named type over one of them included), the string quoted with its single quotes doubled. Use it where Postgres must see the constant itself, as when the expression has to match an expression index or the predicate of a partial index, which a parameter does not under a generic plan. Values from users belong in DynLit: each distinct constant is a distinct statement.

func DynCount

func DynCount() DynExpr

DynCount is count(*).

func DynFunc

func DynFunc(name string, args ...any) DynExpr

DynFunc calls the function name of keel's allow-list with args: DynExprs, Exprs or values bound as parameters. Scalar functions: abs, ceil, floor, round, trunc, sign, sqrt, power, mod, greatest, least, coalesce, nullif, lower, upper, initcap, trim, ltrim, rtrim, btrim, length, substr, left, right, replace, concat, concat_ws, strpos, split_part, lpad, rpad, md5, to_char, date_trunc, date_part, date_bin, age, to_timestamp, timezone, make_date, now, jsonb_extract_path_text, jsonb_typeof, jsonb_array_length, cardinality, array_to_string, array_append, array_remove, array_position, regexp_replace, jsonb_build_array, jsonb_build_object, jsonb_set, width_bucket, gen_random_uuid, int4range, int8range, numrange, tstzrange, ln, exp, log, and the set-returning unnest, generate_series, jsonb_each, jsonb_each_text, jsonb_array_elements, jsonb_array_elements_text and jsonb_object_keys, for keel.Rows. Aggregates, which take Filter and Over: sum, avg, min, max, count (count() is count(*)), bool_and, bool_or, string_agg, stddev, stddev_samp, stddev_pop, variance, var_samp, var_pop, array_agg, jsonb_agg, jsonb_object_agg. Any other name makes an expression that fails.

func DynLit

func DynLit(v any) DynExpr

DynLit binds v as a parameter. A nil v is NULL. Postgres infers the parameter's type from where it is used; Cast fixes it where the context does not (a literal on both sides of an operator, a CASE result).

func DynOf

func DynOf(e Expr) DynExpr

DynOf wraps one of keel's typed expressions (a column of a generated handle, an Agg such as RowNumber or Grouping, a Raw) as a DynExpr. An Agg keeps Filter and Over.

func DynRow

func DynRow(vs ...any) DynExpr

DynRow is the row value ROW(vs…): DynExprs, Exprs or values bound as parameters. Rows compare element by element in order, so DynRow(a, b).Gt(DynRow(x, y)) is the keyset condition of a page ordered by a, b ascending, which an index on (a, b) serves.

func ResolvePath

func ResolvePath(t interface{ TableRef() *Ref }, segments ...string) DynExpr

ResolvePath resolves a column path on a typed handle, query or alias, for code that builds a typed query's filters and order from names chosen at run time (an API's filter and sort fields). The segments are read as Path reads them: the last names a column (its SQL name or Go field name), the ones before it to-one relations (their Go name or its snake_case form) followed from t's table. The result is a column of the same relation path the generated handles build, so ResolvePath(s, "customer", "name") and s.Customer().Name are one join: the typed query LEFT JOINs the relations with their tenant condition where the expression is used, in Where, Order (a keyset page included) and Pick. A name that does not resolve, or a path through a to-many relation, makes an expression that carries the error, which the query's terminal returns.

func (DynExpr) Add

func (d DynExpr) Add(v any) DynExpr

Add is d + v.

func (DynExpr) And

func (d DynExpr) And(ps ...Pred) DynExpr

And is d AND ps…; nil predicates are skipped.

func (DynExpr) As

func (d DynExpr) As(label string) DynExpr

As returns the expression with label as its result column's name. The label is kept in Go and never written into the SQL, so it may be any text.

func (DynExpr) Asc

func (d DynExpr) Asc() Order

Asc orders by d ascending.

func (DynExpr) At

func (d DynExpr) At(i int) DynExpr

At is d[i], the i-th element of an array, from 1.

func (DynExpr) Avg

func (d DynExpr) Avg() DynExpr

Avg is avg(d).

func (DynExpr) Between

func (d DynExpr) Between(lo, hi any) DynExpr

Between is d BETWEEN lo AND hi.

func (DynExpr) Cast

func (d DynExpr) Cast(t ValueType) DynExpr

Cast is CAST(d AS t). A literal is cast as $n::t, and equal literals cast to the same type share one parameter.

func (DynExpr) Coalesce

func (d DynExpr) Coalesce(vs ...any) DynExpr

Coalesce is coalesce(d, vs…).

func (DynExpr) Collate

func (d DynExpr) Collate(name string) DynExpr

Collate is d COLLATE "name", ordering or comparing d by that collation ("C" orders by bytes).

func (DynExpr) Concat

func (d DynExpr) Concat(v any) DynExpr

Concat is d || v.

func (DynExpr) Contains

func (d DynExpr) Contains(s string) DynExpr

Contains is d LIKE '%s%' with % and _ in s matching themselves.

func (DynExpr) ContainsFold

func (d DynExpr) ContainsFold(s string) DynExpr

ContainsFold is Contains ignoring case.

func (DynExpr) Count

func (d DynExpr) Count() DynExpr

Count is count(d): the rows where d is not NULL.

func (DynExpr) CountDistinct

func (d DynExpr) CountDistinct() DynExpr

CountDistinct is count(DISTINCT d).

func (DynExpr) CoveredBy

func (d DynExpr) CoveredBy(v any) DynExpr

CoveredBy is d <@ v: v contains d.

func (DynExpr) Covers

func (d DynExpr) Covers(v any) DynExpr

Covers is d @> v: an array, range or jsonb d contains v.

func (DynExpr) Desc

func (d DynExpr) Desc() Order

Desc orders by d descending.

func (DynExpr) Distinct

func (d DynExpr) Distinct() DynExpr

Distinct aggregates the distinct values of d's argument: array_agg(DISTINCT e). d must be an aggregate.

func (DynExpr) Div

func (d DynExpr) Div(v any) DynExpr

Div is d / v. Integer division truncates; Cast one side to TypeNumeric or TypeFloat64 for a fraction, and wrap the divisor with NullIf(0) where it can be zero.

func (DynExpr) Eq

func (d DynExpr) Eq(v any) DynExpr

Eq is d = v. v is a DynExpr, an Expr or a value bound as a parameter, as for every operator.

func (DynExpr) Err

func (d DynExpr) Err() error

Err returns the error the expression carries: an unresolved name, a function outside the allow-list, a misplaced Filter or Over. A statement using the expression fails with it.

func (DynExpr) Filter

func (d DynExpr) Filter(p Pred) DynExpr

Filter restricts the rows an aggregate sees: agg FILTER (WHERE p). d must be an aggregate.

func (DynExpr) Gt

func (d DynExpr) Gt(v any) DynExpr

Gt is d > v.

func (DynExpr) Gte

func (d DynExpr) Gte(v any) DynExpr

Gte is d >= v.

func (DynExpr) HasPrefix

func (d DynExpr) HasPrefix(s string) DynExpr

HasPrefix is d LIKE 's%' with s escaped.

func (DynExpr) HasSuffix

func (d DynExpr) HasSuffix(s string) DynExpr

HasSuffix is d LIKE '%s' with s escaped.

func (DynExpr) ILike

func (d DynExpr) ILike(pattern any) DynExpr

ILike is d ILIKE pattern.

func (DynExpr) In

func (d DynExpr) In(list any) DynExpr

In is d = ANY(list): list is a slice ([]string, []int64, []any of one type) bound as one array parameter, so the statement text does not depend on its length.

func (DynExpr) InUnnest

func (d DynExpr) InUnnest(arrays ...any) DynExpr

InUnnest is d IN (SELECT * FROM unnest(arrays…)): d, usually a DynRow, equals one of the rows the arrays zip into, element i of each array making row i. Each array is bound as one parameter, so the statement keeps one shape whatever the number of rows.

func (DynExpr) IsDistinctFrom

func (d DynExpr) IsDistinctFrom(v any) DynExpr

IsDistinctFrom is d IS DISTINCT FROM v: inequality that treats NULL as a value.

func (DynExpr) IsFalse

func (d DynExpr) IsFalse() DynExpr

IsFalse is d IS FALSE.

func (DynExpr) IsNotNull

func (d DynExpr) IsNotNull() DynExpr

IsNotNull is d IS NOT NULL.

func (DynExpr) IsNull

func (d DynExpr) IsNull() DynExpr

IsNull is d IS NULL.

func (DynExpr) IsTrue

func (d DynExpr) IsTrue() DynExpr

IsTrue is d IS TRUE.

func (DynExpr) Label

func (d DynExpr) Label() string

Label is the name the expression's result column gets: the one given with As, else the path a column was resolved from ("customer.name"), else the function or aggregate name.

func (DynExpr) Like

func (d DynExpr) Like(pattern any) DynExpr

Like is d LIKE pattern, with % and _ as given.

func (DynExpr) Lt

func (d DynExpr) Lt(v any) DynExpr

Lt is d < v.

func (DynExpr) Lte

func (d DynExpr) Lte(v any) DynExpr

Lte is d <= v.

func (DynExpr) Match

func (d DynExpr) Match(query string, opts ...SearchOption) DynExpr

Match is d @@ query, as TSVectorCol.Match: d is a tsvector column or expression.

func (DynExpr) Max

func (d DynExpr) Max() DynExpr

Max is max(d).

func (DynExpr) Min

func (d DynExpr) Min() DynExpr

Min is min(d).

func (DynExpr) Mod

func (d DynExpr) Mod(v any) DynExpr

Mod is d % v.

func (DynExpr) Mul

func (d DynExpr) Mul(v any) DynExpr

Mul is d * v.

func (DynExpr) Ne

func (d DynExpr) Ne(v any) DynExpr

Ne is d <> v.

func (DynExpr) Neg

func (d DynExpr) Neg() DynExpr

Neg is -d.

func (DynExpr) Not

func (d DynExpr) Not() DynExpr

Not is NOT d.

func (DynExpr) NotIn

func (d DynExpr) NotIn(list any) DynExpr

NotIn is d <> ALL(list).

func (DynExpr) NullIf

func (d DynExpr) NullIf(v any) DynExpr

NullIf is nullif(d, v): NULL when d equals v, for safe division.

func (DynExpr) Or

func (d DynExpr) Or(ps ...Pred) DynExpr

Or is d OR ps…; nil predicates are skipped.

func (DynExpr) OrderBy

func (d DynExpr) OrderBy(os ...Order) DynExpr

OrderBy orders the rows an aggregate reads: jsonb_agg(e ORDER BY …). d must be an aggregate.

func (DynExpr) Over

func (d DynExpr) Over(w Window) DynExpr

Over turns an aggregate or window function into a window function over w: a running or moving aggregate, a rank, an offset or a frame value.

func (DynExpr) Rank

func (d DynExpr) Rank(query string, opts ...SearchOption) DynExpr

Rank is ts_rank(d, query), as TSVectorCol.Rank.

func (DynExpr) RankCD

func (d DynExpr) RankCD(query string, opts ...SearchOption) DynExpr

RankCD is ts_rank_cd(d, query), as TSVectorCol.RankCD.

func (DynExpr) Sub

func (d DynExpr) Sub(v any) DynExpr

Sub is d - v.

func (DynExpr) Sum

func (d DynExpr) Sum() DynExpr

Sum is sum(d).

type DynPreAgg

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

DynPreAgg aggregates the rows of a to-many relation per parent row, so measures over the relation do not fan the query's rows out. Measure returns a column of the per-parent result; the query can select it, filter on it in Having, or aggregate it again (sum of per-shipment distances per customer). A parent without related rows reads NULL.

func (*DynPreAgg) AllParents

func (p *DynPreAgg) AllParents() *DynPreAgg

AllParents makes a PreAggCTE aggregate the related rows of every parent in scope instead of only the parents the query's filters select: one pass without the semi-join that restricts the rows, cheaper when the filters keep most parents, dearer when they keep few. It does nothing to a PreAggLateral, which reads each selected parent's rows.

func (*DynPreAgg) Err

func (p *DynPreAgg) Err() error

Err returns the error of the pre-aggregation or of its path.

func (*DynPreAgg) Measure

func (p *DynPreAgg) Measure(e DynExpr) DynExpr

Measure adds an aggregate over the relation's columns (rel.Col(…).Sum(), DynCount()) and returns the column that holds it for each parent row.

func (*DynPreAgg) Where

func (p *DynPreAgg) Where(ps ...Pred) *DynPreAgg

Where filters the related rows before they are aggregated.

type DynQuery

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

DynQuery is a query whose tables, columns, relations and expressions are chosen at run time. It renders through the same machinery as typed queries: every table it references, joined, in a subquery or in a pre-aggregation, gets the scope's tenant predicate (or needs AllTenants under a system scope), interceptors apply to every statement on a table, and identifiers only ever come from the schema. Its result is a typed columnar DynResult.

A DynQuery is a builder: methods change it and return it, and it must not be used by several goroutines at once. Builder errors (an unknown name, an invalid expression) are kept and returned by Err and by every terminal.

func DynExcept

func DynExcept(parts ...*DynQuery) *DynQuery

DynExcept is the rows of the first part that no other part returns: (q1) EXCEPT (q2) ….

func DynFrom

func DynFrom(t *schema.Table) *DynQuery

DynFrom starts a dynamic query over table t.

func DynFromQuery

func DynFromQuery(sub *DynQuery) *DynQuery

DynFromQuery starts a dynamic query over the rows of sub, as a derived table: its columns are sub's selected expressions, resolved by label with Path. Use it to filter, group or order the result of another dynamic query.

func DynIntersect

func DynIntersect(parts ...*DynQuery) *DynQuery

DynIntersect is the rows every part returns: (q1) INTERSECT (q2) ….

func DynUnion

func DynUnion(parts ...*DynQuery) *DynQuery

DynUnion is the distinct rows of every part: (q1) UNION (q2) …. The parts select the same number of expressions; the result's columns take the first part's labels. Filter, order and limit it like any dynamic query.

func DynUnionAll

func DynUnionAll(parts ...*DynQuery) *DynQuery

DynUnionAll is every row of every part: (q1) UNION ALL (q2) ….

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	names := func(table string) *keel.DynQuery {
		q := reportTables.Query(table)
		return q.Select(q.Path("name").As("name"))
	}
	all := keel.DynUnionAll(names("customers"), names("locations"), names("tags"))
	all.Order(all.Path("name").Asc())
	res, err := all.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Column("name").String)
}

func (*DynQuery) AllTenants

func (q *DynQuery) AllTenants() *DynQuery

AllTenants lets the query cross tenants. It needs a system scope. It covers the query's joined relations, Any and None, and pre-aggregations; a query used in With, DynFromQuery or a set operation declares it itself.

func (*DynQuery) Compute

func (q *DynQuery) Compute(name string, e DynExpr) *DynQuery

Compute fills the computed field named name (its SQL name or Go field name) with e, as Query.Compute does: Path(name) then reads e, in Select, Where, Order and keyset pages, and a query without Select returns the field after the table's default columns. e's type must fit the field when keel knows it.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c, s := fleetdb.Contacts, fleetdb.Shipments
	q := keel.DynFrom(fleetdb.ContactsSchema())
	q.Compute("loads", keel.DynOf(keel.Scalar(s.Pick(keel.Count()).Where(s.CustomerID.EqCol(c.CustomerID))))).
		Select(q.Path("name"), q.Path("loads")).
		Order(q.Path("loads").Desc()).
		Limit(10)
	res, err := q.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Rows)
}

func (*DynQuery) Count

func (q *DynQuery) Count(ctx context.Context, db *DB) (int64, error)

Count returns the number of rows the query returns.

func (*DynQuery) CountEstimate

func (q *DynQuery) CountEstimate(ctx context.Context, db *DB, exactUpTo int64) (RowCount, error)

CountEstimate returns the number of rows the dynamic query returns, counted exactly up to exactUpTo, and the planner's estimate when it returns more, as Query.CountEstimate does: the count wraps the query (SELECT count(*) FROM (SELECT 1 FROM (query) d LIMIT exactUpTo+1) c), so a Limit on the query bounds both the count and the estimate, and EXPLAIN (FORMAT JSON) of the query rides in the same round trip.

func (*DynQuery) Cube

func (q *DynQuery) Cube(es ...Expr) *DynQuery

Cube groups by every subset of es, as Query.Cube.

func (*DynQuery) CustomPlan

func (q *DynQuery) CustomPlan() *DynQuery

CustomPlan plans the query for its parameter values on every execution, as Query.CustomPlan.

func (*DynQuery) Distinct

func (q *DynQuery) Distinct() *DynQuery

Distinct returns each distinct row once (SELECT DISTINCT).

func (*DynQuery) Err

func (q *DynQuery) Err() error

Err returns the first builder error: an unknown table, column or relation, an invalid expression, a label selected twice.

func (*DynQuery) Exists

func (q *DynQuery) Exists(ctx context.Context, db *DB) (bool, error)

Exists reports whether the query returns any row, in one statement that stops at the first row.

func (*DynQuery) Explain

func (q *DynQuery) Explain(ctx context.Context, db *DB, opts ExplainOptions) (*Plan, error)

Explain returns the plan of the dynamic query's statement, as Query.Explain does: an analytics guard compares TotalCost and Rows with its limits before running a report.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")
	q.Select(q.Path("customer", "code"), q.Path("total").Sum().As("revenue")).
		Group(q.Path("customer", "code"))

	plan, err := q.Explain(ctx, db, keel.ExplainOptions{})
	if err != nil {
		log.Fatal(err)
	}
	if plan.TotalCost > 1_000_000 || plan.Rows > 100_000 {
		log.Fatal("report too expensive")
	}
	fmt.Println(plan.TotalCost)
}

func (*DynQuery) Group

func (q *DynQuery) Group(es ...Expr) *DynQuery

Group adds GROUP BY expressions.

func (*DynQuery) GroupingSets

func (q *DynQuery) GroupingSets(sets ...[]Expr) *DynQuery

GroupingSets groups by each of sets in turn: GROUP BY GROUPING SETS (…), as Query.GroupingSets.

func (*DynQuery) Having

func (q *DynQuery) Having(ps ...Pred) *DynQuery

Having adds HAVING predicates.

func (*DynQuery) Join

func (q *DynQuery) Join(c *DynCTE, on ...Pred) *DynQuery

Join adds an INNER JOIN of a CTE declared with With.

func (*DynQuery) LeftJoin

func (q *DynQuery) LeftJoin(c *DynCTE, on ...Pred) *DynQuery

LeftJoin adds a LEFT JOIN of a CTE declared with With.

func (*DynQuery) Limit

func (q *DynQuery) Limit(n int) *DynQuery

Limit caps the number of rows.

func (*DynQuery) Offset

func (q *DynQuery) Offset(n int) *DynQuery

Offset skips n rows. Page reads by keyset instead.

func (*DynQuery) Omit

func (q *DynQuery) Omit(names ...string) *DynQuery

Omit leaves the columns named (SQL or Go field names) out of the default columns a query without Select returns, as Query.Omit does for typed reads.

func (*DynQuery) Order

func (q *DynQuery) Order(os ...Order) *DynQuery

Order sets the sort order.

func (*DynQuery) Page

func (q *DynQuery) Page(ctx context.Context, db *DB, p PageRequest) (*DynResult, error)

Page reads the page of rows after the request's cursor, in the query's order. Every order term must be a selected expression or one keel can add as a hidden column; a query on a table that neither groups nor is DISTINCT gets its primary key appended to the order, so each row has a unique position. Grouped and derived queries must order by expressions that are unique together. The result's Next is the cursor of the following page.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")
	pro := q.Path("pro_number")
	q.Select(pro, q.Path("customer", "code")).Order(pro.Desc())

	cursor := ""
	for {
		res, err := q.Page(ctx, db, keel.After(cursor, 50))
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(res.Column("pro_number").String)
		if res.Next == "" {
			break
		}
		cursor = res.Next
	}
}

func (*DynQuery) Path

func (q *DynQuery) Path(segments ...string) DynExpr

Path resolves a column by name: the last segment names a column (its SQL name or Go field name), the segments before it relations (their Go name or its snake_case form) followed from the base table, each of them to-one. Path("status"), Path("customer", "name"). The relations are LEFT JOINed with their tenant condition when the expression is used. On a derived query the one segment is the label of a column of the inner query.

func (*DynQuery) Rel

func (q *DynQuery) Rel(path ...string) *DynRel

Rel resolves a relation path from the base table: Rel("moves"), Rel("moves", "stops"), Rel("customer").

func (*DynQuery) Rollup

func (q *DynQuery) Rollup(es ...Expr) *DynQuery

Rollup groups by es and each prefix of es down to the grand total, as Query.Rollup.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")

	code := q.Path("customer", "code")
	month := keel.DynFunc("date_trunc", "month", keel.DynFunc("to_timestamp", q.Path("created_at"))).
		As("month")
	revenue := q.Path("total").Sum()
	level := keel.DynOf(keel.Grouping(code, month))
	q.Select(code, month, revenue.As("revenue"),
		keel.DynCount().Filter(q.Path("hazmat").IsTrue()).As("hazmat loads"),
		revenue.Sum().Over(keel.Window{}).As("all revenue"),
		keel.DynOf(keel.Rank().OverWindow(keel.OrderBy(revenue.Desc()))).As("rank"),
		level.As("level")).
		Rollup(code, month).
		Order(level.Asc(), code.Asc(), month.Asc())
	res, err := q.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Rows)
}

func (*DynQuery) Run

func (q *DynQuery) Run(ctx context.Context, db *DB) (*DynResult, error)

Run reads every row the query returns into a columnar result.

func (*DynQuery) Select

func (q *DynQuery) Select(es ...DynExpr) *DynQuery

Select sets the expressions the query returns, in order. Each becomes a column of the result named by its Label. Without Select a table query returns the table's default columns.

func (*DynQuery) Stream

func (q *DynQuery) Stream(ctx context.Context, db *DB, size int) iter.Seq2[*DynResult, error]

Stream runs the query once and yields its rows in batches of size rows, decoding each batch as the rows arrive, so an export of any size holds one batch in memory. Each batch is a separate DynResult the consumer may keep. A query without rows yields one empty batch, so the columns are known. Breaking out of the loop stops the query: outside a transaction keel cancels it; inside one the driver reads and discards the remaining rows. The loop body must not run statements on the transaction the stream reads from.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")
	q.Select(q.Path("pro_number"), q.Path("customer", "name"), q.Path("total"), q.Path("weight"))

	for batch, err := range q.Stream(ctx, db, 5000) {
		if err != nil {
			log.Fatal(err)
		}
		weight := batch.Column("weight")
		for i := range batch.Rows {
			if weight.IsNull(i) {
				continue
			}
			fmt.Println(batch.Columns[0].String[i], weight.Int64[i])
		}
	}
}

func (*DynQuery) Table

func (q *DynQuery) Table() *schema.Table

Table returns the query's base table: a registered table, or the derived table of DynFromQuery and the set operations.

func (*DynQuery) Timeout

func (q *DynQuery) Timeout(d time.Duration) *DynQuery

Timeout bounds the query's execution.

func (*DynQuery) ToSQL

func (q *DynQuery) ToSQL(ctx context.Context, db *DB) (string, []any, error)

ToSQL renders the query's statement as it would run under ctx's scope.

func (*DynQuery) Where

func (q *DynQuery) Where(ps ...Pred) *DynQuery

Where adds predicates, joined by AND. Nil predicates are skipped.

func (*DynQuery) Window

func (q *DynQuery) Window(name string, w Window) *DynQuery

Window declares a named window, as Query.Window.

func (*DynQuery) With

func (q *DynQuery) With(sub *DynQuery) *DynCTE

With declares sub as a common table expression of q and returns it, to Join and to read columns from with Col.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	sub := reportTables.Query("shipments")
	sub.Select(sub.Path("customer_id").As("customer"), sub.Path("total").Sum().As("revenue")).
		Group(sub.Path("customer_id"))

	c := reportTables.Query("customers")
	totals := c.With(sub)
	c.Join(totals, totals.Col("customer").Eq(c.Path("id"))).
		Select(c.Path("code"), totals.Col("revenue")).
		Where(totals.Col("revenue").Gt(10_000))
	res, err := c.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	outer := keel.DynFromQuery(sub)
	outer.Select(keel.DynCount().As("customers"), outer.Path("revenue").Avg().As("average"))
	summary, err := outer.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Rows, summary.Column("average").Numeric[0])
}

type DynRel

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

DynRel is a relation path of a dynamic query, to-one or to-many, for semi-joins (Any, None) and pre-aggregations. Build it with Rel.

func (*DynRel) Any

func (d *DynRel) Any(ps ...Pred) DynExpr

Any is true when a row of the relation matches ps: EXISTS (…), correlated with the row, with the related tables' tenant predicates and interceptors. A path through several to-many relations nests one EXISTS per to-many relation.

func (*DynRel) Col

func (d *DynRel) Col(path ...string) DynExpr

Col resolves a column of the relation's table, through further to-one relations: q.Rel("moves").Col("status"). Use it inside Any, None and pre-aggregated measures.

func (*DynRel) Err

func (d *DynRel) Err() error

Err returns the error resolving the path.

func (*DynRel) None

func (d *DynRel) None(ps ...Pred) DynExpr

None is true when no row of the relation matches ps: NOT EXISTS (…).

func (*DynRel) PreAggregate

func (d *DynRel) PreAggregate(s PreAggStrategy) *DynPreAgg

PreAggregate aggregates the relation per parent row with strategy s. The path must contain a to-many relation; the first one is the level the rows are aggregated to, and to-many relations after it are joined inside the aggregation.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	q := reportTables.Query("shipments")
	code := q.Path("customer", "code")

	moves := q.Rel("moves")
	stops := q.Rel("moves", "stops")
	pa := moves.PreAggregate(keel.PreAggCTE)
	legs := pa.Measure(keel.DynCount().As("legs"))
	loaded := pa.Measure(keel.DynCount().Filter(moves.Col("loaded").IsTrue()).As("loaded legs"))
	q.Select(code, q.Path("total").Sum().As("revenue"), legs.Sum().As("legs"),
		loaded.Sum().As("loaded legs")).
		Where(moves.Any(moves.Col("status").Eq("New")),
			stops.None(stops.Col("location", "city").Eq("Dallas"))).
		Group(code)
	res, err := q.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Rows)
}

type DynResult

type DynResult struct {
	Columns    []DynColumn
	Rows       int
	Next       string
	Total      int64
	TotalExact bool
	// contains filtered or unexported fields
}

DynResult is the typed columnar result of a dynamic query: one DynColumn per selected expression, in order, and the number of rows. Next is the cursor of the following page after Page, empty on the last page. Total and TotalExact are the query's row count when the page request asked for it, as on Page.

func (*DynResult) Column

func (res *DynResult) Column(name string) *DynColumn

Column returns the column labelled name, or nil.

func (*DynResult) CursorAt

func (res *DynResult) CursorAt(i int) string

CursorAt returns the cursor that resumes the page after row i: Page with After(res.CursorAt(i), n) reads the n rows that follow it. It is the format of Next, computed from the row's order values (hidden order columns included) without a statement. It returns "" on a result that Page did not read, and panics when i is out of range.

type ExplainOptions

type ExplainOptions struct {
	// Analyze runs the statement (EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)) to report actual rows,
	// timings and buffer use. It runs inside a transaction that is rolled back, or a savepoint
	// rolled back to when the context carries a transaction, so nothing the statement does is
	// kept.
	Analyze bool
	// GenericPlan explains the plan Postgres builds without the parameters' values
	// (EXPLAIN (GENERIC_PLAN)), the one a prepared statement settles on after its first runs. A
	// predicate that reaches a partial index or an expression index only through a parameter's
	// value shows here as the scan it really gets. It cannot be combined with Analyze.
	GenericPlan bool
}

ExplainOptions configures Explain.

type Expr

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

Expr is a SQL expression. keel's column, aggregate and combinator values implement it; code outside keel builds expressions from those and from Raw.

func ArrayOf

func ArrayOf(q subquery) Expr

ArrayOf is the array of the values of a query that picks one expression: ARRAY(SELECT …), an empty array when the query returns no rows, where array_agg would be NULL. A correlated query collects a row's related values, distinct with Distinct:

keel.ArrayOf(st.Pick(st.City).Where(st.ShipmentID.EqCol(s.ID)).Distinct())

func Scalar

func Scalar(q subquery) Expr

Scalar is the value of a query that picks one expression and returns at most one row, as an expression: (SELECT …). A query that references the outer statement's columns is correlated, so Scalar fills a computed field with an aggregate over related rows:

c.Compute(c.Loads, keel.Scalar(s.Pick(keel.Count()).Where(s.CustomerID.EqCol(c.CustomerID))))

The subquery is scoped like any statement: it gets its tables' tenant predicates.

func Val

func Val(v any) Expr

Val binds v as a parameter where an expression is expected. The same Val written twice in a statement binds v once.

type Float64ArrayCase

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

Float64ArrayCase builds a CASE whose result is double precision[]. Start it with CaseFloat64Array.

func CaseFloat64Array

func CaseFloat64Array() Float64ArrayCase

CaseFloat64Array starts a CASE whose result is double precision[].

func (Float64ArrayCase) Else

Else ends the CASE with ELSE v.

func (Float64ArrayCase) End

End ends the CASE without ELSE.

func (Float64ArrayCase) When

When adds WHEN cond THEN v.

type Float64ArrayExpr

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

Float64ArrayExpr is a computed double precision[] value, with the methods of StringArrayExpr.

func ArrayOfFloat64

func ArrayOfFloat64(es ...Float64Value) Float64ArrayExpr

ArrayOfFloat64 is ARRAY[es…] of double precision values.

func Float64Array

func Float64Array(vs []float64) Float64ArrayExpr

Float64Array binds vs as $n::double precision[].

func Float64ArrayOf

func Float64ArrayOf(e Expr) Float64ArrayExpr

Float64ArrayOf types e as double precision[].

func (Float64ArrayExpr) At

At is (e)[i], the i-th element from 1.

func (Float64ArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (Float64ArrayExpr) ContainedByExpr

func (e Float64ArrayExpr) ContainedByExpr(o Float64ArrayValue) Pred

ContainedByExpr is (e <@ o).

func (Float64ArrayExpr) ContainsAllExpr

func (e Float64ArrayExpr) ContainsAllExpr(o Float64ArrayValue) Pred

ContainsAllExpr is (e @> o).

func (Float64ArrayExpr) IsNotNull

func (e Float64ArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (Float64ArrayExpr) IsNull

func (e Float64ArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (Float64ArrayExpr) Len

func (e Float64ArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (Float64ArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.Float64Array(nil)) turns an empty array into NULL.

func (Float64ArrayExpr) OverlapsExpr

func (e Float64ArrayExpr) OverlapsExpr(o Float64ArrayValue) Pred

OverlapsExpr is (e && o).

func (Float64ArrayExpr) Unnest

func (e Float64ArrayExpr) Unnest() Float64Expr

Unnest is the set-returning unnest(e), for keel.Rows.

func (Float64ArrayExpr) Without

Without is array_remove(e, v).

type Float64ArrayValue

type Float64ArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

Float64ArrayValue is a double precision[] value: a Float64ArrayExpr.

type Float64Case

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

Float64Case builds a CASE whose result is double precision. Start it with CaseFloat64.

func CaseFloat64

func CaseFloat64() Float64Case

CaseFloat64 starts a CASE whose result is double precision.

func (Float64Case) Else

Else ends the CASE with ELSE v.

func (Float64Case) End

func (b Float64Case) End() Float64Expr

End ends the CASE without ELSE.

func (Float64Case) When

func (b Float64Case) When(cond Pred, v Float64Value) Float64Case

When adds WHEN cond THEN v.

type Float64Col

type Float64Col struct{ Col }

Float64Col is a double precision or real column.

func (Float64Col) Abs

func (c Float64Col) Abs() Float64Expr

Abs is abs(col).

func (Float64Col) Add

Add is (col + v), an expression; Increment is the assignment.

func (Float64Col) Avg

func (c Float64Col) Avg() Agg

func (Float64Col) Between

func (c Float64Col) Between(lo, hi float64) Pred

func (Float64Col) Ceil

func (c Float64Col) Ceil() Float64Expr

Ceil is ceil(col).

func (Float64Col) Coalesce

func (c Float64Col) Coalesce(vs ...any) Float64Expr

Coalesce is coalesce(col, vs…), typed as double precision.

func (Float64Col) Div

Div is (col / v).

func (Float64Col) Eq

func (c Float64Col) Eq(v float64) Pred

func (Float64Col) Exp

func (c Float64Col) Exp() Float64Expr

Exp is exp(col).

func (Float64Col) Floor

func (c Float64Col) Floor() Float64Expr

Floor is floor(col).

func (Float64Col) Greatest

func (c Float64Col) Greatest(others ...Float64Value) Float64Expr

Greatest is greatest(col, others…).

func (Float64Col) Gt

func (c Float64Col) Gt(v float64) Pred

func (Float64Col) Gte

func (c Float64Col) Gte(v float64) Pred

func (Float64Col) InArray

func (c Float64Col) InArray(a Float64ArrayValue) Pred

InArray is (col = ANY(a)).

func (Float64Col) Increment

func (c Float64Col) Increment(v float64) Assignment

func (Float64Col) IsDistinctFrom

func (c Float64Col) IsDistinctFrom(v float64) Pred

func (Float64Col) IsDistinctFromExpr

func (c Float64Col) IsDistinctFromExpr(o Float64Value) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (Float64Col) IsNotDistinctFrom

func (c Float64Col) IsNotDistinctFrom(v float64) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (Float64Col) IsNotDistinctFromExpr

func (c Float64Col) IsNotDistinctFromExpr(o Float64Value) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (Float64Col) Least

func (c Float64Col) Least(others ...Float64Value) Float64Expr

Least is least(col, others…).

func (Float64Col) Ln

func (c Float64Col) Ln() Float64Expr

Ln is ln(col).

func (Float64Col) Lt

func (c Float64Col) Lt(v float64) Pred

func (Float64Col) Lte

func (c Float64Col) Lte(v float64) Pred

func (Float64Col) Mul

Mul is (col * v).

func (Float64Col) Ne

func (c Float64Col) Ne(v float64) Pred

func (Float64Col) Neg

func (c Float64Col) Neg() Float64Expr

Neg is (-col).

func (Float64Col) NotInArray

func (c Float64Col) NotInArray(a Float64ArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (Float64Col) NullIf

func (c Float64Col) NullIf(v float64) Float64Expr

NullIf is nullif(col, v).

func (Float64Col) Round

func (c Float64Col) Round() Float64Expr

Round is round(col).

func (Float64Col) Sub

Sub is (col - v).

func (Float64Col) Sum

func (c Float64Col) Sum() Agg

func (Float64Col) To

func (c Float64Col) To(v float64) Assignment

type Float64Expr

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

Float64Expr is a computed double precision value: a date part, a logarithm, a rounded float.

func CastFloat64

func CastFloat64(e Expr) Float64Expr

CastFloat64 is CAST(e AS double precision) of a number or of text holding one.

func ConstFloat64

func ConstFloat64(f float64) Float64Expr

ConstFloat64 writes f into the SQL as '0.5'::double precision, a double precision constant (Postgres would read a bare 0.5 as numeric and a bare 1 as integer, dividing as integers). NaN and infinities make an expression that fails.

func DatePart

func DatePart(field string, t TimeValue) Float64Expr

DatePart is date_part('field', t): a field of t as double precision ("hour", "dow", "epoch", …). An unknown field makes an expression that fails.

func Float64

func Float64(f float64) Float64Expr

Float64 binds f as a double precision value, $n::double precision.

func Float64Of

func Float64Of(e Expr) Float64Expr

Float64Of types e as double precision without changing the SQL; CastFloat64 converts.

func Float64Ptr

func Float64Ptr(p *float64) Float64Expr

Float64Ptr binds *p as $n::double precision, NULL when p is nil.

func NullFloat64

func NullFloat64() Float64Expr

NullFloat64 is NULL::double precision.

func (Float64Expr) Abs

func (e Float64Expr) Abs() Float64Expr

Abs is abs(e).

func (Float64Expr) Add

Add is (e + v).

func (Float64Expr) Asc

func (e Float64Expr) Asc() Order

func (Float64Expr) Avg

func (e Float64Expr) Avg() Agg

func (Float64Expr) Between

func (e Float64Expr) Between(lo, hi float64) Pred

func (Float64Expr) Ceil

func (e Float64Expr) Ceil() Float64Expr

Ceil is ceil(e).

func (Float64Expr) Coalesce

func (e Float64Expr) Coalesce(vs ...any) Float64Expr

Coalesce is coalesce(e, vs…).

func (Float64Expr) Desc

func (e Float64Expr) Desc() Order

func (Float64Expr) Div

Div is (e / v).

func (Float64Expr) Eq

func (e Float64Expr) Eq(v float64) Pred

func (Float64Expr) EqExpr

func (e Float64Expr) EqExpr(o Float64Value) Pred

func (Float64Expr) Exp

func (e Float64Expr) Exp() Float64Expr

Exp is exp(e).

func (Float64Expr) Floor

func (e Float64Expr) Floor() Float64Expr

Floor is floor(e).

func (Float64Expr) Greatest

func (e Float64Expr) Greatest(others ...Float64Value) Float64Expr

Greatest is greatest(e, others…).

func (Float64Expr) Gt

func (e Float64Expr) Gt(v float64) Pred

func (Float64Expr) GtExpr

func (e Float64Expr) GtExpr(o Float64Value) Pred

func (Float64Expr) Gte

func (e Float64Expr) Gte(v float64) Pred

func (Float64Expr) GteExpr

func (e Float64Expr) GteExpr(o Float64Value) Pred

func (Float64Expr) InArray

func (e Float64Expr) InArray(a Float64ArrayValue) Pred

InArray is (e = ANY(a)).

func (Float64Expr) IsDistinctFrom

func (e Float64Expr) IsDistinctFrom(v float64) Pred

func (Float64Expr) IsDistinctFromExpr

func (e Float64Expr) IsDistinctFromExpr(o Float64Value) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (Float64Expr) IsNotDistinctFrom

func (e Float64Expr) IsNotDistinctFrom(v float64) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (Float64Expr) IsNotDistinctFromExpr

func (e Float64Expr) IsNotDistinctFromExpr(o Float64Value) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (Float64Expr) IsNotNull

func (e Float64Expr) IsNotNull() Pred

func (Float64Expr) IsNull

func (e Float64Expr) IsNull() Pred

func (Float64Expr) Least

func (e Float64Expr) Least(others ...Float64Value) Float64Expr

Least is least(e, others…).

func (Float64Expr) Ln

func (e Float64Expr) Ln() Float64Expr

Ln is ln(e), the natural logarithm.

func (Float64Expr) Lt

func (e Float64Expr) Lt(v float64) Pred

func (Float64Expr) LtExpr

func (e Float64Expr) LtExpr(o Float64Value) Pred

func (Float64Expr) Lte

func (e Float64Expr) Lte(v float64) Pred

func (Float64Expr) LteExpr

func (e Float64Expr) LteExpr(o Float64Value) Pred

func (Float64Expr) Max

func (e Float64Expr) Max() Agg

func (Float64Expr) Min

func (e Float64Expr) Min() Agg

func (Float64Expr) Mul

Mul is (e * v).

func (Float64Expr) Ne

func (e Float64Expr) Ne(v float64) Pred

func (Float64Expr) NeExpr

func (e Float64Expr) NeExpr(o Float64Value) Pred

func (Float64Expr) Neg

func (e Float64Expr) Neg() Float64Expr

Neg is (-e).

func (Float64Expr) NotInArray

func (e Float64Expr) NotInArray(a Float64ArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (Float64Expr) NullIf

func (e Float64Expr) NullIf(v float64) Float64Expr

NullIf is nullif(e, v).

func (Float64Expr) Round

func (e Float64Expr) Round() Float64Expr

Round is round(e), to the nearest integer, halves away from zero. Postgres rounds double precision only to whole numbers; CastNumeric(e).RoundTo(places) rounds to decimal places.

func (Float64Expr) Sub

Sub is (e - v).

func (Float64Expr) Sum

func (e Float64Expr) Sum() Agg

type Float64Value

type Float64Value interface {
	Expr
	// contains filtered or unexported methods
}

Float64Value is a double precision value: a Float64Col or a Float64Expr.

type FrameBound

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

FrameBound is one end of a window frame: UnboundedPreceding, Preceding(n), CurrentRow, Following(n) or UnboundedFollowing.

func Following

func Following(offset any) FrameBound

Following is the frame bound offset after the current row, measured as Preceding measures it.

func Preceding

func Preceding(offset any) FrameBound

Preceding is the frame bound offset before the current row: a row count in a Rows frame, a peer group count in a Groups frame, and a distance in the order column's type in a Range frame (an integer, a float, a decimal, or a time.Duration for a timestamp order). offset is bound as a parameter.

type FrameExclusion

type FrameExclusion string

FrameExclusion removes rows around the current row from a window frame.

const (
	// ExcludeNoOthers excludes nothing, the default.
	ExcludeNoOthers FrameExclusion = "NO OTHERS"
	// ExcludeCurrentRow excludes the current row.
	ExcludeCurrentRow FrameExclusion = "CURRENT ROW"
	// ExcludeGroup excludes the current row and its peers.
	ExcludeGroup FrameExclusion = "GROUP"
	// ExcludeTies excludes the current row's peers but not the row itself.
	ExcludeTies FrameExclusion = "TIES"
)

type Func0

type Func0[R FuncResult] struct {
	// contains filtered or unexported fields
}

Func0 is a database function of no arguments returning R, declared with DeclareFunc0.

func DeclareFunc0

func DeclareFunc0[R FuncResult](name string) Func0[R]

DeclareFunc0 declares the database function name ("fn" or "schema.fn") taking no arguments and returning R:

var nextBatch = keel.DeclareFunc0[keel.Int64Expr]("billing.next_batch_number")

func (Func0[R]) Call

func (f Func0[R]) Call() R

Call is name().

func (Func0[R]) Decl

func (f Func0[R]) Decl() *FuncDecl

Decl returns the declaration VerifyFuncs checks.

type Func1

type Func1[R FuncResult, A Expr] struct {
	// contains filtered or unexported fields
}

Func1 is a database function of one argument of type A (a value interface such as StringValue, or an expression type) returning R, declared with DeclareFunc1.

func DeclareFunc1

func DeclareFunc1[R FuncResult, A Expr](name string) Func1[R, A]

DeclareFunc1 declares the database function name taking one A and returning R:

var slug = keel.DeclareFunc1[keel.StringExpr, keel.StringValue]("public.slugify")
q.Where(slug.Call(c.Name).Eq("acme-freight"))

func (Func1[R, A]) Call

func (f Func1[R, A]) Call(a A) R

Call is name(a).

func (Func1[R, A]) Decl

func (f Func1[R, A]) Decl() *FuncDecl

Decl returns the declaration VerifyFuncs checks.

type Func2

type Func2[R FuncResult, A, B Expr] struct {
	// contains filtered or unexported fields
}

Func2 is a database function of two arguments returning R, declared with DeclareFunc2.

func DeclareFunc2

func DeclareFunc2[R FuncResult, A, B Expr](name string) Func2[R, A, B]

DeclareFunc2 declares the database function name taking an A and a B and returning R.

func (Func2[R, A, B]) Call

func (f Func2[R, A, B]) Call(a A, b B) R

Call is name(a, b).

func (Func2[R, A, B]) Decl

func (f Func2[R, A, B]) Decl() *FuncDecl

Decl returns the declaration VerifyFuncs checks.

type Func3

type Func3[R FuncResult, A, B, C Expr] struct {
	// contains filtered or unexported fields
}

Func3 is a database function of three arguments returning R, declared with DeclareFunc3.

func DeclareFunc3

func DeclareFunc3[R FuncResult, A, B, C Expr](name string) Func3[R, A, B, C]

DeclareFunc3 declares the database function name taking an A, a B and a C and returning R.

func (Func3[R, A, B, C]) Call

func (f Func3[R, A, B, C]) Call(a A, b B, c C) R

Call is name(a, b, c).

func (Func3[R, A, B, C]) Decl

func (f Func3[R, A, B, C]) Decl() *FuncDecl

Decl returns the declaration VerifyFuncs checks.

type FuncDecl

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

FuncDecl is a database function declared with DeclareFunc0 … DeclareFunc3: its name, the number of arguments its calls pass and the kind of value it returns. VerifyFuncs checks it against the catalog.

func (*FuncDecl) Arity

func (d *FuncDecl) Arity() int

Arity is the number of arguments the declaration's calls pass.

func (*FuncDecl) Name

func (d *FuncDecl) Name() string

Name is the function's name as declared, schema-qualified or not.

type FuncResult

type FuncResult interface {
	StringExpr | Int64Expr | Int32Expr | Float64Expr | NumericExpr | BoolExpr | TimeExpr | JSONBExpr
	Expr
	// contains filtered or unexported methods
}

FuncResult is the expression type a declared function returns, which fixes how its calls compare and what they decode into.

type Handle

type Handle[T any] struct {
	Ref *Ref
}

Handle is embedded in every generated table type. It starts queries and runs writes for rows of type T, the model's pointer type: Handle[*fleet.Shipment]. Every pointer type shares one shape, so the compiler builds one copy of keel's generic code for all models instead of one per model.

func (Handle[T]) AllTenants

func (h Handle[T]) AllTenants() *Query[T]

AllTenants starts a query that crosses tenants. It needs a system scope.

func (Handle[T]) Any

func (h Handle[T]) Any(ps ...Pred) Pred

Any is true when at least one row of the relation path matches ps: EXISTS (…).

func (Handle[T]) Cache

func (h Handle[T]) Cache(opts ...CacheOption) *Query[T]

Cache starts a query served from the result cache; see Query.Cache.

func (Handle[T]) Compute

func (h Handle[T]) Compute(col Column, e Expr) *Query[T]

Compute starts a query that fills the computed field col with e.

func (Handle[T]) Copy

func (h Handle[T]) Copy(ctx context.Context, db *DB, rows []T) (int64, error)

Copy inserts rows through Postgres's binary COPY protocol and returns how many it inserted. It streams the rows, so it beats CreateMany for loads of many thousands of rows.

Rows are prepared as Create prepares them: the autoid, tenant, autocreate and autoupdate columns are filled, a row of another tenant is refused, and the BeforeCreate, BeforeSave and BeforeWrite hooks run. COPY returns nothing, so readonly columns are not read back, and AfterWrite and Committed hooks see the rows as written. Enum columns need their types registered on the connection, which keel does unless Config.SkipEnums is set.

Copy joins the transaction on the context. Outside one it runs in an implicit transaction when Config.OnBegin is set, after the OnBegin statements, when the table has AfterWrite hooks, or when the context is marked with OnReplica on a DB with replicas, so that its commit position is read with COMMIT.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	rows := make([]*fleet.Shipment, 0, 50_000)
	for i := range 50_000 {
		rows = append(rows, &fleet.Shipment{
			CustomerID: "cus_01",
			ProNumber:  fmt.Sprintf("IMPORT-%06d", i),
			Status:     fleet.StatusNew,
		})
	}
	n, err := fleetdb.Shipments.Copy(ctx, db, rows)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, "shipments imported")
}

func (Handle[T]) Create

func (h Handle[T]) Create(ctx context.Context, db *DB, row T) error

Create inserts row. The autoid column, the tenant columns and the autocreate and autoupdate columns are filled first; readonly columns are read back.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	sh := &fleet.Shipment{
		CustomerID: "cus_01",
		ProNumber:  "PRO-1001",
		Status:     fleet.StatusNew,
		Total:      fleet.Money{Cents: 125_000},
		Labels:     []string{"expedite"},
		Attributes: map[string]any{"trailer": map[string]any{"type": "reefer"}},
	}
	if err := fleetdb.Shipments.Create(ctx, db, sh); err != nil {
		log.Fatal(err)
	}
	fmt.Println(sh.ID, sh.OrganizationID, sh.CreatedAt, sh.StageRank)
}

func (Handle[T]) CreateMany

func (h Handle[T]) CreateMany(ctx context.Context, db *DB, rows []T) error

CreateMany inserts rows with as few statements as Postgres's parameter limit allows, all sent in one round trip.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	stops := []*fleet.Stop{
		{MoveID: "mov_01", LocationID: "loc_chicago", Sequence: 1, Kind: "pickup"},
		{MoveID: "mov_01", LocationID: "loc_dallas", Sequence: 2, Kind: "delivery"},
	}
	if err := fleetdb.Stops.CreateMany(ctx, db, stops); err != nil {
		log.Fatal(err)
	}
}

func (Handle[T]) CreateOrSkip

func (h Handle[T]) CreateOrSkip(ctx context.Context, db *DB, rows []T, targets ...Expr) (int64, error)

CreateOrSkip inserts rows, skipping those that conflict on targets (columns or index expressions, as OnConflict takes them), and returns how many were inserted. Without targets it skips rows that violate any unique constraint (ON CONFLICT DO NOTHING). Inserted rows run the create hooks; skipped rows run none.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Customers
	rows := []*fleet.Customer{{Code: "ACME", Name: "Acme"}, {Code: "INITECH", Name: "Initech"}}
	n, err := c.CreateOrSkip(ctx, db, rows, c.OrganizationID, c.BusinessUnitID, c.Code)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, "new customers")
}

func (Handle[T]) Cube

func (h Handle[T]) Cube(es ...Expr) *Query[T]

Cube starts a query grouped by CUBE.

func (Handle[T]) DeleteRow

func (h Handle[T]) DeleteRow(ctx context.Context, db *DB, row T) error

DeleteRow deletes row by its primary key. Interceptors' predicates join the primary key in the WHERE clause. It returns ErrNotFound when no row was deleted, including a row they exclude.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	st, err := fleetdb.Stops.Get(ctx, db, "stp_01")
	if err != nil {
		log.Fatal(err)
	}
	if err := fleetdb.Stops.DeleteRow(ctx, db, st); errors.Is(err, keel.ErrNotFound) {
		fmt.Println("already deleted")
	} else if err != nil {
		log.Fatal(err)
	}
}

func (Handle[T]) DistinctOn

func (h Handle[T]) DistinctOn(es ...Expr) *Query[T]

DistinctOn starts a DISTINCT ON query.

func (Handle[T]) Group

func (h Handle[T]) Group(es ...Expr) *Query[T]

Group starts a grouped query.

func (Handle[T]) GroupingSets

func (h Handle[T]) GroupingSets(sets ...[]Expr) *Query[T]

GroupingSets starts a query grouped by GROUPING SETS.

func (Handle[T]) InsertFrom

func (h Handle[T]) InsertFrom(ctx context.Context, db *DB, src subquery,
	cols ...Column,
) (int64, error)

InsertFrom inserts the rows of src into cols: INSERT INTO t (cols) SELECT …. src must pick one expression per column, in order. Under a tenant scope the tenant columns are filled from the scope and must not be listed.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	tg, c := fleetdb.Tags, fleetdb.Customers
	n, err := tg.InsertFrom(ctx, db,
		c.Pick(keel.Raw("'tag_' || ?", c.ID), c.Name).Where(c.Email.IsNotNull()),
		tg.ID, tg.Name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, "tags created")
}

func (Handle[T]) InsertFromSQL

func (h Handle[T]) InsertFromSQL(ctx context.Context, db *DB, src subquery,
	cols ...Column,
) (string, []any, error)

InsertFromSQL renders the statement InsertFrom sends, without running it.

func (Handle[T]) Join

func (h Handle[T]) Join(t interface{ TableRef() *Ref }, on ...Pred) *Query[T]

Join starts a query with an INNER JOIN of another table.

func (Handle[T]) JoinLateral

func (h Handle[T]) JoinLateral(sub interface {
	subquery
	TableRef() *Ref
},
) *Query[T]

JoinLateral starts a query with CROSS JOIN LATERAL (sub).

func (Handle[T]) LeftJoin

func (h Handle[T]) LeftJoin(t interface{ TableRef() *Ref }, on ...Pred) *Query[T]

LeftJoin starts a query with a LEFT JOIN of another table.

func (Handle[T]) LeftJoinLateral

func (h Handle[T]) LeftJoinLateral(sub interface {
	subquery
	TableRef() *Ref
},
) *Query[T]

LeftJoinLateral starts a query with LEFT JOIN LATERAL (sub) ON TRUE.

func (Handle[T]) Limit

func (h Handle[T]) Limit(n int) *Query[T]

Limit starts a query limited to n rows.

func (Handle[T]) Load

func (h Handle[T]) Load(ls ...Loader) *Query[T]

Load starts a query that loads relations.

func (Handle[T]) LoadStrategy

func (h Handle[T]) LoadStrategy(s LoadStrategy) *Query[T]

LoadStrategy starts a query that loads its relations with s, as Query.LoadStrategy does.

func (Handle[T]) Lock

func (h Handle[T]) Lock(strength LockStrength, wait ...LockWait) *Query[T]

Lock starts a query that locks the rows it selects.

func (Handle[T]) None

func (h Handle[T]) None(ps ...Pred) Pred

None is true when no row of the relation path matches ps: NOT EXISTS (…).

func (Handle[T]) Omit

func (h Handle[T]) Omit(cols ...Column) *Query[T]

Omit starts a query that leaves cols out of the columns it reads.

func (Handle[T]) Order

func (h Handle[T]) Order(os ...Order) *Query[T]

Order starts an ordered query.

func (Handle[T]) Pick

func (h Handle[T]) Pick(es ...Expr) *Query[T]

Pick starts a query that selects only es.

func (Handle[T]) Query

func (h Handle[T]) Query() *Query[T]

Query starts a query over the table.

func (Handle[T]) Rollup

func (h Handle[T]) Rollup(es ...Expr) *Query[T]

Rollup starts a query grouped by ROLLUP.

func (Handle[T]) Save

func (h Handle[T]) Save(ctx context.Context, db *DB, row T, cols ...Column) error

Save updates row by its primary key. With a version column it compares the stored version, increments it and returns a *ConflictError (matching ErrConflict) when another writer got there first. cols narrows the columns written; the autoupdate column is always written. Interceptors' predicates join the primary key in the WHERE clause: a row they exclude is not written, and Save reports it as ErrConflict on a table with a version column, else ErrNotFound.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Customers
	cust, err := c.Get(ctx, db, "cus_01")
	if err != nil {
		log.Fatal(err)
	}
	cust.Name = "Acme Freight"
	err = c.Save(ctx, db, cust)
	var conflict *keel.ConflictError
	switch {
	case errors.As(err, &conflict):
		fmt.Printf("%s changed since version %d; reload and retry\n", conflict.Table, conflict.Version)
	case err != nil:
		log.Fatal(err)
	default:
		fmt.Println("saved version", cust.Version)
	}
}
Example (Columns)
package main

import (
	"context"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	sh, err := s.Get(ctx, db, "shp_01")
	if err != nil {
		log.Fatal(err)
	}
	now := time.Now().Unix()
	sh.Status = fleet.StatusCanceled
	sh.CanceledAt = &now
	if err := s.Save(ctx, db, sh, s.Status, s.CanceledAt); err != nil {
		log.Fatal(err)
	}
}

func (Handle[T]) SaveExcept

func (h Handle[T]) SaveExcept(ctx context.Context, db *DB, row T, cols ...Column) error

SaveExcept is Save writing every updatable column except cols: the columns a caller must never overwrite from a row it read (secrets, columns another process maintains). The autoupdate column is always written.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Contacts
	contact, err := c.Where(c.Name.Eq("Dana")).One(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	contact.Phone = "555-0100"
	if err := c.SaveExcept(ctx, db, contact, c.Balance); err != nil {
		log.Fatal(err)
	}
}

func (Handle[T]) TableRef

func (h Handle[T]) TableRef() *Ref

TableRef returns the handle's table reference.

func (Handle[T]) Upsert

func (h Handle[T]) Upsert(ctx context.Context, db *DB, rows []T, c Conflict) ([]bool, error)

Upsert inserts rows, updating the existing row on conflict. It reports, per row, whether it was inserted (true) or updated (false). Rows that conflict get the stored version and readonly columns back. Inserted rows run the create hooks and updated rows the update hooks; outside a transaction the upsert runs in an implicit one when the table has AfterWrite hooks for either, so a failing hook rolls every row back.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Customers
	rows := []*fleet.Customer{
		{Code: "ACME", Name: "Acme Freight"},
		{Code: "GLOBEX", Name: "Globex Logistics"},
	}
	inserted, err := c.Upsert(ctx, db, rows,
		keel.OnConflict(c.OrganizationID, c.BusinessUnitID, c.Code).Update(c.Name))
	if err != nil {
		log.Fatal(err)
	}
	for i, r := range rows {
		fmt.Println(r.Code, "inserted:", inserted[i], "version:", r.Version)
	}
}

func (Handle[T]) UpsertFrom

func (h Handle[T]) UpsertFrom(ctx context.Context, db *DB, src subquery, c Conflict,
	cols ...Column,
) (int64, error)

UpsertFrom inserts the rows of src into cols, as InsertFrom does, and resolves rows that conflict with c: INSERT INTO t AS alias (cols) SELECT … ON CONFLICT … DO UPDATE SET …. Without Update, Set or DoNothing a conflicting row takes the listed columns that are updatable. The autoupdate column of an updated row is stamped with the statement's time unless Set assigns it, and the version column is incremented. It returns the number of rows inserted or updated; hooks do not run, as for InsertFrom.

func (Handle[T]) UpsertFromSQL

func (h Handle[T]) UpsertFromSQL(ctx context.Context, db *DB, src subquery, c Conflict,
	cols ...Column,
) (string, []any, error)

UpsertFromSQL renders the statement UpsertFrom sends, without running it.

func (Handle[T]) Where

func (h Handle[T]) Where(ps ...Pred) *Query[T]

Where starts a query filtered by ps.

func (Handle[T]) With

func (h Handle[T]) With(ctes ...*CTETable) *Query[T]

With starts a query that declares CTEs.

type HookOption

type HookOption func(*hookEntry)

HookOption configures a table hook.

func Needs

func Needs(cols ...Column) HookOption

Needs asks for the previous values of cols on updates and deletes, read in the same statement (RETURNING old.…, which needs PostgreSQL 18) into Mutation.Old. It costs nothing unless the table has AfterWrite or Committed hooks for the write's op. A computed field has no stored value, so naming one panics when the hook is registered.

type Int16Col

type Int16Col struct{ Col }

Int16Col is a smallint column.

func (Int16Col) Between

func (c Int16Col) Between(lo, hi int16) Pred

func (Int16Col) Eq

func (c Int16Col) Eq(v int16) Pred

func (Int16Col) Gt

func (c Int16Col) Gt(v int16) Pred

func (Int16Col) Gte

func (c Int16Col) Gte(v int16) Pred

func (Int16Col) In

func (c Int16Col) In(vs ...int16) Pred

func (Int16Col) Increment

func (c Int16Col) Increment(v int16) Assignment

func (Int16Col) IsDistinctFrom

func (c Int16Col) IsDistinctFrom(v int16) Pred

func (Int16Col) IsNotDistinctFrom

func (c Int16Col) IsNotDistinctFrom(v int16) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (Int16Col) Lt

func (c Int16Col) Lt(v int16) Pred

func (Int16Col) Lte

func (c Int16Col) Lte(v int16) Pred

func (Int16Col) Ne

func (c Int16Col) Ne(v int16) Pred

func (Int16Col) NotIn

func (c Int16Col) NotIn(vs ...int16) Pred

func (Int16Col) Sum

func (c Int16Col) Sum() Agg

func (Int16Col) To

func (c Int16Col) To(v int16) Assignment

type Int32ArrayCase

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

Int32ArrayCase builds a CASE whose result is integer[]. Start it with CaseInt32Array.

func CaseInt32Array

func CaseInt32Array() Int32ArrayCase

CaseInt32Array starts a CASE whose result is integer[].

func (Int32ArrayCase) Else

Else ends the CASE with ELSE v.

func (Int32ArrayCase) End

End ends the CASE without ELSE.

func (Int32ArrayCase) When

When adds WHEN cond THEN v.

type Int32ArrayExpr

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

Int32ArrayExpr is a computed integer[] value, with the methods of StringArrayExpr.

func ArrayOfInt32

func ArrayOfInt32(es ...Int32Value) Int32ArrayExpr

ArrayOfInt32 is ARRAY[es…] of integer values.

func Int32Array

func Int32Array(vs []int32) Int32ArrayExpr

Int32Array binds vs as $n::integer[].

func Int32ArrayOf

func Int32ArrayOf(e Expr) Int32ArrayExpr

Int32ArrayOf types e as integer[].

func (Int32ArrayExpr) At

func (e Int32ArrayExpr) At(i int) Int32Expr

At is (e)[i], the i-th element from 1.

func (Int32ArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (Int32ArrayExpr) ContainedByExpr

func (e Int32ArrayExpr) ContainedByExpr(o Int32ArrayValue) Pred

ContainedByExpr is (e <@ o).

func (Int32ArrayExpr) ContainsAllExpr

func (e Int32ArrayExpr) ContainsAllExpr(o Int32ArrayValue) Pred

ContainsAllExpr is (e @> o).

func (Int32ArrayExpr) IsNotNull

func (e Int32ArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (Int32ArrayExpr) IsNull

func (e Int32ArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (Int32ArrayExpr) Len

func (e Int32ArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (Int32ArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.Int32Array(nil)) turns an empty array into NULL.

func (Int32ArrayExpr) OverlapsExpr

func (e Int32ArrayExpr) OverlapsExpr(o Int32ArrayValue) Pred

OverlapsExpr is (e && o).

func (Int32ArrayExpr) Unnest

func (e Int32ArrayExpr) Unnest() Int32Expr

Unnest is the set-returning unnest(e), for keel.Rows.

func (Int32ArrayExpr) Without

Without is array_remove(e, v).

type Int32ArrayValue

type Int32ArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

Int32ArrayValue is an integer[] value: an Int32ArrayExpr.

type Int32Case

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

Int32Case builds a CASE whose result is integer. Start it with CaseInt32.

func CaseInt32

func CaseInt32() Int32Case

CaseInt32 starts a CASE whose result is integer.

func (Int32Case) Else

func (b Int32Case) Else(v Int32Value) Int32Expr

Else ends the CASE with ELSE v.

func (Int32Case) End

func (b Int32Case) End() Int32Expr

End ends the CASE without ELSE.

func (Int32Case) When

func (b Int32Case) When(cond Pred, v Int32Value) Int32Case

When adds WHEN cond THEN v.

type Int32Col

type Int32Col struct{ Col }

Int32Col is an integer column.

func (Int32Col) Abs

func (c Int32Col) Abs() Int32Expr

Abs is abs(col).

func (Int32Col) Add

func (c Int32Col) Add(v Int32Value) Int32Expr

Add is (col + v), an expression; Increment is the assignment.

func (Int32Col) Avg

func (c Int32Col) Avg() Agg

func (Int32Col) Between

func (c Int32Col) Between(lo, hi int32) Pred

func (Int32Col) Coalesce

func (c Int32Col) Coalesce(vs ...any) Int32Expr

Coalesce is coalesce(col, vs…), typed as integer.

func (Int32Col) Div

func (c Int32Col) Div(v Int32Value) Int32Expr

Div is (col / v), truncated toward zero.

func (Int32Col) Eq

func (c Int32Col) Eq(v int32) Pred

func (Int32Col) Greatest

func (c Int32Col) Greatest(others ...Int32Value) Int32Expr

Greatest is greatest(col, others…).

func (Int32Col) Gt

func (c Int32Col) Gt(v int32) Pred

func (Int32Col) Gte

func (c Int32Col) Gte(v int32) Pred

func (Int32Col) In

func (c Int32Col) In(vs ...int32) Pred

func (Int32Col) InArray

func (c Int32Col) InArray(a Int32ArrayValue) Pred

InArray is (col = ANY(a)).

func (Int32Col) Increment

func (c Int32Col) Increment(v int32) Assignment

func (Int32Col) IsDistinctFrom

func (c Int32Col) IsDistinctFrom(v int32) Pred

func (Int32Col) IsDistinctFromExpr

func (c Int32Col) IsDistinctFromExpr(o Int32Value) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (Int32Col) IsNotDistinctFrom

func (c Int32Col) IsNotDistinctFrom(v int32) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (Int32Col) IsNotDistinctFromExpr

func (c Int32Col) IsNotDistinctFromExpr(o Int32Value) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (Int32Col) Least

func (c Int32Col) Least(others ...Int32Value) Int32Expr

Least is least(col, others…).

func (Int32Col) Lt

func (c Int32Col) Lt(v int32) Pred

func (Int32Col) Lte

func (c Int32Col) Lte(v int32) Pred

func (Int32Col) Mod

func (c Int32Col) Mod(v Int32Value) Int32Expr

Mod is (col % v).

func (Int32Col) Mul

func (c Int32Col) Mul(v Int32Value) Int32Expr

Mul is (col * v).

func (Int32Col) Ne

func (c Int32Col) Ne(v int32) Pred

func (Int32Col) Neg

func (c Int32Col) Neg() Int32Expr

Neg is (-col).

func (Int32Col) NotIn

func (c Int32Col) NotIn(vs ...int32) Pred

func (Int32Col) NotInArray

func (c Int32Col) NotInArray(a Int32ArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (Int32Col) NullIf

func (c Int32Col) NullIf(v int32) Int32Expr

NullIf is nullif(col, v).

func (Int32Col) Sub

func (c Int32Col) Sub(v Int32Value) Int32Expr

Sub is (col - v).

func (Int32Col) Sum

func (c Int32Col) Sum() Agg

func (Int32Col) To

func (c Int32Col) To(v int32) Assignment

type Int32Expr

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

Int32Expr is a computed integer value: a length, a position, a bucket number.

func ArrayPosition

func ArrayPosition(list []string, v StringValue) Int32Expr

ArrayPosition is array_position(list, v): the position of v in list, from 1, or NULL. list binds as one text[] parameter, so ordering by it keeps rows in a caller's order: Order(keel.ArrayPosition(ids, q.ID).Asc()).

func CastInt32

func CastInt32(e Expr) Int32Expr

CastInt32 is CAST(e AS integer).

func ConstInt32

func ConstInt32(n int32) Int32Expr

ConstInt32 writes n into the SQL.

func Int32

func Int32(n int32) Int32Expr

Int32 binds n as an integer value, $n::integer.

func Int32Of

func Int32Of(e Expr) Int32Expr

Int32Of types e as integer without changing the SQL.

func Int32Ptr

func Int32Ptr(p *int32) Int32Expr

Int32Ptr binds *p as $n::integer, NULL when p is nil.

func NullInt32

func NullInt32() Int32Expr

NullInt32 is NULL::integer.

func WidthBucket

func WidthBucket(v Int64Value, bounds []int64) Int32Expr

WidthBucket is width_bucket(v, bounds): the number of bounds, sorted ascending, that are at most v (0 below the first, len(bounds) at or above the last). bounds binds as one bigint[] parameter.

func WidthBucketRange

func WidthBucketRange(v Float64Value, lo, hi float64, buckets int) Int32Expr

WidthBucketRange is width_bucket(v, lo, hi, buckets): the bucket, from 1 to buckets, of v in buckets equal-width buckets over [lo, hi); 0 below lo and buckets+1 at or above hi.

func (Int32Expr) Abs

func (e Int32Expr) Abs() Int32Expr

Abs is abs(e).

func (Int32Expr) Add

func (e Int32Expr) Add(v Int32Value) Int32Expr

Add is (e + v).

func (Int32Expr) Asc

func (e Int32Expr) Asc() Order

func (Int32Expr) Avg

func (e Int32Expr) Avg() Agg

func (Int32Expr) Between

func (e Int32Expr) Between(lo, hi int32) Pred

func (Int32Expr) Coalesce

func (e Int32Expr) Coalesce(vs ...any) Int32Expr

Coalesce is coalesce(e, vs…).

func (Int32Expr) Desc

func (e Int32Expr) Desc() Order

func (Int32Expr) Div

func (e Int32Expr) Div(v Int32Value) Int32Expr

Div is (e / v), truncated toward zero.

func (Int32Expr) Eq

func (e Int32Expr) Eq(v int32) Pred

func (Int32Expr) EqExpr

func (e Int32Expr) EqExpr(o Int32Value) Pred

func (Int32Expr) Greatest

func (e Int32Expr) Greatest(others ...Int32Value) Int32Expr

Greatest is greatest(e, others…).

func (Int32Expr) Gt

func (e Int32Expr) Gt(v int32) Pred

func (Int32Expr) GtExpr

func (e Int32Expr) GtExpr(o Int32Value) Pred

func (Int32Expr) Gte

func (e Int32Expr) Gte(v int32) Pred

func (Int32Expr) GteExpr

func (e Int32Expr) GteExpr(o Int32Value) Pred

func (Int32Expr) In

func (e Int32Expr) In(vs ...int32) Pred

func (Int32Expr) InArray

func (e Int32Expr) InArray(a Int32ArrayValue) Pred

InArray is (e = ANY(a)).

func (Int32Expr) IsDistinctFrom

func (e Int32Expr) IsDistinctFrom(v int32) Pred

func (Int32Expr) IsDistinctFromExpr

func (e Int32Expr) IsDistinctFromExpr(o Int32Value) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (Int32Expr) IsNotDistinctFrom

func (e Int32Expr) IsNotDistinctFrom(v int32) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (Int32Expr) IsNotDistinctFromExpr

func (e Int32Expr) IsNotDistinctFromExpr(o Int32Value) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (Int32Expr) IsNotNull

func (e Int32Expr) IsNotNull() Pred

func (Int32Expr) IsNull

func (e Int32Expr) IsNull() Pred

func (Int32Expr) Least

func (e Int32Expr) Least(others ...Int32Value) Int32Expr

Least is least(e, others…).

func (Int32Expr) Lt

func (e Int32Expr) Lt(v int32) Pred

func (Int32Expr) LtExpr

func (e Int32Expr) LtExpr(o Int32Value) Pred

func (Int32Expr) Lte

func (e Int32Expr) Lte(v int32) Pred

func (Int32Expr) LteExpr

func (e Int32Expr) LteExpr(o Int32Value) Pred

func (Int32Expr) Max

func (e Int32Expr) Max() Agg

func (Int32Expr) Min

func (e Int32Expr) Min() Agg

func (Int32Expr) Mod

func (e Int32Expr) Mod(v Int32Value) Int32Expr

Mod is (e % v).

func (Int32Expr) Mul

func (e Int32Expr) Mul(v Int32Value) Int32Expr

Mul is (e * v).

func (Int32Expr) Ne

func (e Int32Expr) Ne(v int32) Pred

func (Int32Expr) NeExpr

func (e Int32Expr) NeExpr(o Int32Value) Pred

func (Int32Expr) Neg

func (e Int32Expr) Neg() Int32Expr

Neg is (-e).

func (Int32Expr) NotIn

func (e Int32Expr) NotIn(vs ...int32) Pred

func (Int32Expr) NotInArray

func (e Int32Expr) NotInArray(a Int32ArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (Int32Expr) NullIf

func (e Int32Expr) NullIf(v int32) Int32Expr

NullIf is nullif(e, v).

func (Int32Expr) Sub

func (e Int32Expr) Sub(v Int32Value) Int32Expr

Sub is (e - v).

func (Int32Expr) Sum

func (e Int32Expr) Sum() Agg

type Int32Value

type Int32Value interface {
	Expr
	// contains filtered or unexported methods
}

Int32Value is an integer value: an Int32Col or an Int32Expr.

type Int64ArrayCase

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

Int64ArrayCase builds a CASE whose result is bigint[]. Start it with CaseInt64Array.

func CaseInt64Array

func CaseInt64Array() Int64ArrayCase

CaseInt64Array starts a CASE whose result is bigint[].

func (Int64ArrayCase) Else

Else ends the CASE with ELSE v.

func (Int64ArrayCase) End

End ends the CASE without ELSE.

func (Int64ArrayCase) When

When adds WHEN cond THEN v.

type Int64ArrayCol

type Int64ArrayCol struct{ Col }

Int64ArrayCol is a bigint[] column.

func (Int64ArrayCol) Append

func (c Int64ArrayCol) Append(v int64) Assignment

Append sets the column to itself with v appended: array_append(col, v).

func (Int64ArrayCol) At

func (c Int64ArrayCol) At(i int) Int64Expr

At is (col)[i].

func (Int64ArrayCol) ContainedBy

func (c Int64ArrayCol) ContainedBy(vs ...int64) Pred

func (Int64ArrayCol) ContainedByExpr

func (c Int64ArrayCol) ContainedByExpr(o Int64ArrayValue) Pred

ContainedByExpr is (col <@ o).

func (Int64ArrayCol) ContainsAll

func (c Int64ArrayCol) ContainsAll(vs ...int64) Pred

func (Int64ArrayCol) ContainsAllExpr

func (c Int64ArrayCol) ContainsAllExpr(o Int64ArrayValue) Pred

ContainsAllExpr is (col @> o).

func (Int64ArrayCol) Has

func (c Int64ArrayCol) Has(v int64) Pred

Has reports whether the array holds v.

func (Int64ArrayCol) Len

func (c Int64ArrayCol) Len() Int32Expr

Len is cardinality(col).

func (Int64ArrayCol) Overlaps

func (c Int64ArrayCol) Overlaps(vs ...int64) Pred

func (Int64ArrayCol) OverlapsExpr

func (c Int64ArrayCol) OverlapsExpr(o Int64ArrayValue) Pred

OverlapsExpr is (col && o).

func (Int64ArrayCol) Remove

func (c Int64ArrayCol) Remove(v int64) Assignment

Remove sets the column to itself without the elements equal to v: array_remove(col, v).

func (Int64ArrayCol) To

func (c Int64ArrayCol) To(vs []int64) Assignment

func (Int64ArrayCol) Unnest

func (c Int64ArrayCol) Unnest() Int64Expr

Unnest is the set-returning unnest(col), for keel.Rows.

func (Int64ArrayCol) Without

Without is array_remove(col, v), an expression; Remove is the assignment.

type Int64ArrayExpr

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

Int64ArrayExpr is a computed bigint[] value, with the methods of StringArrayExpr.

func ArrayOfInt64

func ArrayOfInt64(es ...Int64Value) Int64ArrayExpr

ArrayOfInt64 is ARRAY[es…] of bigint values: keel.ArrayOfInt64(keel.Int64Of(keel.Count().Filter(p)), …) returns several filtered counts in one column.

func Int64Array

func Int64Array(vs []int64) Int64ArrayExpr

Int64Array binds vs as $n::bigint[].

func Int64ArrayOf

func Int64ArrayOf(e Expr) Int64ArrayExpr

Int64ArrayOf types e as bigint[].

func (Int64ArrayExpr) At

func (e Int64ArrayExpr) At(i int) Int64Expr

At is (e)[i], the i-th element from 1.

func (Int64ArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (Int64ArrayExpr) ContainedByExpr

func (e Int64ArrayExpr) ContainedByExpr(o Int64ArrayValue) Pred

ContainedByExpr is (e <@ o).

func (Int64ArrayExpr) ContainsAllExpr

func (e Int64ArrayExpr) ContainsAllExpr(o Int64ArrayValue) Pred

ContainsAllExpr is (e @> o).

func (Int64ArrayExpr) IsNotNull

func (e Int64ArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (Int64ArrayExpr) IsNull

func (e Int64ArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (Int64ArrayExpr) Len

func (e Int64ArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (Int64ArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.Int64Array(nil)) turns an empty array into NULL.

func (Int64ArrayExpr) OverlapsExpr

func (e Int64ArrayExpr) OverlapsExpr(o Int64ArrayValue) Pred

OverlapsExpr is (e && o).

func (Int64ArrayExpr) Unnest

func (e Int64ArrayExpr) Unnest() Int64Expr

Unnest is the set-returning unnest(e), for keel.Rows.

func (Int64ArrayExpr) Without

Without is array_remove(e, v).

type Int64ArrayValue

type Int64ArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

Int64ArrayValue is a bigint[] value: an Int64ArrayCol or an Int64ArrayExpr.

type Int64Case

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

Int64Case builds a CASE whose result is bigint. Start it with CaseInt64.

func CaseInt64

func CaseInt64() Int64Case

CaseInt64 starts a CASE whose result is bigint.

func (Int64Case) Else

func (b Int64Case) Else(v Int64Value) Int64Expr

Else ends the CASE with ELSE v.

func (Int64Case) End

func (b Int64Case) End() Int64Expr

End ends the CASE without ELSE.

func (Int64Case) When

func (b Int64Case) When(cond Pred, v Int64Value) Int64Case

When adds WHEN cond THEN v.

type Int64Col

type Int64Col struct{ Col }

Int64Col is a bigint column.

func (Int64Col) Abs

func (c Int64Col) Abs() Int64Expr

Abs is abs(col).

func (Int64Col) Add

func (c Int64Col) Add(v Int64Value) Int64Expr

Add is (col + v), an expression; Increment is the assignment col = col + v.

func (Int64Col) Avg

func (c Int64Col) Avg() Agg

func (Int64Col) Between

func (c Int64Col) Between(lo, hi int64) Pred

func (Int64Col) Coalesce

func (c Int64Col) Coalesce(vs ...any) Int64Expr

Coalesce is coalesce(col, vs…), typed as bigint.

func (Int64Col) Div

func (c Int64Col) Div(v Int64Value) Int64Expr

Div is (col / v), truncated toward zero.

func (Int64Col) Eq

func (c Int64Col) Eq(v int64) Pred

func (Int64Col) Greatest

func (c Int64Col) Greatest(others ...Int64Value) Int64Expr

Greatest is greatest(col, others…).

func (Int64Col) Gt

func (c Int64Col) Gt(v int64) Pred

func (Int64Col) Gte

func (c Int64Col) Gte(v int64) Pred

func (Int64Col) In

func (c Int64Col) In(vs ...int64) Pred

func (Int64Col) InArray

func (c Int64Col) InArray(a Int64ArrayValue) Pred

InArray is (col = ANY(a)).

func (Int64Col) Increment

func (c Int64Col) Increment(v int64) Assignment

func (Int64Col) IsDistinctFrom

func (c Int64Col) IsDistinctFrom(v int64) Pred

func (Int64Col) IsDistinctFromExpr

func (c Int64Col) IsDistinctFromExpr(o Int64Value) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (Int64Col) IsNotDistinctFrom

func (c Int64Col) IsNotDistinctFrom(v int64) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (Int64Col) IsNotDistinctFromExpr

func (c Int64Col) IsNotDistinctFromExpr(o Int64Value) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (Int64Col) Least

func (c Int64Col) Least(others ...Int64Value) Int64Expr

Least is least(col, others…).

func (Int64Col) Lt

func (c Int64Col) Lt(v int64) Pred

func (Int64Col) Lte

func (c Int64Col) Lte(v int64) Pred

func (Int64Col) Mod

func (c Int64Col) Mod(v Int64Value) Int64Expr

Mod is (col % v).

func (Int64Col) Mul

func (c Int64Col) Mul(v Int64Value) Int64Expr

Mul is (col * v).

func (Int64Col) Ne

func (c Int64Col) Ne(v int64) Pred

func (Int64Col) Neg

func (c Int64Col) Neg() Int64Expr

Neg is (-col).

func (Int64Col) NotIn

func (c Int64Col) NotIn(vs ...int64) Pred

func (Int64Col) NotInArray

func (c Int64Col) NotInArray(a Int64ArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (Int64Col) NullIf

func (c Int64Col) NullIf(v int64) Int64Expr

NullIf is nullif(col, v).

func (Int64Col) Sub

func (c Int64Col) Sub(v Int64Value) Int64Expr

Sub is (col - v).

func (Int64Col) Sum

func (c Int64Col) Sum() Agg

func (Int64Col) To

func (c Int64Col) To(v int64) Assignment

type Int64Expr

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

Int64Expr is a computed bigint value.

func CastInt64

func CastInt64(e Expr) Int64Expr

CastInt64 is CAST(e AS bigint) of a number of another kind (a fraction rounds to the nearest integer), of text holding an integer, or of any expression Postgres casts to bigint.

func ConstInt64

func ConstInt64(n int64) Int64Expr

ConstInt64 writes n into the SQL: 768, or (-1). Postgres types an integer constant as integer when it fits and bigint otherwise, and compares and computes it with a bigint column as written in an index (dimensions = 768).

func Epoch

func Epoch(t TimeValue) Int64Expr

Epoch is CAST(extract(epoch FROM t) AS bigint): t in Unix seconds, rounded.

func GenerateSeries

func GenerateSeries(start, stop Int64Value) Int64Expr

GenerateSeries is the set-returning generate_series(start, stop): the integers from start to stop, for keel.Rows.

func GenerateSeriesBy

func GenerateSeriesBy(start, stop, step Int64Value) Int64Expr

GenerateSeriesBy is generate_series(start, stop, step).

func Int64

func Int64(n int64) Int64Expr

Int64 binds n as a bigint value, $n::bigint.

func Int64Of

func Int64Of(e Expr) Int64Expr

Int64Of types e as bigint without changing the SQL; CastInt64 converts.

func Int64Ptr

func Int64Ptr(p *int64) Int64Expr

Int64Ptr binds *p as $n::bigint, NULL when p is nil.

func NowEpoch

func NowEpoch() Int64Expr

NowEpoch is the current time in Unix seconds, CAST(extract(epoch FROM now()) AS bigint), for models that store times as epoch seconds.

func NullInt64

func NullInt64() Int64Expr

NullInt64 is NULL::bigint.

func (Int64Expr) Abs

func (e Int64Expr) Abs() Int64Expr

Abs is abs(e).

func (Int64Expr) Add

func (e Int64Expr) Add(v Int64Value) Int64Expr

Add is (e + v).

func (Int64Expr) Asc

func (e Int64Expr) Asc() Order

func (Int64Expr) Avg

func (e Int64Expr) Avg() Agg

func (Int64Expr) Between

func (e Int64Expr) Between(lo, hi int64) Pred

func (Int64Expr) Coalesce

func (e Int64Expr) Coalesce(vs ...any) Int64Expr

Coalesce is coalesce(e, vs…): each v an integer bound as a parameter or an expression, taken in order where everything before it is NULL.

func (Int64Expr) Desc

func (e Int64Expr) Desc() Order

func (Int64Expr) Div

func (e Int64Expr) Div(v Int64Value) Int64Expr

Div is (e / v), truncated toward zero.

func (Int64Expr) Eq

func (e Int64Expr) Eq(v int64) Pred

func (Int64Expr) EqExpr

func (e Int64Expr) EqExpr(o Int64Value) Pred

func (Int64Expr) Greatest

func (e Int64Expr) Greatest(others ...Int64Value) Int64Expr

Greatest is greatest(e, others…), the largest non-NULL value.

func (Int64Expr) Gt

func (e Int64Expr) Gt(v int64) Pred

func (Int64Expr) GtExpr

func (e Int64Expr) GtExpr(o Int64Value) Pred

func (Int64Expr) Gte

func (e Int64Expr) Gte(v int64) Pred

func (Int64Expr) GteExpr

func (e Int64Expr) GteExpr(o Int64Value) Pred

func (Int64Expr) In

func (e Int64Expr) In(vs ...int64) Pred

func (Int64Expr) InArray

func (e Int64Expr) InArray(a Int64ArrayValue) Pred

InArray is (e = ANY(a)).

func (Int64Expr) IsDistinctFrom

func (e Int64Expr) IsDistinctFrom(v int64) Pred

func (Int64Expr) IsDistinctFromExpr

func (e Int64Expr) IsDistinctFromExpr(o Int64Value) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (Int64Expr) IsNotDistinctFrom

func (e Int64Expr) IsNotDistinctFrom(v int64) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (Int64Expr) IsNotDistinctFromExpr

func (e Int64Expr) IsNotDistinctFromExpr(o Int64Value) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (Int64Expr) IsNotNull

func (e Int64Expr) IsNotNull() Pred

func (Int64Expr) IsNull

func (e Int64Expr) IsNull() Pred

func (Int64Expr) Least

func (e Int64Expr) Least(others ...Int64Value) Int64Expr

Least is least(e, others…), the smallest non-NULL value.

func (Int64Expr) Lt

func (e Int64Expr) Lt(v int64) Pred

func (Int64Expr) LtExpr

func (e Int64Expr) LtExpr(o Int64Value) Pred

func (Int64Expr) Lte

func (e Int64Expr) Lte(v int64) Pred

func (Int64Expr) LteExpr

func (e Int64Expr) LteExpr(o Int64Value) Pred

func (Int64Expr) Max

func (e Int64Expr) Max() Agg

func (Int64Expr) Min

func (e Int64Expr) Min() Agg

func (Int64Expr) Mod

func (e Int64Expr) Mod(v Int64Value) Int64Expr

Mod is (e % v), the remainder, with the sign of e.

func (Int64Expr) Mul

func (e Int64Expr) Mul(v Int64Value) Int64Expr

Mul is (e * v).

func (Int64Expr) Ne

func (e Int64Expr) Ne(v int64) Pred

func (Int64Expr) NeExpr

func (e Int64Expr) NeExpr(o Int64Value) Pred

func (Int64Expr) Neg

func (e Int64Expr) Neg() Int64Expr

Neg is (-e).

func (Int64Expr) NotIn

func (e Int64Expr) NotIn(vs ...int64) Pred

func (Int64Expr) NotInArray

func (e Int64Expr) NotInArray(a Int64ArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (Int64Expr) NullIf

func (e Int64Expr) NullIf(v int64) Int64Expr

NullIf is nullif(e, v): NULL where e equals v, for a divisor that may be zero.

func (Int64Expr) Sub

func (e Int64Expr) Sub(v Int64Value) Int64Expr

Sub is (e - v).

func (Int64Expr) Sum

func (e Int64Expr) Sum() Agg

type Int64Range

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

Int64Range is a bigint range, built by Int8Range.

func Int8Range

func Int8Range(lo, hi Int64Value) Int64Range

Int8Range is int8range(lo, hi): the bigints from lo, included, to hi, excluded; a NULL bound is unbounded.

func (Int64Range) Covers

func (g Int64Range) Covers(v Int64Value) Pred

Covers is g @> v: v lies in the range.

func (Int64Range) Overlaps

func (g Int64Range) Overlaps(o Int64Range) Pred

Overlaps is g && o: the ranges share a value.

type Int64Value

type Int64Value interface {
	Expr
	// contains filtered or unexported methods
}

Int64Value is a bigint value: an Int64Col or an Int64Expr.

type Intercepted

type Intercepted struct {
	Op    Op
	Read  bool
	Table *schema.Table
	// contains filtered or unexported fields
}

Intercepted is the statement an interceptor sees: its Op (OpRead, OpUpdate or OpDelete), whether it is a read, and its base table.

func (*Intercepted) Where

func (i *Intercepted) Where(ps ...Pred)

Where adds predicates to the intercepted statement.

type Interceptor

type Interceptor func(ctx context.Context, q *Intercepted) error

Interceptor sees a statement on a table before it is rendered and may add predicates to its WHERE clause. It sees every statement whose base is the table and that has a WHERE clause:

  • reads: every query terminal and Batch item, Page's total, projections, ToSQL, to-many relation loads, Any and None, subqueries, and the source query of InsertFrom (OpRead);
  • Save and bulk updates (OpUpdate);
  • DeleteRow and bulk deletes (OpDelete).

On Save and DeleteRow the predicates join the primary key, so a row they exclude is not written. Interceptors never see inserts (Create, CreateMany, Copy, Upsert, CreateOrSkip and the INSERT of InsertFrom), including the update an Upsert applies to a conflicting row, or tables joined into a statement through a relation path.

type IntervalCase

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

IntervalCase builds a CASE whose result is an interval. Start it with CaseInterval.

func CaseInterval

func CaseInterval() IntervalCase

CaseInterval starts a CASE whose result is an interval.

func (IntervalCase) Else

Else ends the CASE with ELSE v.

func (IntervalCase) End

func (b IntervalCase) End() IntervalExpr

End ends the CASE without ELSE.

func (IntervalCase) When

func (b IntervalCase) When(cond Pred, v IntervalValue) IntervalCase

When adds WHEN cond THEN v.

type IntervalExpr

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

IntervalExpr is a computed interval: the difference of two times, a bound interval.

func CastInterval

func CastInterval(e Expr) IntervalExpr

CastInterval is CAST(e AS interval): interval text as an interval.

func Duration

func Duration(d time.Duration) IntervalExpr

Duration binds d as $n::interval, in microseconds, Postgres's resolution.

func Interval

func Interval(s string) IntervalExpr

Interval binds s, an interval in Postgres's input syntax ("1 day", "36 hours", "P1DT12H"), as $n::interval.

func IntervalOf

func IntervalOf(e Expr) IntervalExpr

IntervalOf types e as an interval without changing the SQL.

func NullInterval

func NullInterval() IntervalExpr

NullInterval is NULL::interval.

func (IntervalExpr) Add

Add is (e + o).

func (IntervalExpr) Asc

func (e IntervalExpr) Asc() Order

Asc orders by e ascending.

func (IntervalExpr) Avg

func (e IntervalExpr) Avg() Agg

Avg is the aggregate avg(e).

func (IntervalExpr) Desc

func (e IntervalExpr) Desc() Order

Desc orders by e descending.

func (IntervalExpr) Div

Div is (e / n).

func (IntervalExpr) EqExpr

func (e IntervalExpr) EqExpr(o IntervalValue) Pred

EqExpr is e = o.

func (IntervalExpr) GtExpr

func (e IntervalExpr) GtExpr(o IntervalValue) Pred

GtExpr is e > o.

func (IntervalExpr) GteExpr

func (e IntervalExpr) GteExpr(o IntervalValue) Pred

GteExpr is e >= o.

func (IntervalExpr) IsDistinctFromExpr

func (e IntervalExpr) IsDistinctFromExpr(o IntervalValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (IntervalExpr) IsNotDistinctFromExpr

func (e IntervalExpr) IsNotDistinctFromExpr(o IntervalValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (IntervalExpr) IsNotNull

func (e IntervalExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (IntervalExpr) IsNull

func (e IntervalExpr) IsNull() Pred

IsNull is e IS NULL.

func (IntervalExpr) LtExpr

func (e IntervalExpr) LtExpr(o IntervalValue) Pred

LtExpr is e < o.

func (IntervalExpr) LteExpr

func (e IntervalExpr) LteExpr(o IntervalValue) Pred

LteExpr is e <= o.

func (IntervalExpr) Max

func (e IntervalExpr) Max() Agg

Max is the aggregate max(e).

func (IntervalExpr) Min

func (e IntervalExpr) Min() Agg

Min is the aggregate min(e).

func (IntervalExpr) Mul

Mul is (e * n): keel.Interval("1 day").Mul(c.GraceDays) is that many days.

func (IntervalExpr) NeExpr

func (e IntervalExpr) NeExpr(o IntervalValue) Pred

NeExpr is e <> o.

func (IntervalExpr) Neg

func (e IntervalExpr) Neg() IntervalExpr

Neg is (-e).

func (IntervalExpr) Seconds

func (e IntervalExpr) Seconds() Float64Expr

Seconds is date_part('epoch', e): the interval's length in seconds, months counted as 30 days.

func (IntervalExpr) Sub

Sub is (e - o).

func (IntervalExpr) Sum

func (e IntervalExpr) Sum() Agg

Sum is the aggregate sum(e).

type IntervalValue

type IntervalValue interface {
	Expr
	// contains filtered or unexported methods
}

IntervalValue is an interval: an IntervalExpr (keel.Interval, keel.Duration, TimeExpr.Since, a typed view).

type Isolation

type Isolation uint8

Isolation is a transaction isolation level. The zero value leaves the level to the server's default_transaction_isolation, READ COMMITTED unless the server is configured otherwise. A level is a TxOption, and Config.ReadIsolation sets the level of explicit Read transactions that do not pass one.

const (
	// ReadCommitted runs the transaction at READ COMMITTED: each statement sees what was committed
	// before it began.
	ReadCommitted Isolation = iota + 1
	// RepeatableRead runs the transaction at REPEATABLE READ: every statement sees the snapshot
	// taken by the transaction's first statement. A read-only transaction at this level reads one
	// consistent snapshot across all its statements.
	RepeatableRead
	// Serializable runs the transaction at SERIALIZABLE. Combine it with Retry: a serialization
	// failure is expected under contention.
	Serializable
)

The isolation levels keel begins transactions with.

func (Isolation) String

func (l Isolation) String() string

String returns the level as SQL names it, or "default" for the zero value.

type JSONBCase

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

JSONBCase builds a CASE whose result is jsonb. Start it with CaseJSONB.

func CaseJSONB

func CaseJSONB() JSONBCase

CaseJSONB starts a CASE whose result is jsonb.

func (JSONBCase) Else

func (b JSONBCase) Else(v JSONBValue) JSONBExpr

Else ends the CASE with ELSE v.

func (JSONBCase) End

func (b JSONBCase) End() JSONBExpr

End ends the CASE without ELSE.

func (JSONBCase) When

func (b JSONBCase) When(cond Pred, v JSONBValue) JSONBCase

When adds WHEN cond THEN v.

type JSONBCol

type JSONBCol struct{ Col }

JSONBCol is a jsonb column.

func (JSONBCol) ArrayElementsText

func (c JSONBCol) ArrayElementsText() StringExpr

ArrayElementsText is jsonb_array_elements_text(col), for keel.Rows.

func (JSONBCol) Coalesce

func (c JSONBCol) Coalesce(v any) JSONBExpr

Coalesce is coalesce(col, vs…), typed as jsonb; a value v is encoded as JSON.

func (JSONBCol) Concat

func (c JSONBCol) Concat(o JSONBValue) JSONBExpr

Concat is col || o.

func (JSONBCol) Contains

func (c JSONBCol) Contains(v any) Pred

Contains is col @> v, with v encoded as JSON.

func (JSONBCol) HasAllKeys

func (c JSONBCol) HasAllKeys(keys ...string) Pred

HasAllKeys is col ?& keys.

func (JSONBCol) HasAnyKey

func (c JSONBCol) HasAnyKey(keys ...string) Pred

HasAnyKey is col ?| keys.

func (JSONBCol) HasKey

func (c JSONBCol) HasKey(key string) Pred

HasKey is col ? key.

func (JSONBCol) IsDistinctFromExpr

func (c JSONBCol) IsDistinctFromExpr(o JSONBValue) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (JSONBCol) IsNotDistinctFromExpr

func (c JSONBCol) IsNotDistinctFromExpr(o JSONBValue) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (JSONBCol) Path

func (c JSONBCol) Path(path ...string) DynExpr

Path is col #> path: the value at path as jsonb, NULL when the path is missing; no path is the whole document.

func (JSONBCol) Set

func (c JSONBCol) Set(path []string, v JSONBValue) JSONBExpr

Set is jsonb_set(col, path, v); assign it with ToExpr.

func (JSONBCol) Text

func (c JSONBCol) Text(path ...string) StringExpr

Text is col #>> path: the value at path as text.

func (JSONBCol) To

func (c JSONBCol) To(v any) Assignment

To sets the column to v encoded as JSON.

func (JSONBCol) TypeOf

func (c JSONBCol) TypeOf() StringExpr

TypeOf is jsonb_typeof(col).

type JSONBExpr

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

JSONBExpr is a computed jsonb value: a built object or array, a document with a value set.

func CastJSONB

func CastJSONB(e Expr) JSONBExpr

CastJSONB is CAST(e AS jsonb): JSON text or a json value as jsonb.

func JSONB

func JSONB(v any) JSONBExpr

JSONB binds v encoded as JSON (with the DB's JSON codec) as $n::jsonb. A []byte or json.RawMessage is already JSON and binds as it is.

func JSONBOf

func JSONBOf(e Expr) JSONBExpr

JSONBOf types e as jsonb without changing the SQL.

func JSONBuildArray

func JSONBuildArray(vs ...any) JSONBExpr

JSONBuildArray is jsonb_build_array(vs…); no values is the empty array.

func JSONBuildObject

func JSONBuildObject(pairs ...JSONPair) JSONBExpr

JSONBuildObject is jsonb_build_object(key, value, …), every key bound as text.

func NullJSONB

func NullJSONB() JSONBExpr

NullJSONB is NULL::jsonb, SQL NULL rather than the JSON null keel.JSONB(nil) binds.

func (JSONBExpr) ArrayElementsText

func (e JSONBExpr) ArrayElementsText() StringExpr

ArrayElementsText is the set-returning jsonb_array_elements_text(e): one text row per element of the array e, for keel.Rows.

func (JSONBExpr) Coalesce

func (e JSONBExpr) Coalesce(v any) JSONBExpr

Coalesce is coalesce(e, v): v, an expression or a value encoded as JSON, where e is NULL.

func (JSONBExpr) Concat

func (e JSONBExpr) Concat(o JSONBValue) JSONBExpr

Concat is e || o: the objects merged (o's keys win) or the arrays concatenated.

func (JSONBExpr) Contains

func (e JSONBExpr) Contains(v any) Pred

Contains is e @> v, with v encoded as JSON.

func (JSONBExpr) ContainsExpr

func (e JSONBExpr) ContainsExpr(o JSONBValue) Pred

ContainsExpr is e @> o.

func (JSONBExpr) HasKey

func (e JSONBExpr) HasKey(key string) Pred

HasKey is e ? key.

func (JSONBExpr) IsDistinctFromExpr

func (e JSONBExpr) IsDistinctFromExpr(o JSONBValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (JSONBExpr) IsNotDistinctFromExpr

func (e JSONBExpr) IsNotDistinctFromExpr(o JSONBValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (JSONBExpr) IsNotNull

func (e JSONBExpr) IsNotNull() Pred

func (JSONBExpr) IsNull

func (e JSONBExpr) IsNull() Pred

func (JSONBExpr) Set

func (e JSONBExpr) Set(path []string, v JSONBValue) JSONBExpr

Set is jsonb_set(e, path, v): e with the value at path replaced by v, created when missing.

func (JSONBExpr) Text

func (e JSONBExpr) Text(path ...string) StringExpr

Text is e #>> path: the value at path as text.

func (JSONBExpr) TypeOf

func (e JSONBExpr) TypeOf() StringExpr

TypeOf is jsonb_typeof(e): "object", "array", "string", "number", "boolean" or "null".

type JSONBValue

type JSONBValue interface {
	Expr
	// contains filtered or unexported methods
}

JSONBValue is a jsonb value: a JSONBCol or a JSONBExpr.

type JSONCodec

type JSONCodec struct {
	Marshal   func(v any) ([]byte, error)
	Unmarshal func(data []byte, v any) error
	// UnmarshalNumber decodes the columns tagged jsonnumber: as Unmarshal, except that a number
	// decoded into an interface value (map[string]any, []any, any) is a json.Number holding its
	// literal instead of a float64, so integers past 2^53 keep every digit. With sonic,
	// sonic.Config{UseNumber: true}.Froze().Unmarshal.
	UnmarshalNumber func(data []byte, v any) error
}

JSONCodec is a JSON implementation.

type JSONPair

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

JSONPair is one key and value of JSONBuildObject.

func Pair

func Pair(key string, v any) JSONPair

Pair is the key and value of one member of a JSONBuildObject: v is an expression or a Go value bound under the cast its type implies (use keel.JSONB for a document).

type LoadStrategy

type LoadStrategy uint8

LoadStrategy is how a read loads its to-many relations, many-to-many relations and the picks it does not join: Config.LoadStrategy sets the default and Query.LoadStrategy chooses for one query and the loads below it.

const (
	// LoadPipelined sends the read's statement and the statement of every level of its relation
	// tree in one round trip. A level selects its parents' keys with a subquery over the parent
	// level's own statement (its filters, joins, order, limit, offset and keyset), nested for
	// deeper levels, so it needs no keys read back. It runs only where every statement reads one
	// snapshot: in a transaction at RepeatableRead or Serializable, or outside a transaction,
	// where keel runs the read in an implicit REPEATABLE READ READ ONLY transaction of its own (in
	// the same round trip). A level whose parent level's rows are not the same every time its
	// statement runs loads per level (see the guide's "One round trip"). It is the default.
	LoadPipelined LoadStrategy = iota + 1
	// LoadPerLevel sends one round trip per level: each level reads its parents' keys from the
	// rows of the level before it and binds them as arrays (= ANY($1), unnest).
	LoadPerLevel
)

type Loader

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

Loader is a relation path to load, optionally narrowed: a relation handle (s.Customer()) or a query started from one (m.Order(m.Sequence.Asc())).

type LocalCache

type LocalCache interface {
	// Get returns the value stored under key and true, or false when there is none or it expired.
	Get(key string) ([]byte, bool)
	// Set stores value under key for ttl. A store may refuse a value, which is then a miss.
	Set(key string, value []byte, ttl time.Duration)
	// Bytes returns the bytes the store holds, for CacheStats.
	Bytes() int64
}

LocalCache is the in-process store of the result cache (CacheConfig.Local): encoded entries by key, each with a time to live. Values it returns are only read, never changed; every hit decodes them into new rows. keel.MemoryCache is the implementation keel provides.

func MemoryCache

func MemoryCache(maxBytes int64) LocalCache

MemoryCache returns a LocalCache of at most maxBytes bytes of keys, values and bookkeeping, analytics' otter-backed memory store: a full store evicts the entries least likely to be read again, and a value larger than maxBytes is not kept. A maxBytes of zero or less is DefaultCacheBytes.

type LockStrength

type LockStrength string

LockStrength is the row lock a query takes.

type LockWait

type LockWait string

LockWait is what a locking query does when a row is already locked.

type Mutation

type Mutation[T any] struct {
	Op Op
	// Row is the row as written: the entity passed to an entity write, or the row returned by a
	// bulk write. For DeleteRow it is the entity passed, which may hold only the primary key.
	Row T
	// Old holds the stored values, before the write, of the columns the table's hooks declared
	// with Needs: for every update (Save, an Upsert's conflicting rows, bulk updates) and every
	// delete (DeleteRow, bulk deletes). Other fields are zero. Nil for creates, and when no hook
	// of the table declared Needs.
	Old T
	// contains filtered or unexported fields
}

Mutation is one row written, as a table hook sees it.

func (*Mutation[T]) Changed

func (m *Mutation[T]) Changed(c Column) bool

Changed reports whether the write set column c. Creates set every column, an Upsert's conflicting rows the columns its Conflict assigns, and deletes none.

type NumberValue

type NumberValue interface {
	Expr
	// contains filtered or unexported methods
}

NumberValue is any number: a smallint, integer, bigint, double precision or scanner column, or an Int32Expr, Int64Expr, Float64Expr or NumericExpr.

type NumericArrayCase

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

NumericArrayCase builds a CASE whose result is numeric[]. Start it with CaseNumericArray.

func CaseNumericArray

func CaseNumericArray() NumericArrayCase

CaseNumericArray starts a CASE whose result is numeric[].

func (NumericArrayCase) Else

Else ends the CASE with ELSE v.

func (NumericArrayCase) End

End ends the CASE without ELSE.

func (NumericArrayCase) When

When adds WHEN cond THEN v.

type NumericArrayExpr

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

NumericArrayExpr is a computed numeric[] value, with the methods of StringArrayExpr.

func ArrayOfNumeric

func ArrayOfNumeric(es ...NumericValue) NumericArrayExpr

ArrayOfNumeric is ARRAY[es…] of numeric values.

func NumericArray

func NumericArray(vs any) NumericArrayExpr

NumericArray binds vs, a slice of decimals or of any values Postgres reads as numeric, as $n::numeric[].

func NumericArrayOf

func NumericArrayOf(e Expr) NumericArrayExpr

NumericArrayOf types e as numeric[].

func (NumericArrayExpr) At

At is (e)[i], the i-th element from 1.

func (NumericArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (NumericArrayExpr) ContainedByExpr

func (e NumericArrayExpr) ContainedByExpr(o NumericArrayValue) Pred

ContainedByExpr is (e <@ o).

func (NumericArrayExpr) ContainsAllExpr

func (e NumericArrayExpr) ContainsAllExpr(o NumericArrayValue) Pred

ContainsAllExpr is (e @> o).

func (NumericArrayExpr) IsNotNull

func (e NumericArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (NumericArrayExpr) IsNull

func (e NumericArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (NumericArrayExpr) Len

func (e NumericArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (NumericArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.NumericArray(nil)) turns an empty array into NULL.

func (NumericArrayExpr) OverlapsExpr

func (e NumericArrayExpr) OverlapsExpr(o NumericArrayValue) Pred

OverlapsExpr is (e && o).

func (NumericArrayExpr) Unnest

func (e NumericArrayExpr) Unnest() NumericExpr

Unnest is the set-returning unnest(e), for keel.Rows.

func (NumericArrayExpr) Without

Without is array_remove(e, v).

type NumericArrayValue

type NumericArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

NumericArrayValue is a numeric[] value: a NumericArrayExpr.

type NumericCase

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

NumericCase builds a CASE whose result is numeric. Start it with CaseNumeric.

func CaseNumeric

func CaseNumeric() NumericCase

CaseNumeric starts a CASE whose result is numeric.

func (NumericCase) Else

Else ends the CASE with ELSE v.

func (NumericCase) End

func (b NumericCase) End() NumericExpr

End ends the CASE without ELSE.

func (NumericCase) When

func (b NumericCase) When(cond Pred, v NumericValue) NumericCase

When adds WHEN cond THEN v.

type NumericExpr

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

NumericExpr is a computed numeric value. Its comparisons take any value Postgres reads as numeric (a decimal type, an integer, a float), as an AnyCol's do.

func CastNumeric

func CastNumeric(e Expr) NumericExpr

CastNumeric is CAST(e AS numeric) of a number or of text holding one: exact arithmetic and rounding to decimal places.

func ConstNumeric

func ConstNumeric(s string) NumericExpr

ConstNumeric writes s, a decimal such as "0.035" or "-12", into the SQL as '0.035'::numeric. A string that is not an optionally signed decimal makes an expression that fails.

func NullNumeric

func NullNumeric() NumericExpr

NullNumeric is NULL::numeric.

func Numeric

func Numeric(v any) NumericExpr

Numeric binds v, a decimal or any value Postgres reads as numeric, as $n::numeric.

func NumericOf

func NumericOf(e Expr) NumericExpr

NumericOf types e as numeric without changing the SQL; CastNumeric converts.

func (NumericExpr) Abs

func (e NumericExpr) Abs() NumericExpr

Abs is abs(e).

func (NumericExpr) Add

Add is (e + v).

func (NumericExpr) Asc

func (e NumericExpr) Asc() Order

func (NumericExpr) Avg

func (e NumericExpr) Avg() Agg

func (NumericExpr) Between

func (e NumericExpr) Between(lo, hi any) Pred

func (NumericExpr) Ceil

func (e NumericExpr) Ceil() NumericExpr

Ceil is ceil(e).

func (NumericExpr) Coalesce

func (e NumericExpr) Coalesce(vs ...any) NumericExpr

Coalesce is coalesce(e, vs…).

func (NumericExpr) Desc

func (e NumericExpr) Desc() Order

func (NumericExpr) Div

Div is (e / v).

func (NumericExpr) Eq

func (e NumericExpr) Eq(v any) Pred

func (NumericExpr) EqExpr

func (e NumericExpr) EqExpr(o NumericValue) Pred

func (NumericExpr) Exp

func (e NumericExpr) Exp() NumericExpr

Exp is exp(e).

func (NumericExpr) Floor

func (e NumericExpr) Floor() NumericExpr

Floor is floor(e).

func (NumericExpr) Greatest

func (e NumericExpr) Greatest(others ...NumericValue) NumericExpr

Greatest is greatest(e, others…).

func (NumericExpr) Gt

func (e NumericExpr) Gt(v any) Pred

func (NumericExpr) GtExpr

func (e NumericExpr) GtExpr(o NumericValue) Pred

func (NumericExpr) Gte

func (e NumericExpr) Gte(v any) Pred

func (NumericExpr) GteExpr

func (e NumericExpr) GteExpr(o NumericValue) Pred

func (NumericExpr) InArray

func (e NumericExpr) InArray(a NumericArrayValue) Pred

InArray is (e = ANY(a)).

func (NumericExpr) IsDistinctFrom

func (e NumericExpr) IsDistinctFrom(v any) Pred

func (NumericExpr) IsDistinctFromExpr

func (e NumericExpr) IsDistinctFromExpr(o NumericValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (NumericExpr) IsNotDistinctFrom

func (e NumericExpr) IsNotDistinctFrom(v any) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (NumericExpr) IsNotDistinctFromExpr

func (e NumericExpr) IsNotDistinctFromExpr(o NumericValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (NumericExpr) IsNotNull

func (e NumericExpr) IsNotNull() Pred

func (NumericExpr) IsNull

func (e NumericExpr) IsNull() Pred

func (NumericExpr) Least

func (e NumericExpr) Least(others ...NumericValue) NumericExpr

Least is least(e, others…).

func (NumericExpr) Ln

func (e NumericExpr) Ln() NumericExpr

Ln is ln(e), the natural logarithm.

func (NumericExpr) Lt

func (e NumericExpr) Lt(v any) Pred

func (NumericExpr) LtExpr

func (e NumericExpr) LtExpr(o NumericValue) Pred

func (NumericExpr) Lte

func (e NumericExpr) Lte(v any) Pred

func (NumericExpr) LteExpr

func (e NumericExpr) LteExpr(o NumericValue) Pred

func (NumericExpr) Max

func (e NumericExpr) Max() Agg

func (NumericExpr) Min

func (e NumericExpr) Min() Agg

func (NumericExpr) Mod

Mod is (e % v).

func (NumericExpr) Mul

Mul is (e * v).

func (NumericExpr) Ne

func (e NumericExpr) Ne(v any) Pred

func (NumericExpr) NeExpr

func (e NumericExpr) NeExpr(o NumericValue) Pred

func (NumericExpr) Neg

func (e NumericExpr) Neg() NumericExpr

Neg is (-e).

func (NumericExpr) NotInArray

func (e NumericExpr) NotInArray(a NumericArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (NumericExpr) NullIf

func (e NumericExpr) NullIf(v any) NumericExpr

NullIf is nullif(e, v).

func (NumericExpr) Round

func (e NumericExpr) Round() NumericExpr

Round is round(e), to the nearest integer, halves away from zero.

func (NumericExpr) RoundTo

func (e NumericExpr) RoundTo(places int) NumericExpr

RoundTo is round(e, places): e rounded to places decimal places (to tens, hundreds, … when places is negative).

func (NumericExpr) Sub

Sub is (e - v).

func (NumericExpr) Sum

func (e NumericExpr) Sum() Agg

type NumericValue

type NumericValue interface {
	Expr
	// contains filtered or unexported methods
}

NumericValue is a numeric value: a scanner column (AnyCol, how decimal columns are typed) or a NumericExpr.

type Observer

type Observer interface {
	BeforeQuery(ctx context.Context, e *QueryEvent) context.Context
	AfterQuery(ctx context.Context, e *QueryEvent)
}

Observer sees every statement keel sends: queries, relation loads, writes, COPY, savepoints and advisory locks, but not BEGIN, the Config.OnBegin statements, plan-mode switches or COMMIT.

For each statement keel calls every observer's BeforeQuery in order, the first with the terminal's context and each later one with the context the previous one returned. The context the last one returns is the statement's context: AfterQuery gets it, observers in reverse order, so an observer can start a span in BeforeQuery and end it in AfterQuery. A round trip that carries one statement runs under that statement's context, so a deadline, cancellation or value an observer adds reaches the execution. Statements sent in one round trip (a Batch, the statements of one level of relation loads, the chunks of a CreateMany) each start from the terminal's context, so their contexts are siblings; the round trip itself runs under the terminal's context, and the statements share a start time and a duration.

Example
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log"
	"os"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func newID(prefix string) string {
	b := make([]byte, 10)
	_, _ = rand.Read(b)
	return prefix + hex.EncodeToString(b)
}

// slowQueryLog is an Observer that logs statements slower than threshold.
type slowQueryLog struct{ threshold time.Duration }

func (slowQueryLog) BeforeQuery(ctx context.Context, _ *keel.QueryEvent) context.Context {
	return ctx
}

func (l slowQueryLog) AfterQuery(_ context.Context, e *keel.QueryEvent) {
	if e.Duration >= l.threshold {
		log.Printf("slow %s on %s (%d args, %d rows): %v: %s",
			e.Op, e.Table, e.Args, e.Rows, e.Duration, e.SQL)
	}
}

// traceKey carries a tracingObserver's span on a statement's context.
type traceKey struct{}

// statementSpan is the span of one statement.
type statementSpan struct {
	name  string
	start time.Time
}

// tracingObserver starts a span in BeforeQuery and ends the span it finds on the statement's
// context in AfterQuery, as a tracing library's observer would.
type tracingObserver struct{}

func (tracingObserver) BeforeQuery(ctx context.Context, e *keel.QueryEvent) context.Context {
	return context.WithValue(ctx, traceKey{}, &statementSpan{name: e.Op + " " + e.Table, start: time.Now()})
}

func (tracingObserver) AfterQuery(ctx context.Context, e *keel.QueryEvent) {
	sp, ok := ctx.Value(traceKey{}).(*statementSpan)
	if !ok {
		return
	}
	log.Printf("span %s: %v, %d rows, error %v", sp.name, time.Since(sp.start), e.Rows, e.Err)
}

// appConfig is the configuration of a multi-tenant application.
func appConfig(dsn string) keel.Config {
	return keel.Config{
		DSN:           dsn,
		TenantColumns: []string{"organization_id", "business_unit_id"},
		Tenant: func(ctx context.Context) ([]any, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			if !ok || sc.SystemReason != "" {
				return nil, false
			}
			return []any{sc.OrganizationID, sc.BusinessUnitID}, true
		},
		System: func(ctx context.Context) (string, bool) {
			sc, ok := ctx.Value(scopeKey{}).(scope)
			return sc.SystemReason, ok && sc.SystemReason != ""
		},
		OnBegin: func(ctx context.Context, tx keel.TxInfo) ([]keel.Statement, error) {
			sc, _ := ctx.Value(scopeKey{}).(scope)
			return []keel.Statement{{
				SQL:  "SELECT set_config('app.organization_id', $1, true)",
				Args: []any{string(sc.OrganizationID)},
			}}, nil
		},
		NewID:     newID,
		Observers: []keel.Observer{slowQueryLog{threshold: 200 * time.Millisecond}},
	}
}

func main() {
	cfg := appConfig(os.Getenv("DATABASE_URL"))
	cfg.Observers = append(cfg.Observers, tracingObserver{})
	db, err := keel.Open(context.Background(), cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

func RecordShapes

func RecordShapes(w io.Writer) Observer

RecordShapes returns an observer that writes each distinct statement text it sees to w once, as a JSON line of a Shape. Only statements EXPLAIN can plan are written (those that start with SELECT, WITH, INSERT, UPDATE, DELETE, MERGE, VALUES or TABLE, and not keel's advisory locks), so savepoints, COPY, DDL run through Exec and Explain's own statements are left out. It is safe for concurrent use: a statement already seen costs a read-locked map lookup, and each line goes to w in one Write under a lock, so lines never interleave. It keeps every distinct text in memory, which suits a test run or a staging soak, the runs a plan guard records; write errors are dropped, and RecordShapesFile reports them.

func RecordShapesFile

func RecordShapesFile(path string) (Observer, func() error, error)

RecordShapesFile returns an observer that appends the shapes it sees to the file at path, creating it, as RecordShapes writes them, and a function that closes the file and returns the first write error. The file is opened for appending, so the test binaries of one go test ./... can share it: each line is one write, and keel plans reads each distinct text once.

type Op

type Op uint8

Op is the kind of write a hook sees.

const (
	OpCreate Op = iota
	OpUpdate
	OpDelete
	// OpRead is the Op an interceptor sees for queries, loads and subqueries.
	OpRead
)

func (Op) String

func (o Op) String() string

type Order

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

Order is one ORDER BY term.

func Asc

func Asc(e Expr) Order

Asc orders by e ascending.

func Desc

func Desc(e Expr) Order

Desc orders by e descending.

func (Order) NullsFirst

func (o Order) NullsFirst() Order

NullsFirst sorts NULL values first.

func (Order) NullsLast

func (o Order) NullsLast() Order

NullsLast sorts NULL values last.

type Page

type Page[T any] struct {
	Items []T
	// Next is the cursor of the following page, empty on the last page.
	Next string
	// Total is the number of matching rows when the request asked for it. It is exact up to the
	// request's limit; when more rows match it is the limit, or, when the request asked for an
	// Estimate, the planner's estimate (never less than the limit plus one).
	Total int64
	// TotalExact reports that Total is the exact number of matching rows. It is false when the
	// request asked for no total and when more rows matched than the request's limit.
	TotalExact bool
	// contains filtered or unexported fields
}

Page is one keyset page.

func (Page[T]) CursorAt

func (p Page[T]) CursorAt(i int) string

CursorAt returns the cursor that resumes the query after Items[i]: After(p.CursorAt(i), n) reads the n rows that follow it. It is the cursor format Next uses (Next is the cursor of the last item when a page follows), computed from the row's order values without a statement, for APIs that give every row a cursor, such as a GraphQL connection's edges. It panics when i is out of range, as indexing Items does, and returns "" on a Page that Page did not read.

type PageRequest

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

PageRequest asks for one keyset page.

func After

func After(cursor string, size int) PageRequest

After asks for the page of size rows that follows cursor. An empty cursor asks for the first page.

func (PageRequest) Estimate

func (p PageRequest) Estimate() PageRequest

Estimate makes a total that passes Total's limit the planner's estimate of the matching rows, as CountEstimate reads it: EXPLAIN (FORMAT JSON) of the counted rows rides in the page's round trip. A list showing "about 1.2M" pays for counting at most limit+1 rows. It needs Total.

func (PageRequest) Total

func (p PageRequest) Total(limit int64) PageRequest

Total asks for the number of matching rows, counted exactly up to limit, in the same round trip. When more rows match, Page.Total is limit and Page.TotalExact is false; Estimate makes it the planner's estimate instead.

type Phase

type Phase uint8

Phase is when a table hook runs.

const (
	// BeforeWrite runs inside the transaction before an entity write (Create, CreateMany, Copy,
	// Save, Upsert, CreateOrSkip, DeleteRow). It may change the row or abort the write by returning
	// an error. Bulk writes (Query.Update, Query.Delete) do not run BeforeWrite hooks.
	BeforeWrite Phase = iota
	// AfterWrite runs inside the transaction after the statement, for entity and bulk writes. An error
	// rolls the transaction back: outside a transaction, a write that may run AfterWrite hooks runs in
	// an implicit one, and the hook's context carries it, so what the hook runs through keel (Exec,
	// QueryRows, reads and writes) joins the write and rolls back with it. BeforeWrite hooks get the
	// same context when the write runs in an implicit transaction.
	AfterWrite
	// Committed runs after the transaction commits. Errors are not returned to the caller;
	// report them from the hook.
	Committed
)

type Plan

type Plan struct {
	// TotalCost is the planner's estimated cost of the whole statement, in its cost units.
	TotalCost float64
	// Rows is the planner's estimate of the rows the statement returns.
	Rows float64
	// ActualRows is the number of rows the statement returned; set only with Analyze, as are the
	// other measured fields.
	ActualRows float64
	// ActualTime is the top plan node's total time in milliseconds.
	ActualTime float64
	// PlanningTime is the time spent planning, in milliseconds.
	PlanningTime float64
	// ExecutionTime is the time spent executing, in milliseconds.
	ExecutionTime float64
	// JSON is EXPLAIN's output.
	JSON []byte
}

Plan is the plan Postgres chose for a statement: the planner's estimates, the measured values when Explain analyzed it, and the whole plan as EXPLAIN's JSON.

func (*Plan) Indexes

func (p *Plan) Indexes() []string

Indexes returns the names of the indexes the plan reads, in plan order, each once.

func (*Plan) SeqScans

func (p *Plan) SeqScans() []string

SeqScans returns the tables the plan reads with a sequential scan, in plan order, each once.

type PlanCache

type PlanCache uint8

PlanCache chooses how keel prepares statements.

const (
	// PlanCacheAuto prepares each statement shape once per connection and lets Postgres choose
	// between a generic and a custom plan (plan_cache_mode = auto): it compares the generic plan's
	// cost with the custom plans of the first executions and keeps custom plans when the generic
	// one is worse. This skips parsing and, usually, planning.
	PlanCacheAuto PlanCache = iota
	// PlanCacheCustom prepares statements but plans every execution for its values
	// (plan_cache_mode = force_custom_plan). It skips parsing only; use it when parameter values
	// change plans a lot, as tenants of very different sizes can.
	PlanCacheCustom
	// PlanCacheOff prepares nothing: every execution is parsed and planned. Use it behind a
	// connection pooler that cannot track prepared statements (PgBouncer before 1.21, or without
	// max_prepared_statements).
	PlanCacheOff
)

type PoolState

type PoolState struct {
	Acquired int32
	Idle     int32
	Total    int32
	Max      int32
}

PoolState is a connection pool's state: connections checked out, idle, open and the most it opens.

type Position

type Position uint64

Position is a WAL position (an LSN) of the primary, as Postgres prints it: 16/B374D848.

func CommitPosition

func CommitPosition(ctx context.Context, db *DB) Position

CommitPosition returns the newest position of db that replica reads on ctx must have replayed: the commit position of the last write made with ctx after OnReplica, or the position given to OnReplicaAfter. It is zero when there is none or db has no replicas, and the largest Position when keel could not read a commit's position, which keeps the context's reads on the primary.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := keel.OnReplica(requestContext()), appDB
	s := fleetdb.Shipments

	sh := &fleet.Shipment{CustomerID: "cus_01", ProNumber: "PRO-3002", Status: fleet.StatusNew}
	if err := s.Create(ctx, db, sh); err != nil {
		log.Fatal(err)
	}
	cookie := keel.CommitPosition(ctx, db).String()

	pos, err := keel.ParsePosition(cookie)
	if err != nil {
		log.Fatal(err)
	}
	next := keel.OnReplicaAfter(requestContext(), db, pos)
	n, err := s.Where(s.Status.Eq(fleet.StatusNew)).Count(next, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}

func ParsePosition

func ParsePosition(s string) (Position, error)

ParsePosition parses an LSN as Postgres and Position.String print it.

func (Position) String

func (p Position) String() string

String formats p as Postgres prints an LSN.

type PreAggStrategy

type PreAggStrategy uint8

PreAggStrategy chooses how a pre-aggregation reads a to-many relation.

const (
	// PreAggCTE aggregates the related rows in one grouped pass, keyed by the parent and filtered
	// to the parents the query selects, in a CTE joined to the query: one scan of the related
	// table whatever the number of parents. The default for reports.
	PreAggCTE PreAggStrategy = iota + 1
	// PreAggLateral aggregates each parent's related rows in a LEFT JOIN LATERAL subquery: one
	// index probe per parent, best when the query selects few parents (a page, a top N).
	PreAggLateral
)

type Pred

type Pred interface {
	Expr
	// contains filtered or unexported methods
}

Pred is a boolean expression, accepted by Where, Having and the combinators.

func And

func And(ps ...Pred) Pred

And joins ps with AND. Nil predicates are skipped; And of nothing is TRUE.

func Exists

func Exists(q subquery) Pred

Exists is EXISTS (q).

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c, s := fleetdb.Customers, fleetdb.Shipments
	cutoff := time.Now().AddDate(0, -3, 0).Unix()

	idle, err := c.Where(keel.NotExists(
		s.Where(s.CustomerID.EqCol(c.ID), s.CreatedAt.Gte(cutoff)),
	)).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	hazmatShippers, err := c.Where(
		c.ID.InQuery(s.Pick(s.CustomerID).Where(s.Hazmat.IsTrue())),
	).Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(idle), hazmatShippers)
}

func Not

func Not(p Pred) Pred

Not negates p.

func NotExists

func NotExists(q subquery) Pred

NotExists is NOT EXISTS (q).

func Or

func Or(ps ...Pred) Pred

Or joins ps with OR. Nil predicates are skipped; Or of nothing is FALSE.

func True

func True() Pred

True is the predicate TRUE.

func When

func When(cond bool, p Pred) Pred

When returns p when cond holds and nil otherwise. Where and the combinators skip nil, which makes optional filters one expression each.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// shipmentFilter is a search request with optional fields.
type shipmentFilter struct {
	Search     string
	Statuses   []fleet.Status
	CustomerID ids.ID
	HazmatOnly bool
}

func main() {
	ctx, db := requestContext(), appDB
	f := shipmentFilter{Search: "PRO-10", Statuses: []fleet.Status{fleet.StatusNew}}
	s := fleetdb.Shipments

	rows, err := s.Where(
		keel.When(f.Search != "", s.ProNumber.ContainsFold(f.Search)),
		keel.When(len(f.Statuses) > 0, s.Status.In(f.Statuses...)),
		keel.When(f.CustomerID != "", s.CustomerID.Eq(f.CustomerID)),
		keel.When(f.HazmatOnly, s.Hazmat.IsTrue()),
	).Order(s.CreatedAt.Desc()).Limit(50).List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

type Projection

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

Projection is a statement whose output columns fill a struct: a query that picks expressions, or a Compound. *Query[T] and Compound implement it.

type Query

type Query[T any] struct {
	// contains filtered or unexported fields
}

Query is a query over model T. Builder methods change the query and return it.

func (*Query[T]) All

func (q *Query[T]) All(ctx context.Context, db *DB) ([]T, error)

All returns every matching row. Use it only for sets known to be small.

func (*Query[T]) AllTenants

func (q *Query[T]) AllTenants() *Query[T]

AllTenants lets the query cross tenants. It needs a system scope. The query's relation loads and its Any and None predicates cross tenants with it; a query used as a subquery (Exists, InQuery, a lateral join, a CTE, a set operation) declares AllTenants itself.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withSystem(ctx context.Context, reason string) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{SystemReason: reason})
}

func main() {
	ctx, db := withSystem(context.Background(), "purge canceled shipments"), appDB
	s := fleetdb.Shipments
	m := s.Moves()
	cutoff := time.Now().AddDate(-1, 0, 0).Unix()

	stale, err := s.AllTenants().
		Where(s.Status.Eq(fleet.StatusCanceled), s.CanceledAt.Lt(cutoff), m.None(m.Loaded.IsTrue())).
		Load(s.Customer(), m.Stops()).
		Limit(1000).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	n, err := s.AllTenants().
		Where(s.Status.Eq(fleet.StatusCanceled), s.CanceledAt.Lt(cutoff)).
		Delete(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(stale), n)
}

func (*Query[T]) Any

func (q *Query[T]) Any(ps ...Pred) Pred

Any is true when at least one row of the query matches ps. On a query started from a to-many relation path it is correlated with the parent row.

func (*Query[T]) Cache

func (q *Query[T]) Cache(opts ...CacheOption) *Query[T]

Cache serves the query's One, First, List, All, Count, CountUpTo, CountEstimate, Exists and Page (and the Batch items of the same names) from the result cache (Config.Cache) while the data versions of every table they read (the base table, joins, relation loads, subqueries, Any and None, CTEs, and AlsoReads) are unchanged for the tenant. A Batch is served from the cache only when every item's query asks for it, for the shortest of their TTLs. The cache is bypassed inside a write transaction, a joined one and a REPEATABLE READ or SERIALIZABLE one that has already begun (sent a statement, so its snapshot is taken), under a system scope, for a query with Lock, and while the cache's listener is disconnected. A snapshot transaction that has not begun is served from the cache, and a hit leaves it unbegun: its snapshot is taken by its first statement that reads the database. Without Config.Cache it does nothing.

func (*Query[T]) Clone

func (q *Query[T]) Clone() *Query[T]

Clone returns an independent copy of the query.

func (*Query[T]) Compute

func (q *Query[T]) Compute(col Column, e Expr) *Query[T]

Compute selects e into the computed field col of the rows the query returns: a search rank, an aggregate over related rows, a value of a joined table. col must be a field tagged computed and e's type must fit it (an int64 field takes count(*), a float64 field ts_rank, a string field a string expression); expressions whose type keel cannot tell, such as Raw, are checked by the driver when rows decode. The query can filter and order by col too: keel writes e in its place.

s.Compute(s.Rank, s.SearchVector.Rank(q)).Where(s.SearchVector.Match(q)).Order(s.Rank.Desc())
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c, s := fleetdb.Contacts, fleetdb.Shipments
	rows, err := c.
		Compute(c.CustomerName, c.Customer().Name).
		Compute(c.Loads, keel.Scalar(s.Pick(keel.Count()).Where(s.CustomerID.EqCol(c.CustomerID)))).
		Where(c.Loads.Gt(0)).
		Order(c.Loads.Desc()).
		Limit(50).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range rows {
		fmt.Println(r.Name, r.CustomerName, r.Loads)
	}
}

func (*Query[T]) Count

func (q *Query[T]) Count(ctx context.Context, db *DB) (int64, error)

Count returns the number of matching rows.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	open := s.Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned))

	n, err := open.Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	upTo, err := open.CountUpTo(ctx, db, 1000)
	if err != nil {
		log.Fatal(err)
	}
	exists, err := open.Exists(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, upTo, exists)
}

func (*Query[T]) CountEstimate

func (q *Query[T]) CountEstimate(ctx context.Context, db *DB, exactUpTo int64) (RowCount, error)

CountEstimate returns the number of matching rows, counted exactly up to exactUpTo, and the planner's estimate when more match: a list header shows "1,204" for a small set and "about 1.2M" for a large one without counting it. The count reads at most exactUpTo+1 rows (SELECT count(*) FROM (SELECT 1 … LIMIT exactUpTo+1)), and EXPLAIN (FORMAT JSON) of the same rows without the limit, with the same text and bound arguments, rides in the same round trip; the estimate is the top plan node's row count, so it reflects the query's filter, joins, groups and the bound values. The query's own Limit and Offset do not apply, as for CountUpTo. An estimate is as good as the table's statistics: it can be well off on a table whose statistics are stale, but it is never less than exactUpTo+1. exactUpTo must not be negative.

Example
ctx, db := requestContext(), appDB
s := fleetdb.Shipments

c, err := s.Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned)).CountEstimate(ctx, db, 1000)
if err != nil {
	log.Fatal(err)
}
if c.Exact {
	fmt.Println(c.N, "shipments")
} else {
	fmt.Println("about", c.N, "shipments")
}

func (*Query[T]) CountUpTo

func (q *Query[T]) CountUpTo(ctx context.Context, db *DB, limit int64) (int64, error)

CountUpTo counts matching rows, stopping at limit.

func (*Query[T]) Cube

func (q *Query[T]) Cube(es ...Expr) *Query[T]

Cube groups by every subset of es: CUBE (a, b) is GROUPING SETS ((a, b), (a), (b), ()).

func (*Query[T]) CustomPlan

func (q *Query[T]) CustomPlan() *Query[T]

CustomPlan plans this query for its parameter values on every execution, even when the connection would reuse a generic plan. Use it for queries whose best plan depends on the values, such as a filter whose selectivity differs widely between tenants. It costs no round trip.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	rows, err := s.
		Where(s.Status.Eq(fleet.StatusDelivered), s.CustomerID.Eq("cus_01")).
		CustomPlan().
		Order(s.CreatedAt.Desc()).
		Limit(100).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

func (*Query[T]) Delete

func (q *Query[T]) Delete(ctx context.Context, db *DB) (int64, error)

Delete deletes every matching row and returns the number of rows deleted.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	st := fleetdb.Stops
	n, err := st.Where(st.MoveID.Eq("mov_01"), st.ArrivedAt.IsNull()).Delete(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, "stops deleted")
}

func (*Query[T]) Distinct

func (q *Query[T]) Distinct() *Query[T]

Distinct returns each distinct row once: SELECT DISTINCT.

func (*Query[T]) DistinctOn

func (q *Query[T]) DistinctOn(es ...Expr) *Query[T]

DistinctOn keeps the first row of each group of es (DISTINCT ON).

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	latest, err := s.
		DistinctOn(s.CustomerID).
		Order(s.CustomerID.Asc(), s.CreatedAt.Desc()).
		All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(latest), "customers")
}

func (*Query[T]) Each

func (q *Query[T]) Each(ctx context.Context, db *DB, size int,
	fn func(ctx context.Context, batch []T) error,
) error

Each runs the query page by page in keyset order and calls fn with each batch of at most size rows, holding one batch in memory at a time.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withSystem(ctx context.Context, reason string) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{SystemReason: reason})
}

func main() {
	ctx, db := withSystem(context.Background(), "nightly hazmat audit"), appDB
	s := fleetdb.Shipments

	err := s.AllTenants().Where(s.Hazmat.IsTrue()).Each(ctx, db, 500,
		func(ctx context.Context, batch []*fleet.Shipment) error {
			for _, sh := range batch {
				fmt.Println(sh.OrganizationID, sh.ProNumber)
			}
			return nil
		})
	if err != nil {
		log.Fatal(err)
	}
}

func (*Query[T]) Exists

func (q *Query[T]) Exists(ctx context.Context, db *DB) (bool, error)

Exists reports whether any row matches.

func (*Query[T]) Explain

func (q *Query[T]) Explain(ctx context.Context, db *DB, opts ExplainOptions) (*Plan, error)

Explain returns the plan of the query's main statement, rendered as ToSQL renders it under ctx's scope, with its parameters bound so the plan is the one Postgres chooses for these values.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	q := s.Where(s.Status.Eq(fleet.StatusNew)).Order(s.CreatedAt.Desc()).Limit(50)

	plan, err := q.Explain(ctx, db, keel.ExplainOptions{})
	if err != nil {
		log.Fatal(err)
	}
	if plan.TotalCost > 100_000 {
		fmt.Println("too expensive for an interactive request")
	}

	analyzed, err := q.Explain(ctx, db, keel.ExplainOptions{Analyze: true})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(analyzed.Rows, analyzed.ActualRows, analyzed.ExecutionTime)
}

func (*Query[T]) First

func (q *Query[T]) First(ctx context.Context, db *DB) (T, error)

First returns the first row in the query's order, or ErrNotFound.

func (*Query[T]) Group

func (q *Query[T]) Group(es ...Expr) *Query[T]

Group adds GROUP BY expressions.

func (*Query[T]) GroupingSets

func (q *Query[T]) GroupingSets(sets ...[]Expr) *Query[T]

GroupingSets groups by each of sets in turn, in one statement: GROUP BY GROUPING SETS ((a, b), (a), ()). Each set yields its own rows, with the expressions it does not group by NULL; an empty set is the grand total. Grouping tells the rows of each set apart. Group expressions added before or after are grouped in every set.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	code := s.Customer().Code
	level := keel.Grouping(code, s.Status)

	type total struct {
		Customer *string
		Status   *fleet.Status
		Loads    int64
	}
	rows, err := keel.Into[total](ctx, db, s.
		Pick(code, s.Status, keel.Count()).
		GroupingSets([]keel.Expr{code}, []keel.Expr{s.Status}, nil).
		Having(level.Gt(0)))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

func (*Query[T]) Having

func (q *Query[T]) Having(ps ...Pred) *Query[T]

Having adds HAVING predicates.

func (*Query[T]) Join

func (q *Query[T]) Join(t interface{ TableRef() *Ref }, on ...Pred) *Query[T]

Join adds an INNER JOIN of another table on the given predicates.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	w := fleetdb.Workers
	mgr := w.As("mgr")

	reports, err := w.
		Join(mgr, mgr.ID.EqCol(w.ManagerID)).
		Where(mgr.LastName.Eq("Hopper")).
		Order(w.LastName.Asc()).
		All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	s, c := fleetdb.Shipments, fleetdb.Customers
	rows, err := s.
		LeftJoin(c, c.ID.EqCol(s.BillToID)).
		Where(keel.Or(c.ID.IsNull(), c.Email.IsNotNull())).
		Limit(100).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(reports), len(rows))
}

func (*Query[T]) JoinLateral

func (q *Query[T]) JoinLateral(sub interface {
	subquery
	TableRef() *Ref
},
) *Query[T]

JoinLateral adds CROSS JOIN LATERAL (sub).

func (*Query[T]) LeftJoin

func (q *Query[T]) LeftJoin(t interface{ TableRef() *Ref }, on ...Pred) *Query[T]

LeftJoin adds a LEFT JOIN of another table on the given predicates.

func (*Query[T]) LeftJoinLateral

func (q *Query[T]) LeftJoinLateral(sub interface {
	subquery
	TableRef() *Ref
},
) *Query[T]

LeftJoinLateral adds LEFT JOIN LATERAL (sub) ON TRUE. Columns of sub's table, referenced outside sub, read sub's output.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	m := fleetdb.Moves
	last := fleetdb.Stops.As("last_stop")

	type moveEnd struct {
		Move     ids.ID
		Location ids.ID
		Sequence int32
	}
	rows, err := keel.Into[moveEnd](ctx, db, m.
		LeftJoinLateral(last.
			Pick(last.LocationID, last.Sequence).
			Where(last.MoveID.EqCol(m.ID)).
			Order(last.Sequence.Desc()).
			Limit(1)).
		Pick(m.ID, last.LocationID, last.Sequence).
		Where(m.ShipmentID.Eq("shp_01")))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(rows)
}

func (*Query[T]) Limit

func (q *Query[T]) Limit(n int) *Query[T]

Limit caps the number of rows.

func (*Query[T]) List

func (q *Query[T]) List(ctx context.Context, db *DB) ([]T, error)

List returns the matching rows. The query must have a Limit.

func (*Query[T]) Load

func (q *Query[T]) Load(ls ...Loader) *Query[T]

Load loads relations into the rows.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	m := s.Moves()
	cu := s.Customer()

	rows, err := s.
		Where(s.Status.Eq(fleet.StatusInTransit)).
		Load(
			cu.Pick(cu.Name, cu.Code),
			s.BillTo(),
			m.Where(m.Status.Ne(fleet.StatusCanceled)).
				Order(m.Sequence.Asc()).
				Load(m.Stops().Location()),
			s.Tags(),
		).
		Order(s.CreatedAt.Desc()).
		Limit(50).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, sh := range rows {
		fmt.Println(sh.ProNumber, sh.Customer.Name, len(sh.Moves), len(sh.Tags))
		for _, mv := range sh.Moves {
			for _, st := range mv.Stops {
				fmt.Println("  ", st.Sequence, st.Location.City)
			}
		}
	}
}
Example (Limit)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	m := s.Moves()

	rows, err := s.
		Load(m.Order(m.Sequence.Desc()).Limit(1)).
		Order(s.CreatedAt.Desc()).
		Limit(50).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, sh := range rows {
		if len(sh.Moves) > 0 {
			fmt.Println(sh.ProNumber, "last move", sh.Moves[0].ID)
		}
	}
}
Example (Pick)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	origin := s.OriginStop()

	rows, err := s.
		Where(origin.Location().City.Eq("Austin")).
		Load(origin.Location(), s.DestinationStop().Location()).
		Order(s.CreatedAt.Desc()).
		Limit(50).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, sh := range rows {
		if sh.DestinationStop != nil {
			fmt.Println(sh.ProNumber, sh.OriginStop.Location.City, "→", sh.DestinationStop.Location.City)
		}
	}
}

func (*Query[T]) LoadStrategy

func (q *Query[T]) LoadStrategy(s LoadStrategy) *Query[T]

LoadStrategy chooses how the query's relations load, overriding Config.LoadStrategy for the query and every load below it; a load can choose again for its own subtree: Load(m.LoadStrategy(keel.LoadPerLevel)).

func (*Query[T]) Lock

func (q *Query[T]) Lock(strength LockStrength, wait ...LockWait) *Query[T]

Lock locks the table's selected rows. Joined relations are not locked.

Example
package main

import (
	"context"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		batch, err := s.
			Where(s.Status.Eq(fleet.StatusNew)).
			Order(s.CreatedAt.Asc()).
			Limit(20).
			Lock(keel.ForUpdate, keel.SkipLocked).
			List(ctx, db)
		if err != nil {
			return err
		}
		for _, sh := range batch {
			sh.Status = fleet.StatusAssigned
			if err := s.Save(ctx, db, sh, s.Status); err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}
}

func (*Query[T]) None

func (q *Query[T]) None(ps ...Pred) Pred

None is the negation of Any.

func (*Query[T]) Offset

func (q *Query[T]) Offset(n int) *Query[T]

Offset skips n rows. Prefer Page for anything a person scrolls.

func (*Query[T]) Omit

func (q *Query[T]) Omit(cols ...Column) *Query[T]

Omit leaves cols out of the columns the query reads, so a heavy or secret column (a transcript, raw file contents, a token) is not read; the fields stay zero. The primary key and the columns relation loads need are read anyway. It works on loads too: Load(r.Omit(r.Payload)).

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	c := fleetdb.Contacts
	contacts, err := c.Omit(c.Avatar, c.Prefs).Order(c.Name.Asc()).Limit(50).List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(contacts))
}

func (*Query[T]) One

func (q *Query[T]) One(ctx context.Context, db *DB) (T, error)

One returns the only matching row: ErrNotFound when none matches, ErrNotSingular when more do.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	byID, err := s.Get(ctx, db, "shp_01")
	if errors.Is(err, keel.ErrNotFound) {
		fmt.Println("no such shipment")
	} else if err != nil {
		log.Fatal(err)
	}
	sh, err := s.Where(s.ProNumber.Eq("PRO-1001")).One(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	newest, err := s.Where(s.CustomerID.Eq(sh.CustomerID)).Order(s.CreatedAt.Desc()).First(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	recent, err := s.Where(s.Status.Eq(fleet.StatusNew)).Order(s.CreatedAt.Desc()).Limit(50).List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	tags, err := fleetdb.Tags.Query().All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(byID, newest.ProNumber, len(recent), len(tags))
}

func (*Query[T]) Order

func (q *Query[T]) Order(os ...Order) *Query[T]

Order sets the sort order.

func (*Query[T]) Page

func (q *Query[T]) Page(ctx context.Context, db *DB, p PageRequest) (Page[T], error)

Page returns the page of rows after the request's cursor. The query's order may name columns of its table, columns of to-one relations (belongs, one and pick paths, joined as any read joins them) and expressions; keel adds the primary key when the order does not name it, so every row has a unique position.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	cursor := ""

	page, err := s.
		Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned)).
		Load(s.Customer()).
		Order(s.CreatedAt.Desc()).
		Page(ctx, db, keel.After(cursor, 50).Total(10_000))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(page.Items), page.Total, page.TotalExact)
	if page.Next != "" {
		fmt.Println("next page:", page.Next)
	}
}

func (*Query[T]) Pick

func (q *Query[T]) Pick(es ...Expr) *Query[T]

Pick selects only es. When every expression is a column of the table, the rows still decode into T with the other fields left zero; otherwise the query is a projection for Into.

func (*Query[T]) Rollup

func (q *Query[T]) Rollup(es ...Expr) *Query[T]

Rollup groups by es and by each prefix of es down to the grand total: ROLLUP (a, b) is GROUPING SETS ((a, b), (a), ()), the rows with their subtotals and total.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	code := s.Customer().Code
	level := keel.Grouping(code, s.Status)

	type line struct {
		Customer *string
		Status   *fleet.Status
		Loads    int64
		Revenue  fleet.Money
		Level    int32
	}
	rows, err := keel.Into[line](ctx, db, s.
		Pick(code, s.Status, keel.Count(), s.Total.Sum(), level).
		Rollup(code, s.Status).
		Order(level.Asc(), code.Asc(), s.Status.Asc()))
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range rows {
		switch r.Level {
		case 0:
			fmt.Println(*r.Customer, *r.Status, r.Loads)
		case 1:
			fmt.Println(*r.Customer, "subtotal", r.Loads)
		default:
			fmt.Println("total", r.Loads)
		}
	}
}

func (*Query[T]) TableRef

func (q *Query[T]) TableRef() *Ref

TableRef returns the query's table reference.

func (*Query[T]) Timeout

func (q *Query[T]) Timeout(d time.Duration) *Query[T]

Timeout bounds the query's execution.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	n, err := s.Where(s.SearchVector.Match("reefer")).Timeout(2*time.Second).Count(ctx, db)
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("search took too long")
	} else if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}

func (*Query[T]) ToSQL

func (q *Query[T]) ToSQL(ctx context.Context, db *DB) (string, []any, error)

ToSQL renders the query's main statement as it would run under ctx's scope, for logging, tests and EXPLAIN. Relation loads that run as separate statements are not included.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	sql, args, err := s.
		Pick(s.ID, s.ProNumber).
		Where(s.Status.In(fleet.StatusNew, fleet.StatusAssigned), s.Customer().Code.Eq("ACME")).
		Order(s.CreatedAt.Desc()).
		Limit(20).
		ToSQL(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(sql)
	fmt.Println(args)
}

func (*Query[T]) Update

func (q *Query[T]) Update(ctx context.Context, db *DB, sets ...Assignment) (int64, error)

Update sets columns on every matching row and returns the number of rows changed. The autoupdate column is stamped and the version column incremented unless sets name them.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments
	now := time.Now().Unix()
	n, err := s.
		Where(s.Status.Eq(fleet.StatusNew), s.Customer().Code.Eq("ACME")).
		Update(ctx, db, s.Status.To(fleet.StatusCanceled), s.CanceledAt.To(now), s.Priority.Increment(1))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n, "shipments canceled")
}

func (*Query[T]) UpdateReturning

func (q *Query[T]) UpdateReturning(ctx context.Context, db *DB, sets ...Assignment) ([]T, error)

UpdateReturning sets columns on every matching row, as Update does, and returns the updated rows read in the same statement (WITH keel_upd AS (UPDATE … RETURNING …) SELECT …): their values after the update, readonly columns included, with the autoupdate stamp and the incremented version. The query's Load paths are read too, to-one relations joined into the statement and to-many relations loaded after it, both as of the statement's snapshot (before the update); Order orders the rows; Pick and Omit narrow the columns read back (the primary key and tenant columns are always read, every default column when the table has update hooks, which receive the same rows). The update's tenant predicate and interceptors choose the rows; read interceptors do not apply to them. Limit, Offset, Lock, Group and DistinctOn are refused.

func (*Query[T]) Where

func (q *Query[T]) Where(ps ...Pred) *Query[T]

Where adds predicates, joined by AND. Nil predicates are skipped.

func (*Query[T]) Window

func (q *Query[T]) Window(name string, w Window) *Query[T]

Window declares a named window (WINDOW name AS (…)) that window functions of the query use through NamedWindow. name must match [a-z_][a-z0-9_]*.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	type numbered struct {
		Pro     string
		N       int64
		Running fleet.Money
		Tile    int32
	}
	rows, err := keel.Into[numbered](ctx, db, s.
		Pick(s.ProNumber,
			keel.RowNumber().OverWindow(keel.NamedWindow("per_customer")),
			keel.Sum(s.Total).OverWindow(keel.NamedWindow("per_customer").
				Rows(keel.UnboundedPreceding, keel.CurrentRow)),
			keel.Ntile(4).OverWindow(keel.OrderBy(s.Total.Desc()))).
		Window("per_customer", keel.PartitionBy(s.CustomerID).OrderBy(s.CreatedAt.Asc())).
		Limit(500))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rows))
}

func (*Query[T]) With

func (q *Query[T]) With(ctes ...*CTETable) *Query[T]

With declares CTEs the query uses.

type QueryEvent

type QueryEvent struct {
	Op       string
	Table    string
	SQL      string
	Args     int
	Values   []any
	Replica  string
	Tx       bool
	Start    time.Time
	Duration time.Duration
	Rows     int64
	Err      error
}

QueryEvent describes one statement. SQL never contains values; Args is the number of bound parameters, and Values holds them only when Config.ObserveArgs is set. Replica names the replica the statement ran on (ReplicaStats.Name), and is empty for the primary. Tx reports that the statement ran in an explicit transaction (Write, Read or Tx), not an implicit one. Err is the error the terminal returns: a constraint violation is already a *ConstraintError.

type RawExpr

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

RawExpr is the result of Raw. It is both an expression and a predicate.

func Raw

func Raw(sql string, args ...any) RawExpr

Raw is SQL text that keel does not understand. Each ? in sql is replaced by the next argument: an Expr (a column, a subquery) is written in place with its alias resolved, any other value is bound as a parameter. Use ?? for a literal question mark.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	n, err := s.Where(
		keel.Raw("length(?) > ?", s.ProNumber, 8),
		keel.Raw("? ?? 'trailer'", s.Attributes),
	).Count(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}

type ReadFilter

type ReadFilter func(ctx context.Context, t *schema.Table) ([]Pred, error)

ReadFilter returns the predicates every reference to table t in a read statement must also satisfy, for row-level rules that sit beside tenancy: "own records only", "this region's customers". The predicates are built over the table's own columns, from a generated handle (shipmentdb.Shipments.OwnerID.Eq(id)) or a dynamic query on the table (keel.DynFrom(t).Path("owner_id").Eq(id)); keel resolves those columns to each reference it filters. A predicate may also test related rows with an EXISTS (DynFrom(t).Rel(…).Any), but it cannot join a relation.

type Ref

type Ref struct {
	Table *schema.Table
	// contains filtered or unexported fields
}

Ref is one reference to a table inside statements. Generated code builds the base Ref of every model statically; relation paths and aliases derive further Refs on first use.

type Registry

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

Registry finds an application's tables by name, for code that chooses them at run time: report compilers, data tables with user-chosen columns, generic exports. Build it once from the generated tables (the <Model>Schema functions of the generated packages) and share it; it is read-only after NewRegistry.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

// reportTables is the registry of the tables reports can start from.
var reportTables, _ = keel.NewRegistry(fleetdb.ShipmentsSchema(), fleetdb.CustomersSchema(),
	fleetdb.MovesSchema(), fleetdb.StopsSchema(), fleetdb.TagsSchema(), fleetdb.LocationsSchema())

func main() {
	ctx, db := requestContext(), appDB
	search := "acme"

	q := reportTables.Query("shipments")
	pro := q.Path("pro_number")
	customer := q.Path("customer", "name")
	q.Select(pro, customer, q.Path("status"), q.Path("total")).
		Where(q.Path("status").In([]string{"New", "Assigned"}), customer.ContainsFold(search)).
		Order(pro.Desc()).
		Limit(100)
	res, err := q.Run(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	total := res.Column("total")
	for i := range res.Rows {
		fmt.Println(res.Columns[0].String[i], res.Column("customer.name").String[i], total.Numeric[i])
	}
}

func NewRegistry

func NewRegistry(tables ...*schema.Table) (*Registry, error)

NewRegistry indexes tables by their SQL name. A table listed twice is an error.

func (*Registry) Query

func (g *Registry) Query(name string) *DynQuery

Query starts a dynamic query over the table named name. An unknown name makes a query whose terminals fail.

func (*Registry) Table

func (g *Registry) Table(name string) (*schema.Table, bool)

Table returns the table named name.

func (*Registry) Tables

func (g *Registry) Tables() []*schema.Table

Tables returns the registered tables in the order they were given.

type Repeat

type Repeat struct {
	SQL   string
	Count int
	// CallSites are the distinct places (file:line) outside keel and the generated packages that
	// sent the text again after its first round trip, at most maxCallSites of them. The first run
	// is not captured: keel records a stack only once a text repeats, so a budget costs nothing
	// for statements that do not.
	CallSites []string
}

Repeat is a statement text sent in more than one round trip under a Budget: the shape of an N+1 read when the count follows the size of a result.

type ReplicaConfig

type ReplicaConfig struct {
	// DSN is the replica's connection string. Pool, when set, is used instead; keel applies the
	// same query mode, statement cache and AfterConnect setup as on the primary.
	DSN  string
	Pool *pgxpool.Config
	// Weight is the replica's capacity relative to the others: at equal load a replica of weight
	// 2 takes twice the reads of one of weight 1. Defaults to 1.
	Weight int
}

ReplicaConfig is one streaming replica of the primary.

type ReplicaStats

type ReplicaStats struct {
	// Name is host:port/database.
	Name   string
	Weight int
	// Healthy reports that the last check reached the replica and found it in recovery.
	Healthy bool
	// Lag bounds how far the replica was behind the primary at the last check: it had replayed
	// everything the primary had written Lag before the check.
	Lag time.Duration
	// Position is the WAL position the replica had replayed at the last check.
	Position  Position
	CheckedAt time.Time
	// InFlight counts the terminals and transactions running on the replica now.
	InFlight int64
	// Reads counts the terminals and transactions routed to the replica.
	Reads uint64
	// Err is the error of the last check, if it failed or found the server not in recovery.
	Err error
}

ReplicaStats is the state of one replica as the lag tracker last saw it, with its routing counters.

type Row

type Row struct{}

Row is the row type of a query over a CTE or a compound query; read such queries with Into.

type RowCount

type RowCount struct {
	// N is the number of rows: counted when Exact is set, else the planner's estimate, never less
	// than the limit plus one.
	N int64
	// Exact reports that N was counted: no more rows matched than the limit.
	Exact bool
}

RowCount is a number of matching rows that is exact up to a limit and the planner's estimate above it, as CountEstimate returns it.

type RowsTable

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

RowsTable is a set of rows computed by expressions, usually set-returning functions such as unnest or generate_series, joined laterally so the expressions can read the row they join: LEFT JOIN LATERAL (SELECT unnest(r.operations) AS "op") op ON TRUE. Build it with Rows and join it with JoinLateral or LeftJoinLateral.

func Rows

func Rows(name string, columns []string, exprs ...Expr) *RowsTable

Rows declares a lateral source named name whose columns are the values of exprs, one column per expression in order. Several set-returning functions zip, row i of each making row i.

op := keel.Rows("op", []string{"op"}, keel.DynFunc("unnest", rp.Operations))
roles.LeftJoinLateral(op).Pick(r.ID, op.Col("op"))

func (*RowsTable) Col

func (s *RowsTable) Col(name string) AnyCol

Col references one of the source's columns.

func (*RowsTable) TableRef

func (s *RowsTable) TableRef() *Ref

TableRef returns the source's table reference, so the lateral joins accept it.

type SearchMode

type SearchMode uint8

SearchMode is how a search string becomes a tsquery.

const (
	// WebSearch reads the string as a web search engine does: words are ANDed, "quoted words"
	// match as a phrase, or separates alternatives and -word excludes a word
	// (websearch_to_tsquery). It is the default.
	WebSearch SearchMode = iota + 1
	// Plain ANDs the string's words, ignoring punctuation and operators (plainto_tsquery).
	Plain
	// Phrase matches the string's words next to each other, in order (phraseto_tsquery).
	Phrase
	// Prefix matches each word of the string as the start of a word, so a word typed in part
	// matches: "dal ree" is to_tsquery('dal':* & 'ree':*).
	Prefix
	// AnyWord matches rows holding any of the string's words: to_tsquery('dallas' | 'reefer').
	AnyWord
	// AnyPrefix matches rows holding a word that starts with any of the string's words.
	AnyPrefix
	// TSQuery reads the string as tsquery syntax (to_tsquery): ('dallas':* & 'reefer') | 'tx'.
	// Build it from words the code has cleaned, never from user input: a malformed string fails the
	// statement.
	TSQuery
	// AnyWebSearch reads the string as WebSearch does but matches rows holding any of its words or
	// "quoted phrases", without any -word: "dallas reefer -frozen" finds rows with either word and
	// without "frozen".
	AnyWebSearch
)

type SearchOption

type SearchOption func(*search)

SearchOption shapes a full-text search: the configurations and modes its query is built with, and the normalization of a rank.

func RankNormalization

func RankNormalization(mask int) SearchOption

RankNormalization sets the normalization of Rank and RankCD, the bit mask ts_rank takes as its third argument (1 divides the rank by 1 + the logarithm of the document length, 32 scales it to rank/(rank+1), …). Match ignores it.

func SearchAlso

func SearchAlso(mode SearchMode, text string) SearchOption

SearchAlso ORs another term into the query: text read with mode, under each of the search's configurations. A search of the words typed so far can add their prefixes, ANDed, alongside a WebSearch of the whole string:

m.SearchVector.Match(raw, keel.SearchAlso(keel.TSQuery, "'dal':* & 'ree':*"))

func SearchConfig

func SearchConfig(configs ...string) SearchOption

SearchConfig builds the query with each of the text search configurations named and ORs them: SearchConfig("simple", "english") finds a word whether the column's vector stemmed it or not. Names are configuration identifiers ([a-z_][a-z0-9_]*, optionally schema-qualified); Postgres refuses one it does not have when the statement runs. Without SearchConfig the column's tsvector=<config> tag option applies, else the database's default_text_search_config.

func SearchModes

func SearchModes(modes ...SearchMode) SearchOption

SearchModes builds the query with each of modes and ORs them: SearchModes(keel.WebSearch, keel.Prefix) finds whole words and the words being typed. Without it the mode is WebSearch.

type Shape

type Shape struct {
	SQL   string `json:"sql"`
	Op    string `json:"op"`
	Table string `json:"table"`
	Args  int    `json:"args"`
}

Shape is one distinct statement text keel sent: the text (which never contains values), its operation and table as QueryEvent reports them, and the number of parameters it binds. RecordShapes writes shapes as JSON lines, {"sql": "...", "op": "...", "table": "...", "args": n}, and keel plans reads them back to explain every one.

func ReadShapes

func ReadShapes(r io.Reader) ([]Shape, error)

ReadShapes reads the JSON lines RecordShapes wrote and returns each distinct statement text once, in the order first seen, so a file several processes appended to reads as one set.

type SharedCache

type SharedCache interface {
	// Get returns the value stored under key and true, or false when there is none or it expired.
	Get(ctx context.Context, key string) ([]byte, bool, error)
	// Set stores value under key for ttl.
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
}

SharedCache is a store every process shares (CacheConfig.Shared), such as Redis: encoded entries by key, each with a time to live. keel/cache/rediscache adapts a Redis client, and the stores of keel/analytics/cache fit as they are.

type SlotHolder

type SlotHolder struct {
	Held  time.Duration
	Stack []string
}

SlotHolder is work holding a tenant slot: how long it has held it and the stack that took it.

type SlowWait

type SlowWait struct {
	Kind    WaitKind
	Tenant  string
	Waited  time.Duration
	Stack   []string
	Limit   int
	Waiters int
	Holders []SlotHolder
	Pool    PoolState
}

SlowWait describes a wait that crossed SlowWaitConfig.After. Stack is the waiting caller's, from its first frame outside keel, as "function file:line". For a tenant slot, Limit is the tenant's bound, Waiters counts the work waiting for its slots, this one included, and Holders lists the work holding them: how long each has held its slot and the stack that took it. For a connection, Pool is the pool's state when the report was made.

type SlowWaitConfig

type SlowWaitConfig struct {
	After  time.Duration
	Report func(ctx context.Context, w *SlowWait)
}

SlowWaitConfig reports work that waits longer than After for a tenant slot (Config.TenantConcurrency), a pool connection, or another goroutine's read of the result cache entry it needs, so a starved tenant or an exhausted pool is visible while it happens rather than as requests that hang. Report is called once per wait that crosses After, from the waiting goroutine or a timer's; it must return quickly and must not run statements on the DB. Both fields must be set for keel to watch: it then records the call stack of every tenant slot it hands out and of every connection wait, about a microsecond each.

type Statement

type Statement struct {
	SQL  string
	Args []any
}

Statement is one statement with its arguments.

type StringArrayCase

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

StringArrayCase builds a CASE whose result is text[]. Start it with CaseStringArray.

func CaseStringArray

func CaseStringArray() StringArrayCase

CaseStringArray starts a CASE whose result is text[].

func (StringArrayCase) Else

Else ends the CASE with ELSE v.

func (StringArrayCase) End

End ends the CASE without ELSE.

func (StringArrayCase) When

When adds WHEN cond THEN v.

type StringArrayCol

type StringArrayCol struct{ Col }

StringArrayCol is a text[] or varchar[] column.

func (StringArrayCol) Append

func (c StringArrayCol) Append(v string) Assignment

Append sets the column to itself with v appended: array_append(col, v). A NULL array becomes {v}.

func (StringArrayCol) At

func (c StringArrayCol) At(i int) StringExpr

At is (col)[i], the i-th element from 1.

func (StringArrayCol) ContainedBy

func (c StringArrayCol) ContainedBy(vs ...string) Pred

func (StringArrayCol) ContainedByExpr

func (c StringArrayCol) ContainedByExpr(o StringArrayValue) Pred

ContainedByExpr is (col <@ o).

func (StringArrayCol) ContainsAll

func (c StringArrayCol) ContainsAll(vs ...string) Pred

func (StringArrayCol) ContainsAllExpr

func (c StringArrayCol) ContainsAllExpr(o StringArrayValue) Pred

ContainsAllExpr is (col @> o).

func (StringArrayCol) Has

func (c StringArrayCol) Has(v string) Pred

Has reports whether the array holds v.

func (StringArrayCol) Len

func (c StringArrayCol) Len() Int32Expr

Len is cardinality(col): the number of elements, 0 for an empty array and NULL for NULL.

func (StringArrayCol) Overlaps

func (c StringArrayCol) Overlaps(vs ...string) Pred

func (StringArrayCol) OverlapsExpr

func (c StringArrayCol) OverlapsExpr(o StringArrayValue) Pred

OverlapsExpr is (col && o).

func (StringArrayCol) Remove

func (c StringArrayCol) Remove(v string) Assignment

Remove sets the column to itself without the elements equal to v: array_remove(col, v).

func (StringArrayCol) To

func (c StringArrayCol) To(vs []string) Assignment

func (StringArrayCol) Unnest

func (c StringArrayCol) Unnest() StringExpr

Unnest is the set-returning unnest(col): one row per element, for keel.Rows.

func (StringArrayCol) Without

Without is array_remove(col, v), an expression; Remove is the assignment.

type StringArrayExpr

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

StringArrayExpr is a computed text[] value. Its methods are those of every array expression: At, Len (cardinality), Unnest, Without (array_remove), ContainsAllExpr (@>), ContainedByExpr (<@), OverlapsExpr (&&), Coalesce, IsNull, IsNotNull.

func ArrayOfString

func ArrayOfString(es ...StringValue) StringArrayExpr

ArrayOfString is ARRAY[es…] of text values.

func StringArrayOf

func StringArrayOf(e Expr) StringArrayExpr

StringArrayOf types e as text[].

func TextArray

func TextArray(vs []string) StringArrayExpr

TextArray binds vs as $n::text[].

func (StringArrayExpr) At

func (e StringArrayExpr) At(i int) StringExpr

At is (e)[i], the i-th element from 1.

func (StringArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (StringArrayExpr) ContainedByExpr

func (e StringArrayExpr) ContainedByExpr(o StringArrayValue) Pred

ContainedByExpr is (e <@ o): o holds every element of e.

func (StringArrayExpr) ContainsAllExpr

func (e StringArrayExpr) ContainsAllExpr(o StringArrayValue) Pred

ContainsAllExpr is (e @> o): e holds every element of o.

func (StringArrayExpr) IsNotNull

func (e StringArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (StringArrayExpr) IsNull

func (e StringArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (StringArrayExpr) Len

func (e StringArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (StringArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.TextArray(nil)) turns an empty array into NULL.

func (StringArrayExpr) OverlapsExpr

func (e StringArrayExpr) OverlapsExpr(o StringArrayValue) Pred

OverlapsExpr is (e && o): the arrays share an element.

func (StringArrayExpr) Unnest

func (e StringArrayExpr) Unnest() StringExpr

Unnest is the set-returning unnest(e), for keel.Rows.

func (StringArrayExpr) Without

Without is array_remove(e, v).

type StringArrayValue

type StringArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

StringArrayValue is a text[] value: a StringArrayCol or a StringArrayExpr.

type StringCase

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

StringCase builds a CASE whose result is text. Start it with CaseString.

func CaseString

func CaseString() StringCase

CaseString starts a CASE whose result is text.

func (StringCase) Else

func (b StringCase) Else(v StringValue) StringExpr

Else ends the CASE with ELSE v.

func (StringCase) End

func (b StringCase) End() StringExpr

End ends the CASE without ELSE: rows no WHEN matches are NULL.

func (StringCase) When

func (b StringCase) When(cond Pred, v StringValue) StringCase

When adds WHEN cond THEN v.

type StringCol

type StringCol struct{ Col }

StringCol is a text, varchar, enum or other string-valued column.

func (StringCol) Between

func (c StringCol) Between(lo, hi string) Pred

func (StringCol) Cat

func (c StringCol) Cat(vs ...StringValue) StringExpr

Cat is (col || v), each further value appended the same way.

func (StringCol) Coalesce

func (c StringCol) Coalesce(vs ...any) StringExpr

Coalesce is coalesce(col, vs…), typed as text: each v is a value bound as a parameter (sharing the placeholder of an equal value compared with the column) or an expression.

func (StringCol) Collate

func (c StringCol) Collate(name string) StringExpr

Collate is (col COLLATE "name").

func (StringCol) Contains

func (c StringCol) Contains(s string) Pred

Contains reports whether the column contains s. % and _ in s match themselves.

func (StringCol) ContainsFold

func (c StringCol) ContainsFold(s string) Pred

ContainsFold is Contains ignoring case.

func (StringCol) Eq

func (c StringCol) Eq(v string) Pred

func (StringCol) Greatest

func (c StringCol) Greatest(others ...StringValue) StringExpr

Greatest is greatest(col, others…).

func (StringCol) Gt

func (c StringCol) Gt(v string) Pred

func (StringCol) Gte

func (c StringCol) Gte(v string) Pred

func (StringCol) HasPrefix

func (c StringCol) HasPrefix(s string) Pred

HasPrefix reports whether the column starts with s.

func (StringCol) HasPrefixFold

func (c StringCol) HasPrefixFold(s string) Pred

HasPrefixFold is HasPrefix ignoring case.

func (StringCol) HasSuffix

func (c StringCol) HasSuffix(s string) Pred

HasSuffix reports whether the column ends with s.

func (StringCol) ILike

func (c StringCol) ILike(pattern string) Pred

ILike matches an ILIKE pattern as given.

func (StringCol) ILikeExpr

func (c StringCol) ILikeExpr(pattern StringValue) Pred

ILikeExpr is LikeExpr, ignoring case.

func (StringCol) In

func (c StringCol) In(vs ...string) Pred

func (StringCol) InArray

func (c StringCol) InArray(a StringArrayValue) Pred

InArray is (col = ANY(a)): keel.StringArrayOf(cte.Col("ids")) types a CTE's array column.

func (StringCol) IsDistinctFrom

func (c StringCol) IsDistinctFrom(v string) Pred

func (StringCol) IsDistinctFromExpr

func (c StringCol) IsDistinctFromExpr(o StringValue) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (StringCol) IsNotDistinctFrom

func (c StringCol) IsNotDistinctFrom(v string) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (StringCol) IsNotDistinctFromExpr

func (c StringCol) IsNotDistinctFromExpr(o StringValue) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (StringCol) LTrim

func (c StringCol) LTrim() StringExpr

LTrim is ltrim(col).

func (StringCol) LTrimChars

func (c StringCol) LTrimChars(chars string) StringExpr

LTrimChars is ltrim(col, chars).

func (StringCol) Least

func (c StringCol) Least(others ...StringValue) StringExpr

Least is least(col, others…).

func (StringCol) Left

func (c StringCol) Left(n int) StringExpr

Left is left(col, n).

func (StringCol) Length

func (c StringCol) Length() Int32Expr

Length is length(col).

func (StringCol) Like

func (c StringCol) Like(pattern string) Pred

Like matches a LIKE pattern as given; % and _ keep their meaning.

func (StringCol) LikeExpr

func (c StringCol) LikeExpr(pattern StringValue) Pred

LikeExpr matches the LIKE pattern an expression computes, as col LIKE pattern; % and _ in the pattern keep their meaning, so escape user text that feeds it.

func (StringCol) Lower

func (c StringCol) Lower() StringExpr

Lower is lower(col).

func (StringCol) Lt

func (c StringCol) Lt(v string) Pred

func (StringCol) Lte

func (c StringCol) Lte(v string) Pred

func (StringCol) MD5

func (c StringCol) MD5() StringExpr

MD5 is md5(col).

func (StringCol) Match

func (c StringCol) Match(re string) Pred

Match matches a POSIX regular expression.

func (StringCol) MatchFold

func (c StringCol) MatchFold(re string) Pred

MatchFold matches a POSIX regular expression ignoring case.

func (StringCol) Ne

func (c StringCol) Ne(v string) Pred

func (StringCol) NotIn

func (c StringCol) NotIn(vs ...string) Pred

func (StringCol) NotInArray

func (c StringCol) NotInArray(a StringArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (StringCol) NullIf

func (c StringCol) NullIf(v string) StringExpr

NullIf is nullif(col, v).

func (StringCol) RTrim

func (c StringCol) RTrim() StringExpr

RTrim is rtrim(col).

func (StringCol) RTrimChars

func (c StringCol) RTrimChars(chars string) StringExpr

RTrimChars is rtrim(col, chars).

func (StringCol) RegexpReplace

func (c StringCol) RegexpReplace(pattern, replacement, flags string) StringExpr

RegexpReplace is regexp_replace(col, pattern, replacement[, flags]).

func (StringCol) Replace

func (c StringCol) Replace(from, to string) StringExpr

Replace is replace(col, from, to).

func (StringCol) Right

func (c StringCol) Right(n int) StringExpr

Right is right(col, n).

func (StringCol) SplitPart

func (c StringCol) SplitPart(delim string, n int) StringExpr

SplitPart is split_part(col, delim, n).

func (StringCol) SplitPartExpr

func (c StringCol) SplitPartExpr(delim StringValue, n Int32Value) StringExpr

SplitPartExpr is split_part(col, delim, n) with the delimiter and field number as expressions.

func (StringCol) Substr

func (c StringCol) Substr(start, count int) StringExpr

Substr is substr(col, start, count).

func (StringCol) SubstrFrom

func (c StringCol) SubstrFrom(start int) StringExpr

SubstrFrom is substr(col, start).

func (StringCol) To

func (c StringCol) To(v string) Assignment

func (StringCol) Trim

func (c StringCol) Trim() StringExpr

Trim is btrim(col).

func (StringCol) TrimChars

func (c StringCol) TrimChars(chars string) StringExpr

TrimChars is btrim(col, chars).

func (StringCol) Upper

func (c StringCol) Upper() StringExpr

Upper is upper(col).

type StringExpr

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

StringExpr is a computed text value: a string function, a jsonb path read as text, a typed view.

func CastText

func CastText(e Expr) StringExpr

CastText is CAST(e AS text): an enum, a number, a uuid or a time as text.

func Concat

func Concat(args ...any) StringExpr

Concat is concat(args…): the text of every argument joined, NULL arguments skipped. An argument is an expression of any type or a Go value bound under the cast its type implies.

func ConcatWS

func ConcatWS(sep string, args ...any) StringExpr

ConcatWS is concat_ws(sep, args…): the non-NULL arguments joined with sep, as keel.ConcatWS(" ", w.FirstName, w.LastName) builds a full name.

func ConstText

func ConstText(s string) StringExpr

ConstText writes s into the SQL as a quoted literal, its single quotes doubled. The literal is of unknown type until it meets a value, as a constant in an index definition is: coalescing a varchar column with ConstText("") resolves to varchar and matches an index on that expression. A string holding a NUL byte makes an expression that fails.

func GenRandomUUID

func GenRandomUUID() StringExpr

GenRandomUUID is the text of a new random UUID: CAST(gen_random_uuid() AS text).

func NullText

func NullText() StringExpr

NullText is NULL::text.

func StringOf

func StringOf(e Expr) StringExpr

StringOf types e, an expression keel cannot type itself (a Raw, a Scalar, a DynExpr, a column of a Rows source), as text. It changes nothing in the SQL; CastText converts.

func Text

func Text(s string) StringExpr

Text binds s as a text value, $n::text, where a function or a comparison between expressions takes a StringValue.

func TextPtr

func TextPtr(p *string) StringExpr

TextPtr binds *p as $n::text, NULL when p is nil.

func (StringExpr) Asc

func (e StringExpr) Asc() Order

func (StringExpr) Between

func (e StringExpr) Between(lo, hi string) Pred

func (StringExpr) Cat

func (e StringExpr) Cat(vs ...StringValue) StringExpr

Cat is (e || v), each further value appended the same way, ((e || v) || w): text joined with SQL's NULL semantics, so a NULL operand makes the whole result NULL. keel.Concat and ConcatWS skip NULL arguments instead. Pass keel.Text(s) for a Go string.

func (StringExpr) Coalesce

func (e StringExpr) Coalesce(vs ...any) StringExpr

Coalesce is coalesce(e, vs…): each v a string bound as a parameter or an expression, taken in order where everything before it is NULL.

func (StringExpr) Collate

func (e StringExpr) Collate(name string) StringExpr

Collate is (e COLLATE "name"): e compared and ordered by that collation, as Collate("C") orders by bytes, which a text_pattern_ops or "C" index serves. A name that is empty, longer than 63 bytes or holds a double quote or control character makes an expression that fails.

func (StringExpr) Contains

func (e StringExpr) Contains(s string) Pred

func (StringExpr) ContainsFold

func (e StringExpr) ContainsFold(s string) Pred

func (StringExpr) Desc

func (e StringExpr) Desc() Order

func (StringExpr) Eq

func (e StringExpr) Eq(v string) Pred

func (StringExpr) EqExpr

func (e StringExpr) EqExpr(o StringValue) Pred

func (StringExpr) Greatest

func (e StringExpr) Greatest(others ...StringValue) StringExpr

Greatest is greatest(e, others…), the largest non-NULL value.

func (StringExpr) Gt

func (e StringExpr) Gt(v string) Pred

func (StringExpr) GtExpr

func (e StringExpr) GtExpr(o StringValue) Pred

func (StringExpr) Gte

func (e StringExpr) Gte(v string) Pred

func (StringExpr) GteExpr

func (e StringExpr) GteExpr(o StringValue) Pred

func (StringExpr) HasPrefix

func (e StringExpr) HasPrefix(s string) Pred

func (StringExpr) HasPrefixFold

func (e StringExpr) HasPrefixFold(s string) Pred

func (StringExpr) HasSuffix

func (e StringExpr) HasSuffix(s string) Pred

func (StringExpr) ILike

func (e StringExpr) ILike(p string) Pred

func (StringExpr) ILikeExpr

func (e StringExpr) ILikeExpr(p StringValue) Pred

func (StringExpr) In

func (e StringExpr) In(vs ...string) Pred

func (StringExpr) InArray

func (e StringExpr) InArray(a StringArrayValue) Pred

InArray is (e = ANY(a)): e equals an element of a.

func (StringExpr) IsDistinctFrom

func (e StringExpr) IsDistinctFrom(v string) Pred

func (StringExpr) IsDistinctFromExpr

func (e StringExpr) IsDistinctFromExpr(o StringValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (StringExpr) IsNotDistinctFrom

func (e StringExpr) IsNotDistinctFrom(v string) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (StringExpr) IsNotDistinctFromExpr

func (e StringExpr) IsNotDistinctFromExpr(o StringValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (StringExpr) IsNotNull

func (e StringExpr) IsNotNull() Pred

func (StringExpr) IsNull

func (e StringExpr) IsNull() Pred

func (StringExpr) LTrim

func (e StringExpr) LTrim() StringExpr

LTrim is ltrim(e): e without leading spaces.

func (StringExpr) LTrimChars

func (e StringExpr) LTrimChars(chars string) StringExpr

LTrimChars is ltrim(e, chars): e without leading characters in chars (ltrim(n, '0') drops leading zeros).

func (StringExpr) Least

func (e StringExpr) Least(others ...StringValue) StringExpr

Least is least(e, others…), the smallest non-NULL value.

func (StringExpr) Left

func (e StringExpr) Left(n int) StringExpr

Left is left(e, n): the first n characters, or all but the last -n when n is negative.

func (StringExpr) Length

func (e StringExpr) Length() Int32Expr

Length is length(e), the number of characters.

func (StringExpr) Like

func (e StringExpr) Like(p string) Pred

func (StringExpr) LikeExpr

func (e StringExpr) LikeExpr(p StringValue) Pred

func (StringExpr) Lower

func (e StringExpr) Lower() StringExpr

Lower is lower(e).

func (StringExpr) Lt

func (e StringExpr) Lt(v string) Pred

func (StringExpr) LtExpr

func (e StringExpr) LtExpr(o StringValue) Pred

func (StringExpr) Lte

func (e StringExpr) Lte(v string) Pred

func (StringExpr) LteExpr

func (e StringExpr) LteExpr(o StringValue) Pred

func (StringExpr) MD5

func (e StringExpr) MD5() StringExpr

MD5 is md5(e), the hex digest.

func (StringExpr) Match

func (e StringExpr) Match(re string) Pred

func (StringExpr) MatchFold

func (e StringExpr) MatchFold(re string) Pred

func (StringExpr) Max

func (e StringExpr) Max() Agg

func (StringExpr) Min

func (e StringExpr) Min() Agg

func (StringExpr) Ne

func (e StringExpr) Ne(v string) Pred

func (StringExpr) NeExpr

func (e StringExpr) NeExpr(o StringValue) Pred

func (StringExpr) NotIn

func (e StringExpr) NotIn(vs ...string) Pred

func (StringExpr) NotInArray

func (e StringExpr) NotInArray(a StringArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (StringExpr) NullIf

func (e StringExpr) NullIf(v string) StringExpr

NullIf is nullif(e, v): NULL where e equals v, as NullIf("") turns empty strings into NULL.

func (StringExpr) RTrim

func (e StringExpr) RTrim() StringExpr

RTrim is rtrim(e): e without trailing spaces.

func (StringExpr) RTrimChars

func (e StringExpr) RTrimChars(chars string) StringExpr

RTrimChars is rtrim(e, chars): e without trailing characters in chars.

func (StringExpr) RegexpReplace

func (e StringExpr) RegexpReplace(pattern, replacement, flags string) StringExpr

RegexpReplace is regexp_replace(e, pattern, replacement[, flags]): the first match of the POSIX pattern replaced, every match with flags "g". Empty flags leave the argument out.

func (StringExpr) Replace

func (e StringExpr) Replace(from, to string) StringExpr

Replace is replace(e, from, to): every occurrence of from replaced by to.

func (StringExpr) Right

func (e StringExpr) Right(n int) StringExpr

Right is right(e, n): the last n characters, or all but the first -n when n is negative.

func (StringExpr) SplitPart

func (e StringExpr) SplitPart(delim string, n int) StringExpr

SplitPart is split_part(e, delim, n): the n-th field of e split at delim, from 1; a negative n counts from the end.

func (StringExpr) SplitPartExpr

func (e StringExpr) SplitPartExpr(delim StringValue, n Int32Value) StringExpr

SplitPartExpr is split_part(e, delim, n) with the delimiter and field number as expressions: keel.ConstText and keel.ConstInt32 write them into the statement, so it matches an expression index such as lower(split_part(to_address, '@', 2)).

func (StringExpr) Substr

func (e StringExpr) Substr(start, count int) StringExpr

Substr is substr(e, start, count): count characters from position start, from 1.

func (StringExpr) SubstrFrom

func (e StringExpr) SubstrFrom(start int) StringExpr

SubstrFrom is substr(e, start): the characters from position start, from 1, to the end.

func (StringExpr) Trim

func (e StringExpr) Trim() StringExpr

Trim is btrim(e): e without leading and trailing spaces (SQL trim(e)).

func (StringExpr) TrimChars

func (e StringExpr) TrimChars(chars string) StringExpr

TrimChars is btrim(e, chars): e without leading and trailing characters in chars.

func (StringExpr) Upper

func (e StringExpr) Upper() StringExpr

Upper is upper(e).

type StringValue

type StringValue interface {
	Expr
	// contains filtered or unexported methods
}

StringValue is a text value: a string column (a generated enum or id column included) or a StringExpr.

type TSVectorCol

type TSVectorCol struct{ Col }

TSVectorCol is a tsvector column. Match, Rank and RankCD search it.

func (TSVectorCol) Match

func (c TSVectorCol) Match(query string, opts ...SearchOption) Pred

Match is col @@ query: the rows whose vector matches the search string, built by websearch_to_tsquery with the column's configuration unless opts say otherwise. The string is a bound parameter, so any user input is safe.

s.SearchVector.Match(q)                                              // websearch_to_tsquery
s.SearchVector.Match(q, keel.SearchConfig("simple", "english"))       // either configuration
s.SearchVector.Match(q, keel.SearchModes(keel.WebSearch, keel.Prefix)) // words or prefixes
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	n := fleetdb.Notes
	q := "dallas ree"
	rows, err := n.
		Compute(n.Rank, n.SearchVector.Rank(q)).
		Where(n.SearchVector.Match(q, keel.SearchConfig("simple", "english"),
			keel.SearchModes(keel.WebSearch, keel.Prefix))).
		Order(n.Rank.Desc()).
		Limit(20).
		List(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range rows {
		fmt.Println(r.Body, r.Rank)
	}
}

func (TSVectorCol) Rank

func (c TSVectorCol) Rank(query string, opts ...SearchOption) Agg

Rank is ts_rank(col, query): how well the row's vector matches the search string, a real number for Order and for a computed field. opts build the query as for Match.

func (TSVectorCol) RankCD

func (c TSVectorCol) RankCD(query string, opts ...SearchOption) Agg

RankCD is ts_rank_cd(col, query): the cover density rank, which also weighs how close the matching words are to each other.

type Table

type Table struct{}

Table marks a struct as a keel model. Its `keel` tag names the table:

type Shipment struct {
    keel.Table `keel:"shipments"`
    ...
}

type TimeArrayCase

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

TimeArrayCase builds a CASE whose result is timestamptz[]. Start it with CaseTimeArray.

func CaseTimeArray

func CaseTimeArray() TimeArrayCase

CaseTimeArray starts a CASE whose result is timestamptz[].

func (TimeArrayCase) Else

Else ends the CASE with ELSE v.

func (TimeArrayCase) End

func (b TimeArrayCase) End() TimeArrayExpr

End ends the CASE without ELSE.

func (TimeArrayCase) When

When adds WHEN cond THEN v.

type TimeArrayExpr

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

TimeArrayExpr is a computed timestamptz[] value, with the methods of StringArrayExpr.

func ArrayOfTime

func ArrayOfTime(es ...TimeValue) TimeArrayExpr

ArrayOfTime is ARRAY[es…] of times.

func TimeArray

func TimeArray(vs []time.Time) TimeArrayExpr

TimeArray binds vs as $n::timestamptz[].

func TimeArrayOf

func TimeArrayOf(e Expr) TimeArrayExpr

TimeArrayOf types e as timestamptz[].

func (TimeArrayExpr) At

func (e TimeArrayExpr) At(i int) TimeExpr

At is (e)[i], the i-th element from 1.

func (TimeArrayExpr) Coalesce

Coalesce is coalesce(e, o).

func (TimeArrayExpr) ContainedByExpr

func (e TimeArrayExpr) ContainedByExpr(o TimeArrayValue) Pred

ContainedByExpr is (e <@ o).

func (TimeArrayExpr) ContainsAllExpr

func (e TimeArrayExpr) ContainsAllExpr(o TimeArrayValue) Pred

ContainsAllExpr is (e @> o).

func (TimeArrayExpr) IsNotNull

func (e TimeArrayExpr) IsNotNull() Pred

IsNotNull is e IS NOT NULL.

func (TimeArrayExpr) IsNull

func (e TimeArrayExpr) IsNull() Pred

IsNull is e IS NULL.

func (TimeArrayExpr) Len

func (e TimeArrayExpr) Len() Int32Expr

Len is cardinality(e).

func (TimeArrayExpr) NullIf

NullIf is nullif(e, o): NULL where e equals o, as NullIf(keel.TimeArray(nil)) turns an empty array into NULL.

func (TimeArrayExpr) OverlapsExpr

func (e TimeArrayExpr) OverlapsExpr(o TimeArrayValue) Pred

OverlapsExpr is (e && o).

func (TimeArrayExpr) Unnest

func (e TimeArrayExpr) Unnest() TimeExpr

Unnest is the set-returning unnest(e), for keel.Rows.

func (TimeArrayExpr) Without

func (e TimeArrayExpr) Without(v TimeValue) TimeArrayExpr

Without is array_remove(e, v).

type TimeArrayValue

type TimeArrayValue interface {
	Expr
	// contains filtered or unexported methods
}

TimeArrayValue is a timestamptz[] value: a TimeArrayExpr.

type TimeCase

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

TimeCase builds a CASE whose result is a time. Start it with CaseTime.

func CaseTime

func CaseTime() TimeCase

CaseTime starts a CASE whose result is a time.

func (TimeCase) Else

func (b TimeCase) Else(v TimeValue) TimeExpr

Else ends the CASE with ELSE v.

func (TimeCase) End

func (b TimeCase) End() TimeExpr

End ends the CASE without ELSE.

func (TimeCase) When

func (b TimeCase) When(cond Pred, v TimeValue) TimeCase

When adds WHEN cond THEN v.

type TimeCol

type TimeCol struct{ Col }

TimeCol is a timestamp, timestamptz or date column.

func (TimeCol) Add

func (c TimeCol) Add(i IntervalValue) TimeExpr

Add is (col + i).

func (TimeCol) AtTimeZone

func (c TimeCol) AtTimeZone(zone StringValue) TimeExpr

AtTimeZone is timezone(zone, col).

func (TimeCol) Between

func (c TimeCol) Between(lo, hi time.Time) Pred

func (TimeCol) Coalesce

func (c TimeCol) Coalesce(vs ...any) TimeExpr

Coalesce is coalesce(col, vs…), typed as a time.

func (TimeCol) Eq

func (c TimeCol) Eq(v time.Time) Pred

func (TimeCol) Greatest

func (c TimeCol) Greatest(others ...TimeValue) TimeExpr

Greatest is greatest(col, others…).

func (TimeCol) Gt

func (c TimeCol) Gt(v time.Time) Pred

func (TimeCol) Gte

func (c TimeCol) Gte(v time.Time) Pred

func (TimeCol) InArray

func (c TimeCol) InArray(a TimeArrayValue) Pred

InArray is (col = ANY(a)).

func (TimeCol) IsDistinctFrom

func (c TimeCol) IsDistinctFrom(v time.Time) Pred

IsDistinctFrom is col IS DISTINCT FROM v.

func (TimeCol) IsDistinctFromExpr

func (c TimeCol) IsDistinctFromExpr(o TimeValue) Pred

IsDistinctFromExpr is col IS DISTINCT FROM o.

func (TimeCol) IsNotDistinctFrom

func (c TimeCol) IsNotDistinctFrom(v time.Time) Pred

IsNotDistinctFrom is col IS NOT DISTINCT FROM v.

func (TimeCol) IsNotDistinctFromExpr

func (c TimeCol) IsNotDistinctFromExpr(o TimeValue) Pred

IsNotDistinctFromExpr is col IS NOT DISTINCT FROM o.

func (TimeCol) Least

func (c TimeCol) Least(others ...TimeValue) TimeExpr

Least is least(col, others…).

func (TimeCol) Lt

func (c TimeCol) Lt(v time.Time) Pred

func (TimeCol) Lte

func (c TimeCol) Lte(v time.Time) Pred

func (TimeCol) Ne

func (c TimeCol) Ne(v time.Time) Pred

func (TimeCol) NotInArray

func (c TimeCol) NotInArray(a TimeArrayValue) Pred

NotInArray is (col <> ALL(a)).

func (TimeCol) Since

func (c TimeCol) Since(u TimeValue) IntervalExpr

Since is (col - u).

func (TimeCol) Sub

func (c TimeCol) Sub(i IntervalValue) TimeExpr

Sub is (col - i).

func (TimeCol) To

func (c TimeCol) To(v time.Time) Assignment

func (TimeCol) ToChar

func (c TimeCol) ToChar(format string) StringExpr

ToChar is to_char(col, format).

type TimeExpr

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

TimeExpr is a computed time: now(), to_timestamp of epoch seconds, a time in a zone, a truncated time.

func CastTime

func CastTime(e Expr) TimeExpr

CastTime is CAST(e AS timestamptz): a timestamp, a date or time text as a timestamptz.

func DateTrunc

func DateTrunc(field string, t TimeValue) TimeExpr

DateTrunc is date_trunc('field', t): t truncated to the start of its field ("day", "month", "year", …). An unknown field makes an expression that fails.

func GenerateTimeSeries

func GenerateTimeSeries(start, stop TimeValue, step string) TimeExpr

GenerateTimeSeries is generate_series(start, stop, step): the times from start to stop every step, an interval such as "1 day" bound as $n::interval.

func Now

func Now() TimeExpr

Now is now(), the start time of the current transaction.

func NullTime

func NullTime() TimeExpr

NullTime is NULL::timestamptz.

func Time

func Time(t time.Time) TimeExpr

Time binds t as a timestamptz value, $n::timestamptz.

func TimeOf

func TimeOf(e Expr) TimeExpr

TimeOf types e as a time without changing the SQL.

func TimePtr

func TimePtr(p *time.Time) TimeExpr

TimePtr binds *p as $n::timestamptz, NULL when p is nil.

func ToTimestamp

func ToTimestamp(seconds NumberValue) TimeExpr

ToTimestamp is to_timestamp(seconds): Unix seconds as a timestamptz.

func (TimeExpr) Add

func (e TimeExpr) Add(i IntervalValue) TimeExpr

Add is (e + i): the time an interval later.

func (TimeExpr) Asc

func (e TimeExpr) Asc() Order

func (TimeExpr) AtTimeZone

func (e TimeExpr) AtTimeZone(zone StringValue) TimeExpr

AtTimeZone is timezone(zone, e): a timestamptz as the local time in zone, a timestamp without time zone read as a time in zone. Pass keel.Text(name) for a zone name from Go.

func (TimeExpr) Between

func (e TimeExpr) Between(lo, hi time.Time) Pred

func (TimeExpr) Coalesce

func (e TimeExpr) Coalesce(vs ...any) TimeExpr

Coalesce is coalesce(e, vs…): each v a time bound as a parameter or an expression, taken in order where everything before it is NULL.

func (TimeExpr) Desc

func (e TimeExpr) Desc() Order

func (TimeExpr) Eq

func (e TimeExpr) Eq(v time.Time) Pred

func (TimeExpr) EqExpr

func (e TimeExpr) EqExpr(o TimeValue) Pred

func (TimeExpr) Greatest

func (e TimeExpr) Greatest(others ...TimeValue) TimeExpr

Greatest is greatest(e, others…), the latest non-NULL time.

func (TimeExpr) Gt

func (e TimeExpr) Gt(v time.Time) Pred

func (TimeExpr) GtExpr

func (e TimeExpr) GtExpr(o TimeValue) Pred

func (TimeExpr) Gte

func (e TimeExpr) Gte(v time.Time) Pred

func (TimeExpr) GteExpr

func (e TimeExpr) GteExpr(o TimeValue) Pred

func (TimeExpr) InArray

func (e TimeExpr) InArray(a TimeArrayValue) Pred

InArray is (e = ANY(a)).

func (TimeExpr) IsDistinctFrom

func (e TimeExpr) IsDistinctFrom(v time.Time) Pred

IsDistinctFrom is e IS DISTINCT FROM v.

func (TimeExpr) IsDistinctFromExpr

func (e TimeExpr) IsDistinctFromExpr(o TimeValue) Pred

IsDistinctFromExpr is e IS DISTINCT FROM o.

func (TimeExpr) IsNotDistinctFrom

func (e TimeExpr) IsNotDistinctFrom(v time.Time) Pred

IsNotDistinctFrom is e IS NOT DISTINCT FROM v.

func (TimeExpr) IsNotDistinctFromExpr

func (e TimeExpr) IsNotDistinctFromExpr(o TimeValue) Pred

IsNotDistinctFromExpr is e IS NOT DISTINCT FROM o.

func (TimeExpr) IsNotNull

func (e TimeExpr) IsNotNull() Pred

func (TimeExpr) IsNull

func (e TimeExpr) IsNull() Pred

func (TimeExpr) Least

func (e TimeExpr) Least(others ...TimeValue) TimeExpr

Least is least(e, others…), the earliest non-NULL time.

func (TimeExpr) Lt

func (e TimeExpr) Lt(v time.Time) Pred

func (TimeExpr) LtExpr

func (e TimeExpr) LtExpr(o TimeValue) Pred

func (TimeExpr) Lte

func (e TimeExpr) Lte(v time.Time) Pred

func (TimeExpr) LteExpr

func (e TimeExpr) LteExpr(o TimeValue) Pred

func (TimeExpr) Max

func (e TimeExpr) Max() Agg

func (TimeExpr) Min

func (e TimeExpr) Min() Agg

func (TimeExpr) Ne

func (e TimeExpr) Ne(v time.Time) Pred

func (TimeExpr) NeExpr

func (e TimeExpr) NeExpr(o TimeValue) Pred

func (TimeExpr) NotInArray

func (e TimeExpr) NotInArray(a TimeArrayValue) Pred

NotInArray is (e <> ALL(a)).

func (TimeExpr) Since

func (e TimeExpr) Since(u TimeValue) IntervalExpr

Since is (e - u): the interval from u to e, negative when u is later.

func (TimeExpr) Sub

func (e TimeExpr) Sub(i IntervalValue) TimeExpr

Sub is (e - i): the time an interval earlier.

func (TimeExpr) ToChar

func (e TimeExpr) ToChar(format string) StringExpr

ToChar is to_char(e, format): e formatted by a Postgres template ("YYYY-MM-DD").

type TimeValue

type TimeValue interface {
	Expr
	// contains filtered or unexported methods
}

TimeValue is a time: a TimeCol or a TimeExpr.

type TupleExpr

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

TupleExpr is a row value, ROW(a, b, …), built by keel.Tuple. Rows compare element by element in order, so keel.Tuple(e.OccurredAt, e.ID).Gt(keel.Tuple(after.At, after.ID)) is the keyset condition of a page ordered by occurred_at, id ascending, which an index on (occurred_at, id) serves as one range scan; a Desc page uses Lt.

func Tuple

func Tuple(vs ...any) TupleExpr

Tuple is the row value ROW(vs…): each v is an expression written in place, nil as NULL, or a Go value bound as a parameter whose type Postgres takes from the element it is compared with. A row needs at least two values; one with fewer makes a comparison that fails.

func (TupleExpr) Eq

func (w TupleExpr) Eq(o TupleExpr) Pred

Eq is (w = o).

func (TupleExpr) Gt

func (w TupleExpr) Gt(o TupleExpr) Pred

Gt is (w > o).

func (TupleExpr) Gte

func (w TupleExpr) Gte(o TupleExpr) Pred

Gte is (w >= o).

func (TupleExpr) InUnnest

func (w TupleExpr) InUnnest(arrays ...Expr) Pred

InUnnest is (w IN (SELECT * FROM unnest(arrays…))): w equals one of the rows the arrays zip into, element i of each array making row i. Pass typed array literals (keel.TextArray(keys), keel.Int64Array(ns)), each bound as one parameter, so the statement keeps one shape whatever the number of rows.

func (TupleExpr) IsDistinctFrom

func (w TupleExpr) IsDistinctFrom(o TupleExpr) Pred

IsDistinctFrom is (w IS DISTINCT FROM o): some element differs, NULL counting as a value, as an upsert's UpdateWhere skips a row whose stored values equal the proposed ones.

func (TupleExpr) IsNotDistinctFrom

func (w TupleExpr) IsNotDistinctFrom(o TupleExpr) Pred

IsNotDistinctFrom is (w IS NOT DISTINCT FROM o).

func (TupleExpr) Lt

func (w TupleExpr) Lt(o TupleExpr) Pred

Lt is (w < o).

func (TupleExpr) Lte

func (w TupleExpr) Lte(o TupleExpr) Pred

Lte is (w <= o).

func (TupleExpr) Ne

func (w TupleExpr) Ne(o TupleExpr) Pred

Ne is (w <> o): some element differs.

type TxInfo

type TxInfo struct {
	ReadOnly bool
	Implicit bool
}

TxInfo describes the transaction being started.

type TxOption

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

TxOption configures Read and Write. The isolation levels (ReadCommitted, RepeatableRead, Serializable) are options themselves: keel.Read(ctx, db, fn, keel.RepeatableRead).

func Detached

func Detached() TxOption

Detached runs Read or Write in a new transaction on a connection of its own even when the context carries a transaction, which it then neither joins nor waits for: it commits or rolls back on its own, and its work is not undone when the outer transaction rolls back. It begins with the context's scope, so Config.OnBegin sees that scope, and for fn's duration it hides the outer transaction: statements, Savepoint, LockKey and AfterCommit inside fn use the detached transaction, and its AfterCommit callbacks run when it commits, with the caller's context. Use it for work under another scope than the outer transaction's (a system-scoped read inside a tenant's transaction) and for work that must commit whatever the caller does (allocating a number from a sequence table).

It sees what other transactions committed, not the outer transaction's uncommitted writes, and holds a second pool connection while the outer one holds its first: size the pool for it, and do not wait inside it on locks the outer transaction holds, which never come free. Outside a transaction it is a plain Read or Write.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

func withSystem(ctx context.Context, reason string) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{SystemReason: reason})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	err := keel.Write(ctx, db, func(ctx context.Context) error {
		pro, err := keel.WriteValue(ctx, db, func(ctx context.Context) (int64, error) {
			return keel.QueryValue[int64](ctx, db,
				"UPDATE pro_sequences SET next = next + 1 RETURNING next - 1")
		}, keel.Detached(), keel.LockTimeout(time.Second))
		if err != nil {
			return err
		}
		open, err := keel.ReadValue(withSystem(ctx, "open shipments across tenants"), db,
			func(ctx context.Context) (int64, error) {
				return s.AllTenants().Where(s.Status.Ne(fleet.StatusDelivered)).Count(ctx, db)
			}, keel.Detached())
		if err != nil {
			return err
		}
		fmt.Println("open shipments:", open)
		return s.Create(ctx, db, &fleet.Shipment{ProNumber: fmt.Sprintf("PRO-%d", pro), Status: fleet.StatusNew})
	})
	if err != nil {
		log.Fatal(err)
	}
}

func LockTimeout

func LockTimeout(d time.Duration) TxOption

LockTimeout bounds how long any statement of the transaction waits for a lock.

func ReplicaTx

func ReplicaTx() TxOption

ReplicaTx runs a Read on a replica, for long read-only work such as a report or an export: the transaction holds a replica connection from its first statement to its end, and every terminal in it runs there. It picks the replica as OnReplica does, among those the lag tracker has seen replay the context's commit position, falls back to the primary when none qualifies, and is refused on a Write and with Serializable, which a hot standby does not run.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/emoss08/keel"
	"github.com/emoss08/keel/internal/testmodels/fleet"
	"github.com/emoss08/keel/internal/testmodels/fleet/fleetdb"
	"github.com/emoss08/keel/internal/testmodels/ids"
)

// appDB is the application's database, opened once at startup (see ExampleOpen).
var appDB *keel.DB

// scope is what the application's authentication middleware puts on a request context.
type scope struct {
	OrganizationID ids.ID
	BusinessUnitID ids.ID

	SystemReason string
}

type scopeKey struct{}

func withTenant(ctx context.Context, org, bu ids.ID) context.Context {
	return context.WithValue(ctx, scopeKey{}, scope{OrganizationID: org, BusinessUnitID: bu})
}

// requestContext returns the context of a request authenticated for one tenant.
func requestContext() context.Context {
	return withTenant(context.Background(), "org_acme", "bu_dallas")
}

func main() {
	ctx, db := requestContext(), appDB
	s := fleetdb.Shipments

	err := keel.Read(ctx, db, func(ctx context.Context) error {
		return s.Where(s.Status.Eq(fleet.StatusDelivered)).Each(ctx, db, 1000,
			func(ctx context.Context, batch []*fleet.Shipment) error {
				for _, sh := range batch {
					fmt.Println(sh.ProNumber, sh.Total)
				}
				return nil
			})
	}, keel.ReplicaTx(), keel.RepeatableRead)
	if err != nil {
		log.Fatal(err)
	}
}

func Retry

func Retry(n int) TxOption

Retry runs a top-level transaction up to n more times after a serialization failure or a deadlock. fn must be safe to repeat.

func Setting

func Setting(name, value string) TxOption

Setting sets the run-time parameter name to value for the transaction, as SET LOCAL does: set_config(name, value, true), for a planner or extension setting one query needs, such as hnsw.ef_search. A transaction Read or Write begins queues it with BEGIN, in the same round trip. A Read or Write that joins a transaction already on the context sets it for fn and restores the joined transaction's value when fn returns.

type ValueType

type ValueType uint8

ValueType is the type of a column of a dynamic result, and a type Cast accepts.

const (
	// TypeString is text, varchar, char, name, enums, uuid and any type read in its text form.
	TypeString ValueType = iota + 1
	// TypeInt64 is smallint, integer, bigint and oid.
	TypeInt64
	// TypeFloat64 is real and double precision.
	TypeFloat64
	// TypeNumeric is numeric, kept exact as a Decimal.
	TypeNumeric
	// TypeBool is boolean.
	TypeBool
	// TypeTime is timestamptz and timestamp, in UTC.
	TypeTime
	// TypeDate is date, as midnight UTC.
	TypeDate
	// TypeBytes is bytea.
	TypeBytes
	// TypeJSON is json, jsonb and every other type (arrays, ranges, intervals), as raw JSON.
	TypeJSON
)

func (ValueType) String

func (t ValueType) String() string

String returns the type's name.

type WaitKind

type WaitKind uint8

WaitKind is what a slow wait waited for.

const (
	// WaitTenantSlot is a terminal or transaction waiting for one of its tenant's slots.
	WaitTenantSlot WaitKind = iota + 1
	// WaitConnection is a terminal, transaction or version read waiting for a pool connection.
	WaitConnection
	// WaitCacheFill is a terminal waiting for another goroutine's read of the same result cache
	// entry.
	WaitCacheFill
)

The kinds of wait SlowWaitConfig reports.

func (WaitKind) String

func (k WaitKind) String() string

String returns the kind's name: tenant_slot, connection or cache_fill.

type Window

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

Window is a window specification: the partition, the order and the frame of a window function. The zero Window is OVER (): every row of the result, unordered. Build one with PartitionBy, OrderBy or NamedWindow and give it to Agg.OverWindow; methods return a changed copy.

running := keel.Sum(s.Weight).OverWindow(keel.PartitionBy(s.CustomerID).
    OrderBy(s.CreatedAt.Asc()).Rows(keel.UnboundedPreceding, keel.CurrentRow))

func NamedWindow

func NamedWindow(name string) Window

NamedWindow refers to a window the query declares with Window. It may add an order, when the named window has none, and a frame; it cannot add a partition.

func OrderBy

func OrderBy(os ...Order) Window

OrderBy starts a window over every row, ordered by os.

func PartitionBy

func PartitionBy(es ...Expr) Window

PartitionBy starts a window whose partitions are the rows that share es.

func (Window) Exclude

func (w Window) Exclude(x FrameExclusion) Window

Exclude removes rows around the current row from the frame. It needs a frame.

func (Window) Groups

func (w Window) Groups(start, end FrameBound) Window

Groups sets the frame to GROUPS BETWEEN start AND end: offsets count peer groups.

func (Window) OrderBy

func (w Window) OrderBy(os ...Order) Window

OrderBy adds order terms.

func (Window) PartitionBy

func (w Window) PartitionBy(es ...Expr) Window

PartitionBy adds partition expressions.

func (Window) Range

func (w Window) Range(start, end FrameBound) Window

Range sets the frame to RANGE BETWEEN start AND end: offsets are distances in the value of the window's single order expression, and CurrentRow includes the row's peers.

func (Window) Rows

func (w Window) Rows(start, end FrameBound) Window

Rows sets the frame to ROWS BETWEEN start AND end: offsets count rows.

Directories

Path Synopsis
analyticscli
Package analyticscli adds the analytics commands to a keelcli database binary:
Package analyticscli adds the analytics commands to a keelcli database binary:
cache
Package cache holds result caches for keel/analytics: byte values by key, each with a time to live.
Package cache holds result caches for keel/analytics: byte values by key, each with a time to live.
diff
Package diff compares two runs of the same report: which rows appeared, disappeared or moved, and by how much.
Package diff compares two runs of the same report: which rows appeared, disappeared or moved, and by how much.
export
Package export writes a result (a table.Reader) as CSV, a JSON envelope, an XLSX workbook or an Arrow IPC stream, streaming: each writer holds one batch at a time, so memory stays flat however many rows the result has.
Package export writes a result (a table.Reader) as CSV, a JSON envelope, an XLSX workbook or an Arrow IPC stream, streaming: each writer holds one batch at a time, so memory stays flat however many rows the result has.
format
Package format is the display layer of keel/analytics: how a report column's values are shown.
Package format is the display layer of keel/analytics: how a report column's values are shown.
format/internal/cldrgen command
Command cldrgen writes currency_cldr.go: where each CLDR locale places the currency symbol around a number.
Command cldrgen writes currency_cldr.go: where each CLDR locale places the currency symbol around a number.
formula
Package formula is the typed expression language of keel/analytics report calculations: a lexer, a Pratt parser, an AST that also has a JSON encoding, a type checker over an environment, a function registry with deterministic SQL rendering descriptors, and a canonical printer.
Package formula is the typed expression language of keel/analytics report calculations: a lexer, a Pratt parser, an AST that also has a JSON encoding, a type checker over an environment, a function registry with deterministic SQL rendering descriptors, and a canonical printer.
ir
Package ir is the report definition of keel/analytics: the types an application stores for a user-built report, their JSON encoding and JSON Schema, versioning with upgrades, validation with user-facing errors, dashboard filter merging and drill-through rewriting.
Package ir is the report definition of keel/analytics: the types an application stores for a user-built report, their JSON encoding and JSON Schema, versioning with upgrades, validation with user-facing errors, dashboard filter merging and drill-through rewriting.
ir/internal/gen command
Command gen writes the generated files of analytics/ir: the JSON Schema, the TypeScript golden module and the formula reference in the guide.
Command gen writes the generated files of analytics/ir: the JSON Schema, the TypeScript golden module and the formula reference in the guide.
ir/internal/schemagen
Package schemagen generates the JSON Schema of analytics/ir definitions from the Go types, with descriptions taken from their doc comments.
Package schemagen generates the JSON Schema of analytics/ir definitions from the Go types, with descriptions taken from their doc comments.
ir/tsgen
Package tsgen writes analytics/ir as a TypeScript module: the definition types and JSON Schema, the operator table per value type, the chart and transform rules, the formula function registry and applyFilters, so a client builder uses the server's rules instead of copies of them.
Package tsgen writes analytics/ir as a TypeScript module: the definition types and JSON Schema, the operator table per value type, the chart and transform rules, the formula function registry and applyFilters, so a client builder uses the server's rules instead of copies of them.
rollup
Package rollup maintains pre-aggregated tables for keel/analytics and routes reports to them.
Package rollup maintains pre-aggregated tables for keel/analytics and routes reports to them.
table
Package table is the typed, columnar result that analytics produces and its writers and diffs consume: a schema of columns and a stream of row batches, each column held in one typed vector with a null bitmap.
Package table is the typed, columnar result that analytics produces and its writers and diffs consume: a schema of columns and a stream of row batches, each column held in one typed vector with a null bitmap.
value
Package value names the types a report column holds and the columnar vectors that hold its values.
Package value names the types a report column holds and the columnar vectors that hold its values.
cache
rediscache
Package rediscache puts keel's result cache (keel.CacheConfig.Shared) on a Redis-compatible server (Redis, Valkey, KeyDB, Dragonfly), reached through the application's own client, so keel depends on no client library.
Package rediscache puts keel's result cache (keel.CacheConfig.Shared) on a Redis-compatible server (Redis, Valkey, KeyDB, Dragonfly), reached through the application's own client, so keel depends on no client library.
cmd
keel command
Command keel generates typed data-access packages for keel models and manages migrations.
Command keel generates typed data-access packages for keel models and manages migrations.
Package gen writes the typed data-access packages for keel models.
Package gen writes the typed data-access packages for keel models.
internal
testdump
Package testdump gives the squash tests the pg_dump command of the test database's server.
Package testdump gives the squash tests the pg_dump command of the test database's server.
testmodels/fleet
Package fleet holds the models keel's tests run against.
Package fleet holds the models keel's tests run against.
testmodels/geo
Package geo holds models with PostGIS columns, read and written through a Go type with its own codec, as Trenova's postgis.Point is: a sql.Scanner and driver.Valuer on the pointer that decode EWKB and encode WKB.
Package geo holds models with PostGIS columns, read and written through a Go type with its own codec, as Trenova's postgis.Point is: a sql.Scanner and driver.Valuer on the pointer that decode EWKB and encode WKB.
testmodels/ids
Package ids holds the identifier type of the test models.
Package ids holds the identifier type of the test models.
Package keelcli turns an application's migrations and seeds into the command line its database binaries are made of.
Package keelcli turns an application's migrations and seeds into the command line its database binaries are made of.
cmd/demo command
Command demo is a keelcli database binary over the fake engine, for trying the console and the live views without a database.
Command demo is a keelcli database binary over the fake engine, for trying the console and the live views without a database.
factsport
Package factsport declares the facts about a database that keelcli's doctor and console header read: the connection, server settings and extensions, the backends holding locks or running long queries, and the environment mark.
Package factsport declares the facts about a database that keelcli's doctor and console header read: the connection, server settings and extensions, the backends holding locks or running long queries, and the environment mark.
fake
Package fake is an in-memory migration engine, seed engine and database that implements every keelcli port with simulated timing.
Package fake is an in-memory migration engine, seed engine and database that implements every keelcli port with simulated timing.
internal/adapt
Package adapt implements keelcli's ports over the real engines: keel/migrate for migrations and templates, keel/seed for seeds and the environment mark, and the connection pool for the facts doctor and the console header read.
Package adapt implements keelcli's ports over the real engines: keel/migrate for migrations and templates, keel/seed for seeds and the environment mark, and the connection pool for the facts doctor and the console header read.
internal/doctor
Package doctor runs keelcli's database checks from the facts and migration ports and renders them; the doctor command and the console's Doctor tab share it.
Package doctor runs keelcli's database checks from the facts and migration ports and renders them; the doctor command and the console's Doctor tab share it.
internal/guard
Package guard decides whether a destructive action may run against a database: it reads the database and its environment mark and refuses a production-marked database unless the user said so.
Package guard decides whether a destructive action may run against a database: it reads the database and its environment mark and refuses a production-marked database unless the user said so.
internal/plans
Package plans is keelcli's plan guard: it explains recorded statement shapes with EXPLAIN (GENERIC_PLAN, VERBOSE, FORMAT JSON) and checks every plan against the rules, so a query that would scan a large table, cost too much or sort a page without an index is caught before it ships.
Package plans is keelcli's plan guard: it explains recorded statement shapes with EXPLAIN (GENERIC_PLAN, VERBOSE, FORMAT JSON) and checks every plan against the rules, so a query that would scan a large table, cost too much or sort a page without an index is caught before it ships.
internal/render
Package render turns keelcli's results into text: tables that fit a width, one line per run event, and JSON.
Package render turns keelcli's results into text: tables that fit a width, one line per run event, and JSON.
internal/term
Package term detects whether keelcli's streams are terminals and how wide they are.
Package term detects whether keelcli's streams are terminals and how wide they are.
internal/ui/busy
Package busy shows a spinner beside a label while one engine call runs, for the commands whose call takes a while but reports no progress: template build, squash, reset.
Package busy shows a spinner beside a label while one engine call runs, for the commands whose call takes a while but reports no progress: template build, squash, reset.
internal/ui/console
Package console is keelcli's full-screen console: tabs for migrations, seeds, templates, lint findings and doctor checks over the same ports the commands call.
Package console is keelcli's full-screen console: tabs for migrations, seeds, templates, lint findings and doctor checks over the same ports the commands call.
internal/ui/live
Package live is the view of a running deploy step: one row per migration or seed with a spinner, elapsed time, retries, rows and per-tenant progress, a panel naming the backends a lock waits for, and a summary of what ran and what was left undone.
Package live is the view of a running deploy step: one row per migration or seed with a spinner, elapsed time, retries, rows and per-tenant progress, a panel naming the backends a lock waits for, and a summary of what ran and what was left undone.
internal/ui/prompt
Package prompt holds keelcli's Huh forms: the confirmation of a destructive action and the questions migrate create asks.
Package prompt holds keelcli's Huh forms: the confirmation of a destructive action and the questions migrate create asks.
internal/ui/sqlview
Package sqlview highlights migration SQL for the terminal: keywords, strings, numbers, comments and keel directives, with line numbers and a marked line for lint findings.
Package sqlview highlights migration SQL for the terminal: keywords, strings, numbers, comments and keel directives, with line numbers and a marked line for lint findings.
internal/ui/theme
Package theme is keelcli's one definition of colour and style.
Package theme is keelcli's one definition of colour and style.
migrateport
Package migrateport declares what keelcli needs from a migration engine.
Package migrateport declares what keelcli needs from a migration engine.
seedport
Package seedport declares what keelcli needs from a seed engine.
Package seedport declares what keelcli needs from a seed engine.
Package keellog prints the statements keel sends, for a developer reading a terminal: one header line per statement (operation, table, transaction, duration, rows or outcome, and the code that issued it), then the SQL broken at its clauses, with the bound values written in when keel.Config.ObserveArgs is set.
Package keellog prints the statements keel sends, for a developer reading a terminal: one header line per statement (operation, table, transaction, duration, rows or outcome, and the code that issued it), then the SQL broken at its clauses, with the bound values written in when keel.Config.ObserveArgs is set.
Package keeltest holds test helpers for code that builds statements with keel: golden snapshots of rendered statements, and plan assertions that an index serves a query.
Package keeltest holds test helpers for code that builds statements with keel: golden snapshots of rendered statements, and plan assertions that an index serves a query.
Package migrate applies SQL and Go migrations to a Postgres database.
Package migrate applies SQL and Go migrations to a Postgres database.
Package numeric reads and writes github.com/shopspring/decimal values in PostgreSQL's binary numeric format.
Package numeric reads and writes github.com/shopspring/decimal values in PostgreSQL's binary numeric format.
Package postgis maps PostGIS geometry and geography columns to Go values and builds spatial SQL with keel's expressions.
Package postgis maps PostGIS geometry and geography columns to Go values and builds spatial SQL with keel's expressions.
Package schema holds the runtime description of keel models.
Package schema holds the runtime description of keel models.
Package seed applies the data a database needs, reference data and development fixtures, as declared seeds that re-run exactly when what they are made from changes.
Package seed applies the data a database needs, reference data and development fixtures, as declared seeds that re-run exactly when what they are made from changes.
Package vector builds pgvector expressions for keel queries: the distance operators, the casts an expression index is declared over, and the HNSW search settings.
Package vector builds pgvector expressions for keel queries: the distance operators, the casts an expression index is declared over, and the HNSW search settings.

Jump to

Keyboard shortcuts

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