clickhouse

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package clickhouse provides a ClickHouse dialect for drops.

The shape mirrors the drops/pg package — typed columns via Col[T], table declarations, fluent query builders, and a DB wrapper with the same Hook / Ping / Close / InTx surface — but the SQL it emits is ClickHouse-flavoured:

  • "?" positional placeholders (matches the clickhouse-go database/sql driver)
  • Engine-bound tables (CREATE TABLE … ENGINE = MergeTree() ORDER BY …)
  • Native CH types: Array(T), Nullable(T), LowCardinality(T), Decimal(P,S), Map(K,V), Tuple(...), Enum8/16, DateTime[64], fixed-width integers (UInt8/UInt64/Int8/Int64/…), UUID
  • Aggregates such as uniq, uniqExact, quantile, anyAgg

Driver-agnostic: like drops/pg, the package imports no concrete driver. The bundled drops/stdlib adapter works against any database/ sql-compatible ClickHouse driver — clickhouse-go's stdlib bridge is the standard choice:

import (
    _ "github.com/ClickHouse/clickhouse-go/v2"
    "github.com/bernardoforcillo/drops/clickhouse"
    "github.com/bernardoforcillo/drops/stdlib"
)

sqlDB, _ := sql.Open("clickhouse", "clickhouse://localhost:9000/default")
db := clickhouse.New(stdlib.New(sqlDB))

Templates (Timestamps, SoftDelete, Audit, UUIDPrimaryKey) provide reusable column groups — see template.go for the function-style pattern and mixin.go for the richer Mixin interface. ClickHouse has no foreign keys, so Audit emits plain scalar columns mirroring the target's type. Lifecycle hooks are limited to OnInsert (no builder-side UPDATE/DELETE); default filters on SelectBuilder honour Unscoped() for opt-out.

Entity[T] (see entity.go) binds a Go struct to a Table and exposes Create / CreateMany / Query — the narrow subset of CRUD that maps to ClickHouse's Insert + Select builders. Entity.Validate registers per-row validators that run before Create / CreateMany; on CreateMany the first failing row aborts the whole batch.

What this package does NOT try to mirror from drops/pg:

  • per-row UPDATE/DELETE (ClickHouse mutations are asynchronous and fundamentally different; use ALTER TABLE … UPDATE/DELETE via raw SQL when you need them)
  • ON CONFLICT (handled by engine choice, e.g. ReplacingMergeTree)
  • Foreign keys / referential integrity (ClickHouse has none)
  • Schema introspection and Push (planned)

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNoRows               = errors.New("drops/clickhouse: no rows in result set")
	ErrNoRowsToInsert       = errors.New("drops/clickhouse: INSERT with no rows")
	ErrReturningUnsupported = errors.New("drops/clickhouse: RETURNING is not supported by ClickHouse")
)

Sentinel errors for assertable failure modes.

View Source
var (
	ErrTreeCycleDetected       = errors.New("drops/clickhouse: migration DAG has a cycle")
	ErrTreeMissingParent       = errors.New("drops/clickhouse: migration references unknown parent")
	ErrTreeIrreversible        = errors.New("drops/clickhouse: migration has no Down (irreversible)")
	ErrTreeNoMigrationsApplied = errors.New("drops/clickhouse: no tree migrations applied")
	ErrTreeUnknownMigration    = errors.New("drops/clickhouse: unknown migration ID")
	// ErrTreeMigrationTampered is returned when an already-applied
	// migration's SQL checksum no longer matches what was recorded at
	// apply time.
	ErrTreeMigrationTampered = errors.New("drops/clickhouse: applied migration SQL has changed — investigate before proceeding")
)

Sentinel errors.

View Source
var ErrCircularDependency = errors.New("drops/clickhouse: MatViewManager: circular dependency")

ErrCircularDependency is returned by topological helpers when a cycle is detected.

View Source
var ErrConcurrencyConflict = errors.New("drops/clickhouse: event store concurrency conflict")

ErrConcurrencyConflict is returned by Append when the expectedVersion no longer matches the stream's head — another writer beat us to that version. Wrap your callsite in a retry loop that re-reads the stream before retrying.

Note: unlike the PostgreSQL variant this check is advisory (not database-enforced), so it catches concurrent writers only under normal single-writer-per-aggregate usage.

View Source
var ErrEngineRequired = errors.New("drops/clickhouse: table has no Engine set; call .Engine(clickhouse.MergeTree()) before CreateTable")

ErrEngineRequired is returned by CreateTable when the target table doesn't yet have an Engine set. ClickHouse refuses CREATE TABLE without an ENGINE clause, so we fail fast and clearly.

View Source
var ErrInvalidIdentifier = errors.New("drops/clickhouse: invalid SQL identifier")

ErrInvalidIdentifier is returned when a table / database / column name fails validation. The decorating wrappers use fmt.Errorf with %w so errors.Is(err, ErrInvalidIdentifier) succeeds.

View Source
var Placeholder = drops.WithPlaceholder(func(int) string { return "?" })

Placeholder is the placeholder strategy used by the ClickHouse dialect — bare positional question marks, matching the clickhouse-go database/sql driver.

Functions

func Abs

func Abs(e any) drops.Expression

func And

func And(preds ...drops.Expression) drops.Expression

func AnyAgg

func AnyAgg(e drops.Expression) drops.Expression

