From 1825c3040f5ee71ad26185a7a08f2657ede94df5 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Thu, 17 Sep 2026 16:51:17 +0200 Subject: rebase onto gg@v1.16.0/oblast --- oblast.go | 218 ++++++++++++++++---------------------------------------------- 1 file changed, 57 insertions(+), 161 deletions(-) (limited to 'oblast.go') diff --git a/oblast.go b/oblast.go index 57d0c63..66b31b7 100644 --- a/oblast.go +++ b/oblast.go @@ -1,185 +1,81 @@ // SPDX-FileCopyrightText: 2026 Stefan Majewsky // SPDX-License-Identifier: Apache-2.0 -// Package oblast is an ORM library for Go, focusing specifically on just the loading and storing of records in the most efficient manner possible. -// No utilities are provided for generating DDL or managing schema migrations, or for building complex OLAP queries. -// -// # Usage pattern -// -// Oblast can load or store any struct type by matching individual fields to column names (on load) or query arguments (on store). -// Struct types that are suitable for this kind of mapping are called "record types" throughout this package documentation. -// -// To use this library, first declare a record type, and create a [Store] for it once to analyze the type and prepare the respective OLTP queries: -// -// type LogEntry struct { -// ID int64 `db:"id,auto"` -// CreatedAt time.Time `db:"created_at"` -// Message string `db:"message"` -// } -// var logEntryStore = oblast.NewStore[LogEntry]( -// oblast.PostgresDialect(), -// oblast.TableNameIs("log_entries"), -// oblast.PrimaryKeyIs("id"), -// ) -// -// Then use it many times to perform load and store operations: -// -// func doStuff(db *gsql.DB) error { -// newEntry := LogEntry{ -// CreatedAt: time.Now(), -// Message: "Hello World.", -// } -// err := logEntryStore.Insert(dbh, &newEntry) -// if err != nil { -// return err -// } -// fmt.Printf("created log entry %d", newEntry.ID) -// -// allEntries, err := logEntryStore.SelectWhere(dbh, `created_at < NOW()`) -// if err != nil { -// return err -// } -// fmt.Printf("there are %d log entries so far", len(allEntries)) -// } -// -// In this example, [*gsql.DB] is a thin wrapper around [*sql.DB], which can be obtained with the [gsql.NewDB] function. -// A [*gsql.DB] can be used in the same way as an [*sql.DB], but if Oblast is only to be used for specific functions, -// then individual [*sql.Conn] or [*sql.Tx] instances can also be wrapped with the [gsql.NewConn] and [gsql.NewTx] functions. -// -// The gsql package serves as an abstraction around different database driver libraries, -// allowing Oblast to also be used with different database drivers such as pgx (see documentation in package gsql for details). -// -// # Mapping rules for record types -// -// If the database column has a different name (or casing, e.g. "id" vs. "ID") than the field name, provide it in the field tag "db". -// The field tag may also contain additional options, separated from the column name by commas. -// To have Oblast ignore a field, either make it private or declare its column name as "-". -// For example: -// -// type Example struct { -// FirstValue string `db:"first_value"` // maps to DB column "first_value" -// SecondValue string // maps to DB column "SecondValue" -// ThirdValue string `db:"third_value,auto"` // maps to DB column "third_value" with "auto" option -// FourthValue string `db:",auto"` // maps to DB column "FourthValue" with "auto" option -// Cache map[string]any `db:"-"` // ignored by Oblast because of column name "-" -// action func() // ignored by Oblast because field is private -// } -// -// The following field options are understood: -// - "auto": During [Store.Insert], do not store this field's value. Instead, the database will auto-generate a value, which will be read back into the record. In SQL dialects that use [sql.Result.LastInsertId] for this (as opposed to a RETURNING clause), only at most one field per record type may have this option, and it must be of an integer type. -// -// It is possible to place mapped fields within sub-structs, including within embedded types. -// This is useful e.g. to avoid code duplication for database columns that are repeated across multiple types: -// -// type Timestamps struct { -// CreatedAt time.Time `db:"created_at"` -// UpdatedAt *time.Time `db:"updated_at"` -// DeletedAt *time.Time `db:"deleted_at"` -// } -// -// type FooRecord struct { -// ID int64 `db:"id,auto"` -// Name string `db:"name"` -// Timestamps Timestamps -// } -// // ... and other struct types that use type Timestamps ... -// -// This behavior may be undesirable on custom struct types that implement [sql.Scanner] and/or [driver.Valuer], or are understood by a [driver.NamedValueChecker] set up by your SQL driver. -// To keep Oblast from recursing into struct types and mapping their fields, provide an explicit `db:"..."` tag on them: -// -// type GeoPoint struct { -// Longitude, Latitude int -// } -// func (p *GeoPoint) Scan(src any) error {...} -// func (p GeoPoint) Value() (driver.Value, error) {...} -// -// type Event struct { -// ID int64 `db:",auto"` -// Description string -// Time time.Time -// // explicit tag ensures that Location.Longitude and Location.Latitude are not mapped individually -// Location GeoPoint `db:"Location"` -// } +// Package oblast has moved to https://pkg.go.dev/go.xyrillian.de/gg/oblast (except for type [RuntimeIndex]). package oblast // import "go.xyrillian.de/oblast" import ( - "database/sql" - "database/sql/driver" - "fmt" - "reflect" + "context" "go.xyrillian.de/gg/gsql" + gg_oblast "go.xyrillian.de/gg/oblast" ) -var ( - // the following types appear in docstring links - _ sql.Scanner = nil - _ driver.NamedValueChecker = nil - _ *gsql.DB = nil -) +// Dialect has moved to gg/oblast (follow the link below). +type Dialect = gg_oblast.Dialect -// PlanOption is an option that can be given to [NewStore] to influence query planning for a certain type of record. -type PlanOption func(*planOpts) +// MariaDBDialect has moved to gg/oblast (follow the link below). +var MariaDBDialect = gg_oblast.MariaDBDialect -// TableNameIs is a PlanOption for record types that correspond to exactly one database table (as opposed to a join of multiple tables). -// This option is required to enable any of the methods of [Store] that use partially or fully auto-generated query strings. -func TableNameIs(name string) PlanOption { - return func(opts *planOpts) { opts.TableName = name } -} +// PostgresDialect has moved to gg/oblast (follow the link below). +var PostgresDialect = gg_oblast.PostgresDialect + +// SqliteDialect has moved to gg/oblast (follow the link below). +var SqliteDialect = gg_oblast.SqliteDialect + +// MissingRecordError has moved to gg/oblast (follow the link below). +type MissingRecordError[R any] = gg_oblast.MissingRecordError[R] + +// PlanOption has moved to gg/oblast (follow the link below). +type PlanOption = gg_oblast.PlanOption + +// TableNameIs has moved to gg/oblast (follow the link below). +var TableNameIs = gg_oblast.TableNameIs + +// PrimaryKeyIs has moved to gg/oblast (follow the link below). +var PrimaryKeyIs = gg_oblast.PrimaryKeyIs + +// StructTagKeyIs has moved to gg/oblast (follow the link below). +var StructTagKeyIs = gg_oblast.StructTagKeyIs -// PrimaryKeyIs is a PlanOption for record types that correspond to a database table with a primary key. -// This option is required to enable use of the [Store.Update] and [Store.Delete] methods. -func PrimaryKeyIs(columnNames ...string) PlanOption { - return func(opts *planOpts) { opts.PrimaryKeyColumnNames = columnNames } +// ReadOnly has moved to gg/oblast (follow the link below). +var ReadOnly = gg_oblast.ReadOnly + +// Store has moved to gg/oblast (follow the link below). +type Store[R any] = gg_oblast.Store[R] + +// NewStore has moved to gg/oblast (follow the link below). +func NewStore[R any](dialect Dialect, opts ...PlanOption) (Store[R], error) { + return gg_oblast.NewStore[R](dialect, opts...) } -// StructTagKeyIs is a PlanOption for record types that allows renaming the struct tag key that Oblast inspects from its default value of "db". -// For example, providing StructTagKeyIs("oblast") means that a struct tag like `db:",auto"` must be written as `oblast:",auto"` instead. -// -// This is useful when migrating from or to another ORM library that uses the same `db:"..."` tag as Oblast, but with conflicting semantics. -func StructTagKeyIs(key string) PlanOption { - return func(opts *planOpts) { opts.StructTagKey = key } +// MustNewStore has moved to gg/oblast (follow the link below). +func MustNewStore[R any](dialect Dialect, opts ...PlanOption) Store[R] { + return gg_oblast.MustNewStore[R](dialect, opts...) } -// ReadOnly is a PlanOption that disables all write operations for the resulting [Store] type -// (i.e., [Store.Insert], [Store.Update], [Store.Upsert] and [Store.Delete]). -// Besides read-only tables (i.e. tables where the current user lacks write permissions), -// this is useful for record types that only model a few columns of a table and which, -// when used in write operations, might result in incomplete records. -func ReadOnly() PlanOption { - return func(opts *planOpts) { opts.ReadOnly = true } +// PreparedSelectQuery has moved to gg/oblast (follow the link below). +type PreparedSelectQuery[R any] = gg_oblast.PreparedSelectQuery[R] + +// Selection has moved to gg/oblast (follow the link below). +type Selection[R any] = gg_oblast.Selection[R] + +// Select has moved to gg/oblast (follow the link below). +func Select[T any](ctx context.Context, db gsql.Handle, query string, args ...any) Selection[T] { + return gg_oblast.Select[T](ctx, db, query, args...) } -// Store holds information on how to read and write data into record type R, -// and can also be used to execute autogenerated queries if the respective [PlanOption] values were provided during [NewStore]. -type Store[R any] struct { - plan plan +// SelectOne has moved to gg/oblast (follow the link below). +func SelectOne[T any](ctx context.Context, db gsql.Handle, query string, args ...any) (T, error) { + return gg_oblast.SelectOne[T](ctx, db, query, args...) } -// NewStore initializes a store for record type R. -// Returns an error if R is not a struct type. -// -// In most situations, the intended usage pattern is to call NewStore (or [MustNewStore]) once per record type, -// and hold the result in a global variable. -// -// When dealing with private one-off record types that are declared within the function or method using them, -// NewStore (or [MustNewStore]) may also be called once per function call. -// NewStore will internally cache its results and return a cheap copy on subsequent calls with the same arguments, -// only incurring the cost of a read lock on a mutex. -func NewStore[R any](dialect Dialect, opts ...PlanOption) (Store[R], error) { - plan, err := getOrBuildPlan(reflect.TypeFor[R](), dialect, collectPlanOptions(opts)) - if err != nil { - var zero R - return Store[R]{}, fmt.Errorf("cannot use type %T for queries: %w", zero, err) - } - return Store[R]{plan}, err +// TupleSelect has moved to gg/oblast (follow the link below). +func TupleSelect[R any](ctx context.Context, db gsql.Handle, query string, args ...any) Selection[R] { + return gg_oblast.TupleSelect[R](ctx, db, query, args...) } -// MustNewStore is like [NewStore], but panics on error. -func MustNewStore[R any](dialect Dialect, opts ...PlanOption) Store[R] { - store, err := NewStore[R](dialect, opts...) - if err != nil { - panic(err.Error()) - } - return store +// TupleSelectOne has moved to gg/oblast (follow the link below). +func TupleSelectOne[R any](ctx context.Context, db gsql.Handle, query string, args ...any) (R, error) { + return gg_oblast.TupleSelectOne[R](ctx, db, query, args...) } -- cgit v1.3.1