AnyAgg returns an arbitrary value from the group (CH's `any`). Named AnyAgg to avoid colliding with Go's any keyword visually.

func AnyHeavy

func AnyHeavy(e drops.Expression) drops.Expression

func AnyLast

func AnyLast(e drops.Expression) drops.Expression

AnyLast / AnyHeavy variants.

func ArgMax

func ArgMax(value, by drops.Expression) drops.Expression

Argument-aware aggregates: argMax / argMin.

func ArgMin

func ArgMin(value, by drops.Expression) drops.Expression

func As

func As(e drops.Expression, alias string) drops.Expression

As renames an expression (for SELECT projections).

func Avg

func Between

func Between(left, low, high any) drops.Expression

func Cast added in v0.5.0

func Cast(e any, typeSQL string) drops.Expression

Cast is an alias for CastAs, provided for source-level parity with the other dialects.

func CastAs added in v0.5.0

func CastAs(e any, typeSQL string) drops.Expression

CastAs renders CAST(<e> AS <type>) — ClickHouse's standard-SQL type conversion. typeSQL is a ClickHouse type name such as "UInt32", "String" or "DateTime". ClickHouse also accepts the toType() function family (toInt32, toString, …); use Func for those.

func Coalesce

func Coalesce(args ...any) drops.Expression

func Count

func CountAll

func CountAll() drops.Expression

func CreateDatabase

func CreateDatabase(name string) drops.Expression

func CreateDatabaseIfNotExists

func CreateDatabaseIfNotExists(name string) drops.Expression

func CreateTable

func CreateTable(t *Table) drops.Expression

CreateTable returns a CREATE TABLE statement for t. It panics-via- expression: rendering builds a SQL fragment that ends up emitting a clearly-marked error string if the engine is missing, so a caller who forgets gets a loud failure at exec time rather than silent bad DDL. Use CreateTableErr if you want the engine check at build time.

Example

ExampleCreateTable shows the rendered DDL for a MergeTree table.

package main

import (
	"fmt"

	"github.com/bernardoforcillo/drops/clickhouse"
)

// Schema fixtures used by the godoc examples — picked from a small
// analytics-style table so the rendered SQL is recognisable.
var exEvents = clickhouse.NewTable("events")

func main() {
	sql, _ := clickhouse.ToSQL(clickhouse.CreateTableIfNotExists(exEvents))
	// trim for godoc readability — only the first two clauses
	end := 0
	for n := 0; n < 2; n++ {
		end = nthIndex(sql, "\n", end+1)
		if end < 0 {
			break
		}
	}
	fmt.Println(sql[:end])
}

// nthIndex returns the index of the nth occurrence of sep in s
// starting from start, or -1.
func nthIndex(s, sep string, start int) int {
	for i := start; i < len(s); i++ {
		if s[i] == sep[0] {
			return i
		}
	}
	return -1
}
Output:
CREATE TABLE IF NOT EXISTS "events" (
	"id" UUID,

func CreateTableErr

func CreateTableErr(t *Table) (drops.Expression, error)

CreateTableErr returns the DDL or ErrEngineRequired. Use it in migration tooling that wants a definite error rather than SQL that references a sentinel string.

func CreateTableIfNotExists

func CreateTableIfNotExists(t *Table) drops.Expression

CreateTableIfNotExists is the IF NOT EXISTS variant.

func DateDiff

func DateDiff(unit string, a, b any) drops.Expression

func DenseRank added in v0.5.0

func DenseRank() drops.Expression

func DropDatabase

func DropDatabase(name string) drops.Expression

func DropDatabaseIfExists

func DropDatabaseIfExists(name string) drops.Expression

func DropTable

func DropTable(t *Table) drops.Expression

func DropTableIfExists

func DropTableIfExists(t *Table) drops.Expression

func Eq

func Eq(left, right any) drops.Expression

func Exists added in v0.5.0

func Exists(q drops.Expression) drops.Expression

Exists renders EXISTS (<subquery>).

func FirstValue added in v0.5.0

func FirstValue(expr any) drops.Expression

func Func

func Func(name string, args ...any) drops.Expression

Func is the escape hatch for any function not covered by helpers.

func GroupArray

func GroupArray(e drops.Expression) drops.Expression

GroupArray returns an array of all values in the group.

func GroupUniqArray

func GroupUniqArray(e drops.Expression) drops.Expression

GroupUniqArray is the deduplicated variant.

func Gt

func Gt(left, right any) drops.Expression

func Gte

func Gte(left, right any) drops.Expression

func ILike

func ILike(left, pattern any) drops.Expression

func IfNull

func IfNull(e, fallback any) drops.Expression

func In

func In(left any, values ...any) drops.Expression

func InSub added in v0.5.0

func InSub(value any, sub drops.Expression) drops.Expression

InSub renders "<value> IN (<subquery>)".

func IsNotNull

func IsNotNull(e any) drops.Expression

func IsNull

func IsNull(e any) drops.Expression

func Lag added in v0.5.0

func Lag(expr any, args ...any) drops.Expression

Lag renders lagInFrame(expr [, offset [, default]]) — ClickHouse's spelling of the lag window function.

func LastValue added in v0.5.0

func LastValue(expr any) drops.Expression

func Lead added in v0.5.0

func Lead(expr any, args ...any) drops.Expression

Lead renders leadInFrame(expr [, offset [, default]]).

func Length

func Length(e any) drops.Expression

func Like

func Like(left, pattern any) drops.Expression

func LoggerHook

func LoggerHook(log LoggerFunc, opts ...LoggerOptions) drops.Hook

LoggerHook re-exports drops.LoggerHook. See its documentation for behaviour and tuning options.

func Lower

func Lt

func Lt(left, right any) drops.Expression

func Lte

func Lte(left, right any) drops.Expression

func Max

func Min

func Ne

func Ne(left, right any) drops.Expression

func Not

func NotExists added in v0.5.0

func NotExists(q drops.Expression) drops.Expression

NotExists renders NOT EXISTS (<subquery>).

func NotIn

func NotIn(left any, values ...any) drops.Expression

func NotInSub added in v0.5.0

func NotInSub(value any, sub drops.Expression) drops.Expression

NotInSub renders "<value> NOT IN (<subquery>)".

func Now

func Now() drops.Expression

func NthValue added in v0.5.0

func NthValue(expr, n any) drops.Expression

func OptimizeTable

func OptimizeTable(t *Table, final bool) drops.Expression

OptimizeTable triggers a merge round; final=true emits FINAL so all data is collapsed into a single part (expensive on large tables).

func Or

func Or(preds ...drops.Expression) drops.Expression

func Over added in v0.5.0

func Over(fn drops.Expression, win *Window) drops.Expression

Over wraps an aggregate or window function with an OVER clause.

func Quantile

func Quantile(level float64, e drops.Expression) drops.Expression

Quantile is the approximate quantile aggregate; level is in [0, 1].

clickhouse.Quantile(0.95, Latency)  // 95th percentile

func QuantileExact

func QuantileExact(level float64, e drops.Expression) drops.Expression

QuantileExact uses the exact algorithm; QuantileTiming is optimised for non-negative integers (latency in ms).

func QuantileTiming

func QuantileTiming(level float64, e drops.Expression) drops.Expression

func Rank added in v0.5.0

func Rank() drops.Expression

func Round

func Round(e any) drops.Expression

func RowNumber added in v0.5.0

func RowNumber() drops.Expression

func Subquery added in v0.5.0

func Subquery(q drops.Expression) drops.Expression

Subquery wraps an expression (typically a SELECT) in parentheses for use as a scalar subquery.

func Sum

func ToDate

func ToDate(e any) drops.Expression

func ToDateTime

func ToDateTime(e any) drops.Expression

func ToSQL

func ToSQL(e drops.Expression) (sql string, args []any)

ToSQL renders an Expression with the ClickHouse placeholder style. Use it when you need to inspect generated SQL outside of the builders (logging, snapshotting, tests).

func ToStartOfDay

func ToStartOfDay(e any) drops.Expression

func ToStartOfHour

func ToStartOfHour(e any) drops.Expression

func ToStartOfMinute

func ToStartOfMinute(e any) drops.Expression

func ToStartOfMonth

func ToStartOfMonth(e any) drops.Expression

func ToYYYYMM

func ToYYYYMM(e any) drops.Expression

func ToYYYYMMDD

func ToYYYYMMDD(e any) drops.Expression

func TruncateTable

func TruncateTable(t *Table) drops.Expression

func TruncateTableIfExists

func TruncateTableIfExists(t *Table) drops.Expression

func Uniq

Uniq is an approximate-distinct count (HyperLogLog).

func UniqExact

func UniqExact(e drops.Expression) drops.Expression

UniqExact is the exact distinct count.

func UniqHLL12

func UniqHLL12(e drops.Expression) drops.Expression

UniqHLL12 is the configurable HLL approximation.

func Upper

Types

type AggregateSnapshot added in v0.5.0

type AggregateSnapshot struct {
	AggregateType string
	AggregateID   string
	Version       int64
	State         json.RawMessage
	CreatedAt     time.Time
}

AggregateSnapshot persists the materialised state of an aggregate at a specific version so reads avoid replaying every event since the beginning of time. Optional — small aggregates can replay fast enough without.

type AuditCols added in v0.2.0

type AuditCols[T any] struct {
	CreatedBy *Col[T]
	UpdatedBy *Col[T]
}

AuditCols holds the typed handles created by Audit. The type parameter mirrors the type of the supplied identity column.

func Audit added in v0.2.0

func Audit[T any](t *Table, target *Col[T]) AuditCols[T]

Audit appends "createdBy" and "updatedBy" columns to t, mirroring target's SQL type. ClickHouse has no foreign-key enforcement, so the columns are plain scalars; the typed handles let queries still compare them against target safely.

type AuditMixin added in v0.2.0

type AuditMixin[T any] struct {
	Target *Col[T]
	Cols   AuditCols[T]
}

AuditMixin registers "createdBy" and "updatedBy" columns of the same SQL type as Target. ClickHouse has no foreign-key enforcement, so the columns are plain scalars.

func (*AuditMixin[T]) Apply added in v0.2.0

func (m *AuditMixin[T]) Apply(t *Table)

Apply implements Mixin.

type CTE added in v0.5.0

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

CTE describes one common table expression.

func CTEDef added in v0.5.0

func CTEDef(name string, query drops.Expression) *CTE

CTEDef returns a CTE definition binding name to query.

func (*CTE) Name added in v0.5.0

func (c *CTE) Name() string

Name returns the CTE's alias.

func (*CTE) Ref added in v0.5.0

func (c *CTE) Ref() drops.Expression

Ref references the CTE as a relation in a subsequent FROM/JOIN.

type CaseExpr added in v0.5.0

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

CaseExpr is the in-progress CASE expression.

func Case added in v0.5.0

func Case() *CaseExpr

Case begins a searched CASE expression. Chain When / Else / End. ClickHouse supports the standard CASE form (and multiIf as a function-style alternative).

func CaseOn added in v0.5.0

func CaseOn(value any) *CaseExpr

CaseOn begins a simple CASE expression on a value.

func (*CaseExpr) Else added in v0.5.0

func (c *CaseExpr) Else(value any) *CaseExpr

Else sets the ELSE value.

func (*CaseExpr) End added in v0.5.0

func (c *CaseExpr) End() drops.Expression

End finalises the CASE expression.

func (*CaseExpr) When added in v0.5.0

func (c *CaseExpr) When(cond, value any) *CaseExpr

When adds a WHEN <cond> THEN <value> branch.

type Col

type Col[T any] struct {
	*Column
}

Col is the typed handle for a column whose Go value type is T.

func Add

func Add[T any](t *Table, c *Col[T]) *Col[T]

Add registers c with t and returns it. Type inference keeps the *Col[T] handle typed.

var Events = clickhouse.NewTable("events")
var (
    EventID = clickhouse.Add(Events, clickhouse.UUID("id"))
    EventTS = clickhouse.Add(Events, clickhouse.DateTime("ts", "UTC"))
)
Example

ExampleAdd shows the canonical table+columns declaration pattern.

package main

import (
	"fmt"

	"github.com/bernardoforcillo/drops/clickhouse"
)

func main() {
	logs := clickhouse.NewTable("logs")
	id := clickhouse.Add(logs, clickhouse.UInt64("id"))
	msg := clickhouse.Add(logs, clickhouse.String("message").LowCardinality())
	fmt.Printf("%s, %s\n", id.Name(), msg.Name())
}
Output:
id, message

func Bool

func Bool(name string) *Col[bool]

func Custom

func Custom[T any](name, typeSQL string) *Col[T]

Custom creates a column with an arbitrary type literal — useful for IPv4/IPv6, AggregateFunction(...), or vendor types not covered here.

func Date

func Date(name string) *Col[time.Time]

func Date32

func Date32(name string) *Col[time.Time]

func DateTime

func DateTime(name, tz string) *Col[time.Time]

DateTime renders DateTime, or DateTime('TZ') when tz is non-empty.

func DateTime64

func DateTime64(name string, precision int, tz string) *Col[time.Time]

DateTime64 renders DateTime64(precision[, 'TZ']). precision is the sub-second precision (0..9).

func Decimal

func Decimal(name string, precision, scale int) *Col[string]

Decimal(precision, scale) — represented as string to preserve the full arbitrary-precision value.

func FixedString

func FixedString(name string, n int) *Col[string]

FixedString returns a FixedString(n) column. n must be > 0.

func Float32

func Float32(name string) *Col[float32]

func Float64

func Float64(name string) *Col[float64]

func Int8

func Int8(name string) *Col[int8]

func Int16

func Int16(name string) *Col[int16]

func Int32

func Int32(name string) *Col[int32]

func Int64

func Int64(name string) *Col[int64]

func JSON

func JSON(name string) *Col[json.RawMessage]

func String

func String(name string) *Col[string]

func UInt8

func UInt8(name string) *Col[uint8]

func UInt16

func UInt16(name string) *Col[uint16]

func UInt32

func UInt32(name string) *Col[uint32]

func UInt64

func UInt64(name string) *Col[uint64]

func UUID

func UUID(name string) *Col[string]

func (*Col[T]) Between

func (c *Col[T]) Between(lo, hi T) drops.Expression

func (*Col[T]) Codec

func (c *Col[T]) Codec(spec string) *Col[T]

Codec sets the CODEC(...) clause — e.g. Codec("ZSTD(3)"), Codec("Delta, LZ4"). Pass the inner text without parentheses.

func (*Col[T]) Comment

func (c *Col[T]) Comment(text string) *Col[T]

Comment attaches a free-form comment that ends up in the CREATE TABLE column definition.

func (*Col[T]) Default

func (c *Col[T]) Default(sqlExpr string) *Col[T]

Default sets a raw SQL default expression (e.g. "0", "now()", "'pending'", "uuidv4()"). DEFAULT expressions cannot be parameterised.

func (*Col[T]) Eq

func (c *Col[T]) Eq(v T) drops.Expression

func (*Col[T]) EqCol

func (c *Col[T]) EqCol(o *Col[T]) drops.Expression

func (*Col[T]) Expr

func (c *Col[T]) Expr(e drops.Expression) ColumnValue

Expr binds an arbitrary SQL expression to the column in an INSERT.

func (*Col[T]) Gt

func (c *Col[T]) Gt(v T) drops.Expression

func (*Col[T]) GtCol

func (c *Col[T]) GtCol(o *Col[T]) drops.Expression

func (*Col[T]) Gte

func (c *Col[T]) Gte(v T) drops.Expression

func (*Col[T]) GteCol

func (c *Col[T]) GteCol(o *Col[T]) drops.Expression

func (*Col[T]) ILike

func (c *Col[T]) ILike(pattern string) drops.Expression

func (*Col[T]) In

func (c *Col[T]) In(values ...T) drops.Expression

func (*Col[T]) IsNotNull

func (c *Col[T]) IsNotNull() drops.Expression

func (*Col[T]) IsNull

func (c *Col[T]) IsNull() drops.Expression

func (*Col[T]) Like

func (c *Col[T]) Like(pattern string) drops.Expression

func (*Col[T]) LowCardinality

func (c *Col[T]) LowCardinality() *Col[T]

LowCardinality wraps the column type in LowCardinality(...).

func (*Col[T]) Lt

func (c *Col[T]) Lt(v T) drops.Expression

func (*Col[T]) LtCol

func (c *Col[T]) LtCol(o *Col[T]) drops.Expression

func (*Col[T]) Lte

func (c *Col[T]) Lte(v T) drops.Expression

func (*Col[T]) LteCol

func (c *Col[T]) LteCol(o *Col[T]) drops.Expression

func (*Col[T]) Ne

func (c *Col[T]) Ne(v T) drops.Expression

func (*Col[T]) NeCol

func (c *Col[T]) NeCol(o *Col[T]) drops.Expression

func (*Col[T]) NotIn

func (c *Col[T]) NotIn(values ...T) drops.Expression

func (*Col[T]) Nullable

func (c *Col[T]) Nullable() *Col[T]

Nullable wraps the column type in Nullable(...).

func (*Col[T]) TTL

func (c *Col[T]) TTL(expr string) *Col[T]

TTL sets a per-column TTL expression.

func (*Col[T]) Val

func (c *Col[T]) Val(v T) ColumnValue

Val binds a typed value as the column's payload in an INSERT row.

type ColRef

type ColRef interface {
	drops.Expression
	// contains filtered or unexported methods
}

ColRef is implemented by *Column and *Col[T]. Use it where the value type doesn't matter (engine ORDER BY / PARTITION BY, index columns, SELECT projections).

type Column

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

Column is the type-erased AST node for a column reference. Most user code holds a *Col[T] (returned by every type constructor) which embeds *Column and adds type-safe builder + operator methods.

func (*Column) As

func (c *Column) As(alias string) drops.Expression

As / Asc / Desc helpers.

func (*Column) Asc

func (c *Column) Asc() drops.Expression

func (*Column) Codec

func (c *Column) Codec() string

Codec returns the CODEC(...) clause, or empty.

func (*Column) Comment

func (c *Column) Comment() string

Comment returns the column comment, or empty.

func (*Column) DefaultSQL

func (c *Column) DefaultSQL() string

DefaultSQL returns the raw DEFAULT expression.

func (*Column) Desc

func (c *Column) Desc() drops.Expression

func (*Column) HasDefault

func (c *Column) HasDefault() bool

HasDefault reports whether a DEFAULT clause was set.

func (*Column) IsNullable

func (c *Column) IsNullable() bool

IsNullable reports whether the column was wrapped in Nullable().

func (*Column) Name

func (c *Column) Name() string

Name returns the column's unqualified identifier.

func (*Column) TTL

func (c *Column) TTL() string

TTL returns the per-column TTL expression, or empty.

func (*Column) Table

func (c *Column) Table() *Table

Table returns the table this column was registered with, or nil before registration.

func (*Column) Type

func (c *Column) Type() ColumnType

Type returns the column's SQL type.

func (*Column) WriteSQL

func (c *Column) WriteSQL(b *drops.Builder)

WriteSQL writes a qualified reference to the column.

type ColumnType

type ColumnType interface {
	TypeSQL() string
}

ColumnType describes the SQL type literal of a ClickHouse column — the bit that goes right after the column name in DDL.

func TypeArray

func TypeArray(inner ColumnType) ColumnType

TypeArray wraps an inner type as Array(T).

func TypeEnum8

func TypeEnum8(values map[string]int8) ColumnType

TypeEnum8 / TypeEnum16 render the labelled enum types.

TypeEnum8(map[string]int8{"a": 1, "b": 2})

The output order is sorted by value so the generated DDL is stable across runs.

func TypeEnum16

func TypeEnum16(values map[string]int16) ColumnType

func TypeLowCardinality

func TypeLowCardinality(inner ColumnType) ColumnType

TypeLowCardinality wraps an inner type as LowCardinality(T).

func TypeMap

func TypeMap(key, value ColumnType) ColumnType

TypeMap renders Map(K, V).

func TypeNullable

func TypeNullable(inner ColumnType) ColumnType

TypeNullable wraps an inner type as Nullable(T).

func TypeTuple

func TypeTuple(members ...ColumnType) ColumnType

TypeTuple renders Tuple(T1, T2, ...).

type ColumnValue

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

ColumnValue pairs a target column with the value or expression to bind for it in an INSERT row. Construct one with (*Col[T]).Val or (*Col[T]).Expr.

type Cursor added in v0.5.0

type Cursor string

Cursor is an opaque page marker — obtain one from EncodeCursor and pass it to SelectBuilder.AfterCursor / BeforeCursor.

func EncodeCursor added in v0.5.0

func EncodeCursor(spec CursorSpec, values ...any) (Cursor, error)

EncodeCursor builds a cursor from values matching the spec, one per key in declaration order. Returns an error when values is the wrong length or contains an unsupported type.

func (Cursor) Decode added in v0.5.0

func (c Cursor) Decode() ([]any, error)

Decode returns the values held inside the cursor, in spec order. Unknown / corrupt cursors return an error so callers can treat the request as "first page" (or reject the input outright).

type CursorSpec added in v0.5.0

type CursorSpec struct {
	Keys []OrderKey
}

CursorSpec is the ordered list of keys that defines the cursor shape — used by OrderByCursor and AfterCursor so the same shape drives both the ORDER BY and the keyset WHERE.

func NewCursorSpec added in v0.5.0

func NewCursorSpec(keys ...OrderKey) CursorSpec

NewCursorSpec returns a CursorSpec carrying the supplied keys in declaration order. The last key should be a primary key (or otherwise unique column) so equal leading values still resolve to a deterministic page boundary.

type DB

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

DB is the entry point for issuing ClickHouse queries through a drops.Driver. The same Hook / Ping / Close / InTx contract as drops/pg's DB, but every emitted statement uses "?" placeholders.

Safe for concurrent use by multiple goroutines provided the underlying Driver is. Builders returned by Select / Insert are not — create one per query.

func New

func New(drv drops.Driver) *DB

New wraps a drops.Driver as a DB.

func (*DB) Begin

func (db *DB) Begin(ctx context.Context) (*DB, drops.Tx, error)

Begin opens a transaction. Note that ClickHouse only supports transactions in the MergeTree family from recent versions and the guarantees are weaker than PostgreSQL — use sparingly.

func (*DB) Close

func (db *DB) Close() error

Close releases the underlying driver if it implements io.Closer.

func (*DB) Driver

func (db *DB) Driver() drops.Driver

Driver returns the underlying driver.

func (*DB) Exec

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

Exec runs a raw SQL statement (with "?" placeholders).

func (*DB) ExecExpr

func (db *DB) ExecExpr(ctx context.Context, e drops.Expression) (drops.Result, error)

ExecExpr renders e to SQL and runs it. Convenience for DDL helpers.

func (*DB) Hook

func (db *DB) Hook() drops.Hook

Hook returns the currently attached hook, or nil.

func (*DB) InTx

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

InTx runs fn inside a transaction. Rollback uses a detached context with a short timeout so a cancelled caller-ctx doesn't poison cleanup. Hook events fire for begin/commit/rollback.

func (*DB) Insert

func (db *DB) Insert(t *Table) *InsertBuilder

Insert begins an INSERT INTO <t>.

func (*DB) Ping

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

Ping verifies the connection with SELECT 1.

func (*DB) Query

func (db *DB) Query(ctx context.Context, sql string, args ...any) (drops.Rows, error)

Query runs a raw SQL query.

func (*DB) Select

func (db *DB) Select(cols ...drops.Expression) *SelectBuilder

Select begins a SELECT.

Example

ExampleDB_Select shows a quantile + grouped analytics query.

package main

import (
	"fmt"

	"github.com/bernardoforcillo/drops/clickhouse"
)

// Schema fixtures used by the godoc examples — picked from a small
// analytics-style table so the rendered SQL is recognisable.
var (
	exEvents = clickhouse.NewTable("events")

	exEventTS = clickhouse.Add(exEvents, clickhouse.DateTime("ts", "UTC"))

	exEventDur = clickhouse.Add(exEvents, clickhouse.Float64("duration_ms"))
)

func main() {
	db := clickhouse.New(nil)
	sql, _ := db.Select(
		clickhouse.As(clickhouse.ToStartOfDay(exEventTS), "day"),
		clickhouse.As(clickhouse.QuantileTiming(0.95, exEventDur), "p95"),
		clickhouse.As(clickhouse.CountAll(), "hits"),
	).
		From(exEvents).
		GroupBy(clickhouse.ToStartOfDay(exEventTS)).
		OrderBy(exEventTS.Asc()).
		ToSQL()
	fmt.Println(sql)
}
Output:
SELECT toStartOfDay("events"."ts") AS "day", quantileTiming(?)("events"."duration_ms") AS "p95", count() AS "hits" FROM "events" GROUP BY toStartOfDay("events"."ts") ORDER BY "events"."ts" ASC

func (*DB) WithHook

func (db *DB) WithHook(hook drops.Hook) *DB

WithHook returns a shallow copy with hook installed; nil removes.

Example

ExampleDB_WithHook shows attaching the dialect-neutral LoggerHook to a ClickHouse DB.

package main

import (
	"context"
	"fmt"

	"github.com/bernardoforcillo/drops"
	"github.com/bernardoforcillo/drops/clickhouse"
)

// Schema fixtures used by the godoc examples — picked from a small
// analytics-style table so the rendered SQL is recognisable.
var (
	exEvents = clickhouse.NewTable("events")

	exEventTS  = clickhouse.Add(exEvents, clickhouse.DateTime("ts", "UTC"))
	exEventUID = clickhouse.Add(exEvents, clickhouse.UInt64("userId"))
)

func init() {
	exEvents.
		Engine(clickhouse.MergeTree()).
		OrderBy(exEventTS, exEventUID).
		PartitionBy(clickhouse.ToYYYYMM(exEventTS))
}

func main() {
	db := clickhouse.New(exampleNoopDriver{}).WithHook(
		drops.LoggerHook(func(format string, args ...any) {
			fmt.Printf("logged: "+format+"\n", args...)
		}),
	)
	_, _ = db.Exec(context.Background(), "SELECT 1")
	// Sample output omitted — duration is non-deterministic. The hook
	// produces one line per call with kind, status, elapsed and SQL.
}

// exampleNoopDriver lets the godoc examples render through DB without
// pulling in a real ClickHouse driver. Production code never embeds
// a no-op.
type exampleNoopDriver struct{}

func (exampleNoopDriver) Exec(context.Context, string, ...any) (drops.Result, error) {
	return exampleNoopResult{}, nil
}
func (exampleNoopDriver) Query(context.Context, string, ...any) (drops.Rows, error) {
	return nil, fmt.Errorf("not implemented")
}
func (exampleNoopDriver) Begin(context.Context) (drops.Tx, error) {
	return nil, fmt.Errorf("not implemented")
}

type exampleNoopResult struct{}

func (exampleNoopResult) RowsAffected() (int64, error) { return 0, nil }

type Engine

type Engine interface {
	WriteEngine(b *drops.Builder)
}

Engine is the table-engine specification. ClickHouse requires every table to declare one; Engine values render as the text right after the ENGINE = keyword inside CREATE TABLE.

Common engines are provided as constructor helpers below; for anything else (Distributed, Kafka, MaterializedView source engines, AggregatingMergeTree with explicit parameters) use Raw.

func AggregatingMergeTree

func AggregatingMergeTree() Engine

AggregatingMergeTree is the empty-constructor form.

func CollapsingMergeTree

func CollapsingMergeTree(signCol string) Engine

CollapsingMergeTree(sign_column).

func Log

func Log() Engine

func Memory

func Memory() Engine

func MergeTree

func MergeTree() Engine

MergeTree returns the MergeTree() engine. ORDER BY / PARTITION BY / settings are configured on the table itself (Table.OrderBy etc.).

func Null

func Null() Engine

func Raw

func Raw(text string) Engine

Raw builds an engine spec from a literal string. Use for engines the typed constructors don't cover.

clickhouse.Raw("Distributed(cluster, db, table, rand())")

func ReplacingMergeTree

func ReplacingMergeTree(versionCol string) Engine

ReplacingMergeTree(version_column) — version is optional; pass an empty string for the default form ReplacingMergeTree().

func ReplicatedMergeTree

func ReplicatedMergeTree(zkPath, replica string) Engine

ReplicatedMergeTree(zk_path, replica). The path/replica strings are passed verbatim — typically use macros: '/clickhouse/tables/{shard}/foo'.

func StripeLog

func StripeLog() Engine

func SummingMergeTree

func SummingMergeTree(cols ...string) Engine

SummingMergeTree(columns...) — optional list of columns to sum.

func TinyLog

func TinyLog() Engine

func VersionedCollapsingMergeTree

func VersionedCollapsingMergeTree(signCol, versionCol string) Engine

VersionedCollapsingMergeTree(sign_column, version_column).

type Entity added in v0.2.0

type Entity[T any] struct {
	// contains filtered or unexported fields
}

Entity binds a Go struct T to a Table and precomputes the column ↔ field mapping used by the CRUD shortcuts. ClickHouse exposes only Insert and Select builders (mutations are async ALTERs and not first-class), so the Entity surface is intentionally narrow:

  • Create / CreateMany insert one or many rows from the typed struct. There is no RETURNING in ClickHouse, so the caller is responsible for filling every column they care about.
  • Query returns a typed builder for SELECT chains that scan into T directly.

Declare an Entity once at package level:

type Event struct {
    ID     string    `drop:"id"`
    UserID uint64    `drop:"userId"`
    Kind   string    `drop:"kind"`
    Ts     time.Time `drop:"ts"`
}

var (
    Events    = clickhouse.NewTable("events").Engine(clickhouse.MergeTree())
    EventID   = clickhouse.Add(Events, clickhouse.UUID("id"))
    EventEnt  = clickhouse.NewEntity[Event](Events)
)

func NewEntity added in v0.2.0

func NewEntity[T any](t *Table) *Entity[T]

NewEntity validates that T is a struct and builds the column ↔ field index map. It panics on a non-struct type because schema declarations are typically loaded at process startup.

Field matching mirrors the row scanner: `drop:"colname"` tag wins, otherwise field name and snake_case form are tried.

func (*Entity[T]) Create added in v0.2.0

func (e *Entity[T]) Create(db *DB, ctx context.Context, r *T) (drops.Result, error)

Create inserts a single row from r. ClickHouse has no RETURNING, so r is not refreshed — any DEFAULT-driven values (timestamps, UUIDs) stay zero on the Go side.

func (*Entity[T]) CreateMany added in v0.2.0

func (e *Entity[T]) CreateMany(db *DB, ctx context.Context, rs []T) (drops.Result, error)

CreateMany inserts a batch of rows. This is the typical analytics pattern; for very large batches drop down to the native columnar protocol via clickhouse-go's Prepare/Exec loop. Validators run against every row before any SQL is issued — the first failure aborts the whole batch.

func (*Entity[T]) Query added in v0.2.0

func (e *Entity[T]) Query(db *DB) *EntityQuery[T]

Query returns a typed query builder that scans into T.

func (*Entity[T]) Table added in v0.2.0

func (e *Entity[T]) Table() *Table

Table returns the table the entity is bound to.

func (*Entity[T]) Validate added in v0.2.0

func (e *Entity[T]) Validate(v Validator[T]) *Entity[T]

Validate registers a validator that runs before Create / CreateMany. Validators are invoked in registration order; the first to return a non-nil error aborts the operation.

type EntityQuery added in v0.2.0

type EntityQuery[T any] struct {
	// contains filtered or unexported fields
}

EntityQuery is the typed counterpart of SelectBuilder — same shape, but its executors return ([]T, error) and (T, error) directly.

func (*EntityQuery[T]) All added in v0.2.0

func (q *EntityQuery[T]) All(ctx context.Context) ([]T, error)

All executes the query and returns the matching rows as a typed slice.

func (*EntityQuery[T]) Limit added in v0.2.0

func (q *EntityQuery[T]) Limit(n int64) *EntityQuery[T]

Limit sets the LIMIT.

func (*EntityQuery[T]) Offset added in v0.2.0

func (q *EntityQuery[T]) Offset(n int64) *EntityQuery[T]

Offset sets the OFFSET.

func (*EntityQuery[T]) One added in v0.2.0

func (q *EntityQuery[T]) One(ctx context.Context) (T, error)

One executes the query and returns the first matching row. Returns ErrNoRows if the query produces no rows.

func (*EntityQuery[T]) OrderBy added in v0.2.0

func (q *EntityQuery[T]) OrderBy(exprs ...drops.Expression) *EntityQuery[T]

OrderBy appends ORDER BY expressions.

func (*EntityQuery[T]) Prewhere added in v0.2.0

func (q *EntityQuery[T]) Prewhere(preds ...drops.Expression) *EntityQuery[T]

Prewhere appends PREWHERE predicates.

func (*EntityQuery[T]) Unscoped added in v0.2.0

func (q *EntityQuery[T]) Unscoped() *EntityQuery[T]

Unscoped opts out of the table's DefaultFilter predicates.

func (*EntityQuery[T]) Where added in v0.2.0

func (q *EntityQuery[T]) Where(preds ...drops.Expression) *EntityQuery[T]

Where appends predicates joined by AND.

type EnvelopeCipher added in v0.5.0

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

EnvelopeCipher performs AES-256-GCM with per-call data encryption keys wrapped by the configured KMS. Ciphertext format on the wire:

[magic: 1 byte] [wrappedDEK length: uint32 BE] [wrappedDEK] [nonce: 12 bytes] [ciphertext + GCM tag]

The header is fixed-length-prefixed so the decryptor can split without scanning. The magic byte versions the format.

func NewEnvelopeCipher added in v0.5.0

func NewEnvelopeCipher(kms KMS) *EnvelopeCipher

NewEnvelopeCipher wraps kms in the cipher contract. Panics on a nil KMS — encryption with a nil KMS is never a useful programmer intent.

func (*EnvelopeCipher) Decrypt added in v0.5.0

func (c *EnvelopeCipher) Decrypt(ctx context.Context, blob []byte) ([]byte, error)

Decrypt undoes Encrypt — unwraps the DEK via KMS, then opens the ciphertext with AES-GCM. Returns an error when the input has the wrong magic byte, truncated header, or fails the AEAD check.

func (*EnvelopeCipher) Encrypt added in v0.5.0

func (c *EnvelopeCipher) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error)

Encrypt wraps plaintext into a self-contained ciphertext. The returned bytes are safe to store in a String column (as hex) or FixedString / LowCardinality column depending on your schema choice.

type Event added in v0.5.0

type Event struct {
	// Offset is a nanosecond-epoch timestamp that acts as the
	// store-wide ordering key. Use it as the high-watermark when
	// streaming for projections.
	Offset int64

	// AggregateType / AggregateID identify the stream.
	AggregateType string
	AggregateID   string

	// Version is the per-stream offset starting at 0. Append's
	// expectedVersion compares against this.
	Version int64

	// Type is the application-level event name (e.g. "matchStarted").
	Type string

	// Payload is the JSON-encoded event body.
	Payload json.RawMessage

	// Headers carry tracing / correlation metadata.
	Headers map[string]string

	// CreatedAt is the wall-clock time of the append.
	CreatedAt time.Time
}

Event is one entry in the store.

type EventInput added in v0.5.0

type EventInput struct {
	// Type is required — names the event.
	Type string

	// Payload is encoded with encoding/json. Pre-encoded
	// json.RawMessage / []byte / string pass through untouched.
	Payload any

	// Headers attach tracing / correlation metadata stored as JSON.
	Headers map[string]string
}

EventInput is the per-event payload passed to Append.

type EventStore added in v0.5.0

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

EventStore is the append-only event log bound to a single table.

func NewEventStore added in v0.5.0

func NewEventStore(db *DB, table string) *EventStore

NewEventStore returns a store bound to db. The table name is the SQL identifier used for all queries; use NewEventStoreTable to declare matching DDL.

func (*EventStore) Append added in v0.5.0

func (s *EventStore) Append(ctx context.Context, aggregateType, aggregateID string, expectedVersion int64, events ...EventInput) error

Append writes events to the stream identified by (aggregateType, aggregateID) starting at expectedVersion+1. Returns ErrConcurrencyConflict if the stream's head has advanced past expectedVersion.

The check is non-atomic in ClickHouse (no unique-constraint enforcement on MergeTree). For strict single-writer semantics, serialize appends to the same aggregate outside the database.

func (*EventStore) LatestVersion added in v0.5.0

func (s *EventStore) LatestVersion(ctx context.Context, aggregateType, aggregateID string) (int64, error)

LatestVersion returns the highest version recorded for the stream, or -1 when the stream is empty. Use this before Append to compute the expectedVersion for a fresh write.

func (*EventStore) Load added in v0.5.0

func (s *EventStore) Load(ctx context.Context, aggregateType, aggregateID string, fromVersion int64) ([]Event, error)

Load returns events for an aggregate in version order, starting after fromVersion. Pass 0 to read from the beginning.

func (*EventStore) LoadSnapshot added in v0.5.0

func (s *EventStore) LoadSnapshot(ctx context.Context, table, aggregateType, aggregateID string) (AggregateSnapshot, bool, error)

LoadSnapshot fetches the latest snapshot for an aggregate. The query uses FINAL so it reflects the deduplicated (latest-version) row even before CH background merges have run. Returns ok=false when no snapshot exists — fall back to replaying from version 0.

func (*EventStore) SaveSnapshot added in v0.5.0

func (s *EventStore) SaveSnapshot(ctx context.Context, table string, snap AggregateSnapshot) error

SaveSnapshot inserts a new snapshot. With ReplacingMergeTree(version) as the table engine, ClickHouse will eventually deduplicate to the highest-version row per aggregate. Use LoadSnapshot (which reads FINAL) to always get the latest.

func (*EventStore) Stream added in v0.5.0

func (s *EventStore) Stream(ctx context.Context, fromOffset int64, limit int) ([]Event, error)

Stream returns events from the global log starting after fromOffset, up to limit rows. fromOffset is a nanosecond-epoch timestamp — use Event.Offset from the last row of the previous batch. Pass 0 to read from the beginning. Used by projection workers to fan events out into derived tables.

type Factory added in v0.5.0

type Factory[T any] struct {
	// contains filtered or unexported fields
}

Factory builds and inserts test rows for an Entity. The template callback returns a fresh value each call; the supplied seq is a monotonically increasing counter the template can interpolate into fields that need to stay unique across a batch.

var EventFactory = clickhouse.NewFactory(EventEnt, func(seq int) Event {
    return Event{
        ID:     fmt.Sprintf("evt-%d", seq),
        UserID: uint64(seq),
        Kind:   "pageview",
        Ts:     time.Now(),
    }
})

// In a test
e, _  := EventFactory.Create(ctx, db)           // one row, inserted
es, _ := EventFactory.CreateN(ctx, db, 100)     // batch insert
pv := EventFactory.With(func(e *Event) { e.Kind = "purchase" })
p, _ := pv.Create(ctx, db)

Factories are safe to share across goroutines — the sequence counter is atomic. Call Reset between subtests if you need stable identifiers.

func NewFactory added in v0.5.0

func NewFactory[T any](e *Entity[T], template func(seq int) T) *Factory[T]

NewFactory binds template to entity. The template fires once per Build / Create call; the seq it receives is the post-increment counter so the first invocation sees seq=1.

func (*Factory[T]) Build added in v0.5.0

func (f *Factory[T]) Build() T

Build returns a fresh value without touching the database.

func (*Factory[T]) BuildN added in v0.5.0

func (f *Factory[T]) BuildN(n int) []T

BuildN returns n freshly templated values without touching the database. Each value receives a distinct seq.

func (*Factory[T]) Create added in v0.5.0

func (f *Factory[T]) Create(ctx context.Context, db *DB) (T, error)

Create builds one value and inserts it. ClickHouse has no RETURNING so the returned T is the value as built — server-side defaults (e.g. DEFAULT expressions) are not reflected back.

func (*Factory[T]) CreateN added in v0.5.0

func (f *Factory[T]) CreateN(ctx context.Context, db *DB, n int) ([]T, error)

CreateN builds n values and inserts them in a single batch statement via Entity.CreateMany. Returns the templated values; server-side defaults are not read back (matches Entity.CreateMany's semantics).

func (*Factory[T]) Entity added in v0.5.0

func (f *Factory[T]) Entity() *Entity[T]

Entity returns the underlying Entity[T] handle.

func (*Factory[T]) Reset added in v0.5.0

func (f *Factory[T]) Reset()

Reset rewinds the sequence counter to zero. Typically called in test setup between subtests so identifiers stay stable from one run to the next.

func (*Factory[T]) Sequence added in v0.5.0

func (f *Factory[T]) Sequence() int

Sequence returns the current sequence value without advancing. Useful for assertions on factory state in test helpers.

func (*Factory[T]) With added in v0.5.0

func (f *Factory[T]) With(mutate func(*T)) *Factory[T]

With returns a child factory whose Build / Create calls run the parent template, then apply mutate to the result. Use it to spin off variant factories — different event kinds, user tiers, etc. — without redeclaring the whole template.

Child factories share the parent's sequence counter so identifiers remain unique across both.

type InsertBuilder

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

InsertBuilder composes an INSERT INTO …(cols) VALUES (…), (…), … statement. ClickHouse-optimal bulk loads use the native columnar protocol via clickhouse-go's Prepare/Exec loop; for that path drop down to the driver directly. This builder is the convenient form for small batches and one-off rows.

func (*InsertBuilder) Columns

func (i *InsertBuilder) Columns(cols ...ColRef) *InsertBuilder

Columns explicitly fixes the column list (and order) before any Row call. Useful when the first row in your batch omits columns you want present in the SQL.

func (*InsertBuilder) Exec

func (i *InsertBuilder) Exec(ctx context.Context) (drops.Result, error)

Exec runs the INSERT.

func (*InsertBuilder) Row

func (i *InsertBuilder) Row(values ...ColumnValue) *InsertBuilder

Row appends a single row. The first Row fixes the column list.

func (*InsertBuilder) Rows

func (i *InsertBuilder) Rows(rows ...[]ColumnValue) *InsertBuilder

Rows appends multiple rows in one call.

func (*InsertBuilder) ToSQL

func (i *InsertBuilder) ToSQL() (sql string, args []any)

ToSQL renders the statement.

func (*InsertBuilder) WriteSQL

func (i *InsertBuilder) WriteSQL(b *drops.Builder)

WriteSQL renders the INSERT statement.

type InsertHook added in v0.2.0

type InsertHook interface {
	BeforeInsert(ctx *InsertHookCtx)
}

InsertHook is invoked once per INSERT statement, before rendering.

type InsertHookCtx added in v0.2.0

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

InsertHookCtx exposes which columns the caller already bound and lets the hook append additional bindings that apply to every row.

func (*InsertHookCtx) Has added in v0.2.0

func (c *InsertHookCtx) Has(col *Column) bool

Has reports whether col is already bound on the INSERT.

func (*InsertHookCtx) Set added in v0.2.0

func (c *InsertHookCtx) Set(v ColumnValue)

Set binds a typed ColumnValue (e.g. the result of (*Col[T]).Val(v)).

func (*InsertHookCtx) SetExpr added in v0.2.0

func (c *InsertHookCtx) SetExpr(col *Column, expr drops.Expression)

SetExpr binds expr to col across every row, unless col is already bound — typical for DB-evaluated defaults such as drops.Raw("now()").

type InsertHookFunc added in v0.2.0

type InsertHookFunc func(*InsertHookCtx)

InsertHookFunc adapts a plain function to the InsertHook interface.

func (InsertHookFunc) BeforeInsert added in v0.2.0

func (f InsertHookFunc) BeforeInsert(ctx *InsertHookCtx)

BeforeInsert implements InsertHook.

type KMS added in v0.5.0

type KMS interface {
	// Wrap encrypts dek under the master key. drops calls this
	// once per encrypted column write.
	Wrap(ctx context.Context, dek []byte) ([]byte, error)

	// Unwrap decrypts wrappedDEK. drops calls this once per
	// encrypted column read.
	Unwrap(ctx context.Context, wrappedDEK []byte) ([]byte, error)
}

KMS is the key-management contract drops uses for envelope encryption. It wraps and unwraps a data encryption key (DEK) without exposing the master key to drops.

Real implementations live in user code so drops doesn't carry a dependency on any specific KMS SDK. Common adapters are around 30 lines each.

type LocalKMS added in v0.5.0

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

LocalKMS is an in-process KMS backed by a 32-byte master key. Useful for tests and local development — DO NOT use in production where the master key needs to come from an HSM / KMS service.

func NewLocalKMS added in v0.5.0

func NewLocalKMS(key []byte) (*LocalKMS, error)

NewLocalKMS wraps key (must be 32 bytes for AES-256-GCM) as a LocalKMS. The same key must be supplied on every process restart — losing it loses every encrypted row.

func (*LocalKMS) Unwrap added in v0.5.0

func (k *LocalKMS) Unwrap(_ context.Context, wrappedDEK []byte) ([]byte, error)

Unwrap is the inverse of Wrap.

func (*LocalKMS) Wrap added in v0.5.0

func (k *LocalKMS) Wrap(_ context.Context, dek []byte) ([]byte, error)

Wrap encrypts dek under the master key. The output prefixes the nonce so Unwrap can recover it without external metadata.

type LoggerFunc

type LoggerFunc = drops.LoggerFunc

LoggerFunc / LoggerOptions / LoggerHook re-export the dialect-neutral helpers from the root drops package. A single Hook function works against pg.DB, clickhouse.DB and qdrant.Client unchanged.

type LoggerOptions

type LoggerOptions = drops.LoggerOptions

LoggerFunc / LoggerOptions / LoggerHook re-export the dialect-neutral helpers from the root drops package. A single Hook function works against pg.DB, clickhouse.DB and qdrant.Client unchanged.

type MatView added in v0.5.0

type MatView struct {
	// Name is the (unquoted) view name.
	Name string

	// DependsOn lists the upstream relations (base tables and other
	// views) feeding into this view. Used to pick refresh order in
	// RefreshDownstream.
	DependsOn []string

	// Every, when non-zero, schedules a periodic refresh under Start.
	Every time.Duration
}

MatView registers one refreshable materialized view with the manager.

type MatViewManager added in v0.5.0

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

MatViewManager tracks registered views and coordinates refresh across dependents.

func NewMatViewManager added in v0.5.0

func NewMatViewManager(db *DB) *MatViewManager

NewMatViewManager returns an empty manager bound to db.

func (*MatViewManager) Add added in v0.5.0

Add registers v. Returns the manager for chaining. Duplicate names overwrite the prior entry.

func (*MatViewManager) LastRefresh added in v0.5.0

func (m *MatViewManager) LastRefresh(name string) time.Time

LastRefresh reports the last successful refresh time for a view, or zero if it has never been refreshed by this manager.

func (*MatViewManager) Refresh added in v0.5.0

func (m *MatViewManager) Refresh(ctx context.Context, name string) error

Refresh issues SYSTEM REFRESH VIEW on the single named view. Returns an error when the view is not registered or the SQL fails.

func (*MatViewManager) RefreshAll added in v0.5.0

func (m *MatViewManager) RefreshAll(ctx context.Context) error

RefreshAll refreshes every registered view in topological order. Failures abort the run.

func (*MatViewManager) RefreshDownstream added in v0.5.0

func (m *MatViewManager) RefreshDownstream(ctx context.Context, upstream string) error

RefreshDownstream refreshes every view registered with the manager that transitively depends on upstream, in topological order. Used when an upstream base table has been written to and callers want to fan the refresh out to derived views.

func (*MatViewManager) Start added in v0.5.0

func (m *MatViewManager) Start(ctx context.Context) error

Start runs the periodic scheduler honouring each view's Every interval. Blocks until ctx is cancelled.

func (*MatViewManager) Views added in v0.5.0

func (m *MatViewManager) Views() []MatView

Views returns the registered views in name-sorted order.

func (*MatViewManager) WithPollInterval added in v0.5.0

func (m *MatViewManager) WithPollInterval(d time.Duration) *MatViewManager

WithPollInterval overrides how often Start wakes to check for due refreshes. The default (250ms) is fine for typical schedules measured in seconds; increase it for minute-scale schedules to spare the timer.

type Mixin added in v0.2.0

type Mixin interface {
	Apply(*Table)
}

Mixin is the richer companion of the plain template functions in template.go. ClickHouse exposes only InsertHook and SelectBuilder default filters — UPDATE/DELETE happen via async ALTERs and are not modelled by builders — so the rich mixins here are more limited than in drops/pg.

clickhouse.ApplyMixins(Events,
    clickhouse.UUIDPrimaryKeyMixin{},
    clickhouse.TimestampsMixin{},  // adds createdAt, updatedAt
    clickhouse.SoftDeleteMixin{},  // adds deletedAt + default scope
)

type MixinFunc added in v0.2.0

type MixinFunc func(*Table)

MixinFunc adapts a plain function to the Mixin interface.

func (MixinFunc) Apply added in v0.2.0

func (f MixinFunc) Apply(t *Table)

Apply implements Mixin.

type NullsOrdering added in v0.5.0

type NullsOrdering string

NullsOrdering is the NULLS FIRST / NULLS LAST modifier on an ORDER BY clause.

const (
	// NullsDefault inherits ClickHouse's per-direction default.
	NullsDefault NullsOrdering = ""
	// NullsFirst pushes NULL values to the start of the order.
	NullsFirst NullsOrdering = "FIRST"
	// NullsLast pushes NULL values to the end of the order.
	NullsLast NullsOrdering = "LAST"
)

type OrderKey added in v0.5.0

type OrderKey struct {
	// Col is the column referenced by both the ORDER BY clause
	// and the keyset WHERE comparison.
	Col ColRef

	// Desc selects descending order. Defaults to ascending.
	Desc bool

	// Nulls picks the NULLS FIRST / NULLS LAST clause.
	Nulls NullsOrdering
}

OrderKey is one (column, direction) pair making up a cursor shape. Combine multiple OrderKey values in a CursorSpec to form a stable ordering — the typical pattern is (sort_column DESC, primary_key DESC) so rows with identical sort values still produce a unique page boundary.

type PlanStep added in v0.5.0

type PlanStep struct {
	ID          string
	Name        string
	Branch      string
	Description string
	Action      string // "apply" or "rollback"
}

PlanStep is one action in the dry-run output of Plan().

type SelectBuilder

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

SelectBuilder composes a ClickHouse SELECT. It mirrors the drops/pg surface (Where, OrderBy, GroupBy, Limit, joins) plus CH-specific clauses (PREWHERE, FINAL, SAMPLE, SETTINGS).

func (*SelectBuilder) AfterCursor added in v0.5.0

func (s *SelectBuilder) AfterCursor(spec CursorSpec, c Cursor) *SelectBuilder

AfterCursor appends the keyset WHERE clause for forward paging past cursor c. Decoding errors are stored on the builder and returned by Rows / All / One so callers don't have to guard every chained call.

func (*SelectBuilder) All

func (s *SelectBuilder) All(ctx context.Context, dest any) error

All scans every row into dest.

func (*SelectBuilder) AllJoin

func (s *SelectBuilder) AllJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) AnyJoin

func (s *SelectBuilder) AnyJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) AsofJoin

func (s *SelectBuilder) AsofJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) BeforeCursor added in v0.5.0

func (s *SelectBuilder) BeforeCursor(spec CursorSpec, c Cursor) *SelectBuilder

BeforeCursor is the reverse — appends WHERE constraints that page backward past c. Combine with a reversed ORDER BY (or reverse the returned slice on the caller side) to present the page in the expected order.

func (*SelectBuilder) Count

func (s *SelectBuilder) Count(ctx context.Context) (int64, error)

Count wraps the current SELECT in `SELECT count() FROM (... )`.

func (*SelectBuilder) Distinct

func (s *SelectBuilder) Distinct() *SelectBuilder

Distinct toggles SELECT DISTINCT.

func (*SelectBuilder) Final

func (s *SelectBuilder) Final() *SelectBuilder

Final appends FINAL after the table, forcing CH to merge parts at read time (handy with ReplacingMergeTree / CollapsingMergeTree when you accept the cost).

func (*SelectBuilder) From

func (s *SelectBuilder) From(t *Table) *SelectBuilder

From sets the FROM table. Required before execution.

func (*SelectBuilder) FullJoin

func (s *SelectBuilder) FullJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) GroupBy

func (s *SelectBuilder) GroupBy(exprs ...drops.Expression) *SelectBuilder

GroupBy / Having / OrderBy / Limit / Offset.

func (*SelectBuilder) Having

func (s *SelectBuilder) Having(preds ...drops.Expression) *SelectBuilder

func (*SelectBuilder) Join

Join / LeftJoin / RightJoin / FullJoin / AnyJoin / AllJoin / AsofJoin.

func (*SelectBuilder) LeftJoin

func (s *SelectBuilder) LeftJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) Limit

func (s *SelectBuilder) Limit(n int64) *SelectBuilder

func (*SelectBuilder) Offset

func (s *SelectBuilder) Offset(n int64) *SelectBuilder

func (*SelectBuilder) One

func (s *SelectBuilder) One(ctx context.Context, dest any) error

One scans the first row into dest. Returns ErrNoRows if empty.

func (*SelectBuilder) OrderBy

func (s *SelectBuilder) OrderBy(exprs ...drops.Expression) *SelectBuilder

func (*SelectBuilder) OrderByCursor added in v0.5.0

func (s *SelectBuilder) OrderByCursor(spec CursorSpec) *SelectBuilder

OrderByCursor sets ORDER BY according to spec. The same spec must be passed to AfterCursor / BeforeCursor on subsequent pages so the keyset WHERE matches.

func (*SelectBuilder) Prewhere

func (s *SelectBuilder) Prewhere(preds ...drops.Expression) *SelectBuilder

Prewhere adds a PREWHERE predicate — evaluated before the main WHERE, with the right primary-key columns it can dramatically cut scanned data on MergeTree tables.

func (*SelectBuilder) RightJoin

func (s *SelectBuilder) RightJoin(t *Table, on drops.Expression) *SelectBuilder

func (*SelectBuilder) Rows

func (s *SelectBuilder) Rows(ctx context.Context) (drops.Rows, error)

Rows runs the SELECT and returns the raw cursor.

func (*SelectBuilder) SampleBy

func (s *SelectBuilder) SampleBy(e any) *SelectBuilder

SampleBy adds a SAMPLE clause (e.g. SampleBy(0.1) for 10%).

func (*SelectBuilder) Setting

func (s *SelectBuilder) Setting(key, value string) *SelectBuilder

Setting appends a "key = value" pair to the SETTINGS clause.

func (*SelectBuilder) ToSQL

func (s *SelectBuilder) ToSQL() (sql string, args []any)

ToSQL renders the statement using the ClickHouse placeholder style.

func (*SelectBuilder) Unscoped added in v0.2.0

func (s *SelectBuilder) Unscoped() *SelectBuilder

Unscoped opts out of the FROM table's DefaultFilter predicates for this SELECT.

func (*SelectBuilder) Where

func (s *SelectBuilder) Where(preds ...drops.Expression) *SelectBuilder

Where appends predicates joined by AND.

func (*SelectBuilder) With added in v0.5.0

func (s *SelectBuilder) With(ctes ...*CTE) *SelectBuilder

With prepends a WITH clause to the SELECT. Multiple calls accumulate.

func (*SelectBuilder) WriteSQL

func (s *SelectBuilder) WriteSQL(b *drops.Builder)

WriteSQL renders the SELECT.

type SoftDeleteCols added in v0.2.0

type SoftDeleteCols struct {
	DeletedAt *Col[time.Time]
}

SoftDeleteCols holds the typed handle created by SoftDelete.

func SoftDelete added in v0.2.0

func SoftDelete(t *Table) SoftDeleteCols

SoftDelete appends a Nullable(DateTime) "deletedAt" column to t. A row is treated as live while deletedAt IS NULL.

type SoftDeleteMixin added in v0.2.0

type SoftDeleteMixin struct {
	Cols SoftDeleteCols
}

SoftDeleteMixin registers a Nullable(DateTime) "deletedAt" column and a default filter that excludes already-deleted rows from SELECTs. ClickHouse has no UPDATE/DELETE builder, so the "soft delete the row" operation is left to the caller as a raw ALTER TABLE … UPDATE statement.

func (*SoftDeleteMixin) Apply added in v0.2.0

func (m *SoftDeleteMixin) Apply(t *Table)

Apply implements Mixin.

type Table

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

Table represents a ClickHouse table. Beyond columns, it carries the engine spec plus the optional clauses every CREATE TABLE may stipulate: ORDER BY, PARTITION BY, PRIMARY KEY, SAMPLE BY, TTL, and the SETTINGS bag.

func ApplyMixins added in v0.2.0

func ApplyMixins(t *Table, mixins ...Mixin) *Table

ApplyMixins runs each mixin against t in order and returns t.

func NewDatabaseTable

func NewDatabaseTable(database, name string) *Table

NewDatabaseTable scopes the table to an explicit database.

func NewEventStoreTable added in v0.5.0

func NewEventStoreTable(name string) *Table

NewEventStoreTable declares the canonical ClickHouse event-store layout. Run the DDL alongside the rest of your schema.

Engine: MergeTree, ORDER BY (aggregateType, aggregateID, version). There is no auto-increment ID column in ClickHouse; the global ordering cursor is the createdAt DateTime64(9) timestamp.

func NewSnapshotTable added in v0.5.0

func NewSnapshotTable(name string) *Table

NewSnapshotTable declares the snapshot store. The engine is ReplacingMergeTree(version) so ClickHouse automatically merges away older snapshots, keeping only the row with the highest version per (aggregateType, aggregateID). Reads use FINAL to force an immediate merge.

func NewTable

func NewTable(name string) *Table

NewTable creates a table in the default database. The name is validated and a bad identifier panics at startup (see ErrInvalidIdentifier).

func (*Table) Alias

func (t *Table) Alias() string

func (*Table) As

func (t *Table) As(alias string) *Table

As returns a shallow copy bound to alias.

func (*Table) Col

func (t *Table) Col(name string) *Column

Col looks up a column by name.

func (*Table) Columns

func (t *Table) Columns() []*Column

Columns returns the columns in declaration order.

func (*Table) Database

func (t *Table) Database() string

func (*Table) DefaultFilter added in v0.2.0

func (t *Table) DefaultFilter(e drops.Expression) *Table

DefaultFilter appends a predicate applied to every Select against the table, unless the builder is marked Unscoped().

func (*Table) Engine

func (t *Table) Engine(e Engine) *Table

Engine sets the table's engine. Required before CREATE TABLE.

func (*Table) Name

func (t *Table) Name() string

Name / Database / Alias accessors.

func (*Table) OnInsert added in v0.2.0

func (t *Table) OnInsert(h InsertHook) *Table

OnInsert registers a hook invoked by InsertBuilder.WriteSQL.

func (*Table) OrderBy

func (t *Table) OrderBy(cols ...ColRef) *Table

OrderBy sets the ORDER BY columns (MergeTree family).

func (*Table) PartitionBy

func (t *Table) PartitionBy(exprs ...drops.Expression) *Table

PartitionBy sets the PARTITION BY expression(s).

func (*Table) PrimaryKey

func (t *Table) PrimaryKey(cols ...ColRef) *Table

PrimaryKey sets an explicit PRIMARY KEY (defaults to ORDER BY when omitted on MergeTree-family engines).

func (*Table) SampleBy

func (t *Table) SampleBy(e drops.Expression) *Table

SampleBy sets the SAMPLE BY expression.

func (*Table) Setting

func (t *Table) Setting(key, value string) *Table

Setting appends a "key = value" pair to the SETTINGS clause.

func (*Table) TTL

func (t *Table) TTL(expr string) *Table

TTL sets the table-wide TTL expression (raw SQL).

func (*Table) WriteSQL

func (t *Table) WriteSQL(b *drops.Builder)

WriteSQL writes the FROM/JOIN form so a *Table satisfies drops.Expression and can appear anywhere a SQL fragment is expected.

type TimestampsCols added in v0.2.0

type TimestampsCols struct {
	CreatedAt *Col[time.Time]
	UpdatedAt *Col[time.Time]
}

TimestampsCols holds the typed handles created by Timestamps.

func Timestamps added in v0.2.0

func Timestamps(t *Table) TimestampsCols

Timestamps appends "createdAt" and "updatedAt" DateTime columns defaulting to now() to t.

type TimestampsMixin added in v0.2.0

type TimestampsMixin struct {
	Cols TimestampsCols
}

TimestampsMixin registers "createdAt" and "updatedAt" DateTime columns. ClickHouse has no UPDATE builder so there is no UpdateHook equivalent; both columns get DEFAULT now() so INSERT without an explicit value picks up the server clock.

func (*TimestampsMixin) Apply added in v0.2.0

func (m *TimestampsMixin) Apply(t *Table)

Apply implements Mixin.

type TreeMigration added in v0.5.0

type TreeMigration struct {
	ID          string
	Name        string
	Branch      string   // defaults to "main"
	Parents     []string // empty = root; two entries = merge commit
	Description string   // from -- drops:description: header
	Up          func(ctx context.Context, db *DB) error
	Down        func(ctx context.Context, db *DB) error
	// contains filtered or unexported fields
}

TreeMigration is one node in the migration DAG.

type TreeMigrator added in v0.5.0

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

TreeMigrator manages a DAG of migrations tracked in the drops database.

func NewTreeMigrator added in v0.5.0

func NewTreeMigrator(db *DB) *TreeMigrator

NewTreeMigrator returns a migrator bound to db.

func (*TreeMigrator) Add added in v0.5.0

Add registers a migration node. Panics on duplicate ID.

func (*TreeMigrator) AddFS added in v0.5.0

func (m *TreeMigrator) AddFS(fsys fs.FS, dir string) error

AddFS loads migrations from a branch-per-subdirectory layout.

Folder structure:

<dir>/
  main/
    main-001_create_events.up.sql
    main-001_create_events.down.sql
  feat/payments/
    pay-001_payments_table.up.sql   ← -- drops:parents: main-002
  merges/
    merge-001_unified.up.sql        ← -- drops:parents: main-003,pay-002

Optional header: -- drops:description: one-line summary

func (*TreeMigrator) AddSQL added in v0.5.0

func (m *TreeMigrator) AddSQL(id, name, branch string, parents []string, upSQL, downSQL string) *TreeMigrator

AddSQL registers a SQL-only migration.

func (*TreeMigrator) Branches added in v0.5.0

func (m *TreeMigrator) Branches() []string

Branches returns distinct branch labels sorted lexicographically.

func (*TreeMigrator) Checkout added in v0.5.0

func (m *TreeMigrator) Checkout(ctx context.Context, id string) error

Checkout applies or rolls back to land exactly on id's ancestor set.

func (*TreeMigrator) Down added in v0.5.0

func (m *TreeMigrator) Down(ctx context.Context) error

Down rolls back all current head migrations in reverse topological order.

func (*TreeMigrator) DownTo added in v0.5.0

func (m *TreeMigrator) DownTo(ctx context.Context, id string) error

DownTo rolls back all applied descendants of id, stopping before id.

func (*TreeMigrator) Heads added in v0.5.0

func (m *TreeMigrator) Heads(ctx context.Context) ([]TreeStatus, error)

Heads returns the applied leaf nodes — current tips of all live branches.

func (*TreeMigrator) Plan added in v0.5.0

func (m *TreeMigrator) Plan(ctx context.Context) ([]PlanStep, error)

Plan returns the ordered list of migrations that Up() would apply, without touching the schema. Returns ErrTreeMigrationTampered if any applied migration's SQL has changed.

func (*TreeMigrator) Status added in v0.5.0

func (m *TreeMigrator) Status(ctx context.Context) ([]TreeStatus, error)

Status returns the full DAG status in topological order. Tampered is set true for applied migrations whose SQL has changed since apply time.

func (*TreeMigrator) Up added in v0.5.0

func (m *TreeMigrator) Up(ctx context.Context) error

Up applies every pending migration in topological order. No transaction wrapper — CH transaction support is limited. If markApplied fails after a successful Up, re-running is safe only when Up is idempotent (recommended for CH migrations).

func (*TreeMigrator) UpTo added in v0.5.0

func (m *TreeMigrator) UpTo(ctx context.Context, id string) error

UpTo applies the target and all ancestors not yet applied.

func (*TreeMigrator) Validate added in v0.5.0

func (m *TreeMigrator) Validate() error

Validate checks for cycles and missing parents without touching the DB.

func (*TreeMigrator) WithDatabase added in v0.5.0

func (m *TreeMigrator) WithDatabase(database string) *TreeMigrator

WithDatabase overrides the tracking database (default "drops").

type TreeStatus added in v0.5.0

type TreeStatus struct {
	ID          string
	Name        string
	Branch      string
	Parents     []string
	Description string
	Applied     bool
	AppliedAt   time.Time
	IsHead      bool // applied with no applied successors
	IsReady     bool // unapplied, all parents applied
	Checksum    string
	Tampered    bool // stored checksum differs from current SQL
}

TreeStatus is one row produced by Status.

type UUIDPrimaryKeyCols added in v0.2.0

type UUIDPrimaryKeyCols struct {
	ID *Col[string]
}

UUIDPrimaryKeyCols holds the typed handle created by UUIDPrimaryKey.

func UUIDPrimaryKey added in v0.2.0

func UUIDPrimaryKey(t *Table) UUIDPrimaryKeyCols

UUIDPrimaryKey appends an "id" UUID column defaulting to generateUUIDv4(). ClickHouse has no primary-key constraint; pair the column with the table's ORDER BY (or PRIMARY KEY clause on MergeTree-family engines) to make it the row identifier.

type UUIDPrimaryKeyMixin added in v0.2.0

type UUIDPrimaryKeyMixin struct {
	Cols UUIDPrimaryKeyCols
}

UUIDPrimaryKeyMixin registers an "id" UUID column defaulting to generateUUIDv4(). Pair the column with the table's ORDER BY (or PRIMARY KEY clause on MergeTree-family engines) to make it the row identifier.

func (*UUIDPrimaryKeyMixin) Apply added in v0.2.0

func (m *UUIDPrimaryKeyMixin) Apply(t *Table)

Apply implements Mixin.

type Validator added in v0.2.0

type Validator[T any] func(*T) error

Validator is called before Create / CreateMany with a pointer to each candidate row. Returning a non-nil error aborts the operation before any SQL is issued.

type Window added in v0.5.0

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

Window describes the contents of an OVER (...) clause.

func WindowSpec added in v0.5.0

func WindowSpec() *Window

WindowSpec begins building a window specification.

func (*Window) Frame added in v0.5.0

func (w *Window) Frame(spec string) *Window

Frame sets the raw frame specification — e.g. "ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING".

func (*Window) OrderBy added in v0.5.0

func (w *Window) OrderBy(exprs ...drops.Expression) *Window

OrderBy adds ORDER BY entries to the window.

func (*Window) PartitionBy added in v0.5.0

func (w *Window) PartitionBy(exprs ...drops.Expression) *Window

PartitionBy adds PARTITION BY columns.

Jump to

Keyboard shortcuts

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