Documentation
¶
Overview ¶
Package enginesql is r3's SQL engine, serving two roles:
Full CRUD via BaseCRUD: a generic r3.CRUD over database/sql with reflection-based struct scanning. Raw SQL drivers (pq, pgx, mysql, sqlite3) embed it and configure only a Flavor.
Shared query preparation via PreparedListQuery: ORM drivers (GORM, Bun, go-pg) execute through their own API but reuse this to translate r3 filters, sorts, and pagination into SQL-ready pieces instead of duplicating it.
Key types: BaseCRUD, BaseRaw (arbitrary-query escape hatch), StructMeta (reflected columns/PK/relations/soft-delete), Flavor, PreparedListQuery.
Public so third-party drivers can reuse it.
Index ¶
- Constants
- Variables
- func ColumnsString(cols []string) string
- func FieldsToColumns(fields r3.Fields) []stringdeprecated
- func FinalizeCount[T any](entities []T, paginatedCount int64, isPaginated bool) ([]T, int64)deprecated
- func ManagedTimestampColumns(meta StructMeta, naming r3.NamingConfig, op r3.WriteOp) []string
- func NewBaseQuerier[T any, ID comparable](db *sql.DB, flavor Flavor, opts ...r3.Option) r3.Querier[T, ID]
- func RequireMutableColumns(ctx context.Context, schema r3.Schema, cols []string) error
- func RunPreloads(ctx context.Context, executor SQLExecutor, meta *StructMeta, flavor Flavor, ...) error
- func ScanAggregateRows(rows *sql.Rows) ([]r3.AggregateRow, error)
- func ScanEntities[T any](rows *sql.Rows, meta *StructMeta) ([]T, error)
- func SetTimeColumns(meta StructMeta, entityPtr any, cols []string, t time.Time)
- func WriteColumns(ctx context.Context, schema r3.Schema, cols []string, op r3.WriteOp) []string
- type BaseCRUD
- func (r *BaseCRUD[T, ID]) Aggregate(ctx context.Context, qarg ...r3.Query) ([]r3.AggregateRow, error)
- func (r *BaseCRUD[T, ID]) BeginTx(ctx context.Context) (r3.TxCRUD[T, ID], error)
- func (r *BaseCRUD[T, ID]) Count(ctx context.Context, qarg ...r3.Query) (int64, error)
- func (r *BaseCRUD[T, ID]) Create(ctx context.Context, entity T) (T, error)
- func (r *BaseCRUD[T, ID]) Delete(ctx context.Context, id ID) error
- func (r *BaseCRUD[T, ID]) Get(ctx context.Context, id ID, qarg ...r3.Query) (T, error)
- func (r *BaseCRUD[T, ID]) HardDelete(ctx context.Context, id ID) error
- func (r *BaseCRUD[T, ID]) List(ctx context.Context, qarg ...r3.Query) ([]T, int64, error)
- func (r *BaseCRUD[T, ID]) Patch(ctx context.Context, entity T, fields r3.Fields) (T, error)
- func (r *BaseCRUD[T, ID]) PatchWhere(ctx context.Context, filters r3.Filters, entity T, fields r3.Fields) (int64, error)
- func (r *BaseCRUD[T, ID]) Restore(ctx context.Context, id ID) error
- func (r *BaseCRUD[T, ID]) SQLDB() *sql.DB
- func (r *BaseCRUD[T, ID]) Update(ctx context.Context, entity T) (T, error)
- func (r *BaseCRUD[T, ID]) Upsert(ctx context.Context, entity T, opts ...r3.UpsertOption) (T, error)
- type BaseRaw
- type Defaultsdeprecated
- func NewDefaults() Defaultsdeprecated
- type DefaultsManagerdeprecated
- type Flavor
- func (f Flavor) ConvertPlaceholders(clause string, startIdx int) (string, int)
- func (f Flavor) DateTruncExpr(col string, unit r3.BucketUnit) (string, error)
- func (f Flavor) Placeholders(count, startIdx int) string
- func (f Flavor) QuoteIdentifiers(sql string) string
- func (f Flavor) SetExprs(cols []string, startIdx int) string
- func (f Flavor) UpsertClause(conflictCols, updateCols []string) string
- func (f Flavor) WhereEq(column string, nextIdx int) string
- type PlaceholderStyle
- type PreparedAggregateQuery
- type PreparedListQuery
- type RelationKind
- type RelationMeta
- type SQLExecutor
- type StructMeta
- func (m *StructMeta) CopyColumnFields(dstPtr, src any)
- func (m *StructMeta) FieldIndicesExcludingColumns(excludedCols []string) ([]string, []int)
- func (m *StructMeta) FieldIndicesForColumns(selectedCols []string) ([]string, []int)
- func (m *StructMeta) FieldValues(entity any) []any
- func (m *StructMeta) FieldValuesForColumns(entity any, columns []string) []any
- func (m *StructMeta) NonPKColumns() []string
- func (m *StructMeta) NonPKFieldValues(entity any) []any
- func (m *StructMeta) PKValue(entity any) any
- func (m *StructMeta) ProjectionColumns(q r3.Query) ([]string, []int)
- func (m *StructMeta) ScanDest(entityPtr any) []any
- func (m *StructMeta) ScanDestForFieldIndices(entityPtr any, fieldIndices []int) []any
- func (m *StructMeta) SelectColumns(q r3.Query) ([]string, error)
- func (m *StructMeta) SetPKValue(entityPtr, val any)
- func (m *StructMeta) ValidatePatchColumns(columns []string) ([]string, error)
Constants ¶
const ( // RelHasMany is a one-to-many relationship (City has many Translations). RelHasMany = r3tag.RelHasMany // RelBelongsTo is a many-to-one relationship (Location belongs to City). RelBelongsTo = r3tag.RelBelongsTo // RelManyToMany is a many-to-many relationship via a join table. RelManyToMany = r3tag.RelManyToMany )
Variables ¶
var ( FlavorPostgres = Flavor{ Placeholder: PlaceholderDollar, SupportsRETURNING: true, TimestampFunc: "NOW()", UsesOnConflictClause: true, BucketExpr: postgresBucketExpr, } FlavorSQLite = Flavor{ Placeholder: PlaceholderQuestion, SupportsRETURNING: true, TimestampFunc: "datetime('now')", UsesOnConflictClause: true, BucketExpr: sqliteBucketExpr, } FlavorMySQL = Flavor{ Placeholder: PlaceholderQuestion, SupportsRETURNING: false, TimestampFunc: "CURRENT_TIMESTAMP", IdentifierQuote: "`", BucketExpr: mysqlBucketExpr, } )
Pre-defined flavors for common databases.
Functions ¶
func ColumnsString ¶
ColumnsString joins column names with commas (SQL-specific formatting).
func FieldsToColumns
deprecated
func FinalizeCount
deprecated
func ManagedTimestampColumns ¶
func ManagedTimestampColumns(meta StructMeta, naming r3.NamingConfig, op r3.WriteOp) []string
ManagedTimestampColumns returns the model's system-managed timestamp columns the engine writes for op: created_at AND updated_at on Create, updated_at on Update/Patch. The system stamps these with server time even though they are read-only to callers - Creatable/Mutable mean "no caller may set this", not "never written". Soft-delete is excluded (managed by Delete/Restore).
func NewBaseQuerier ¶
func NewBaseQuerier[T any, ID comparable](db *sql.DB, flavor Flavor, opts ...r3.Option) r3.Querier[T, ID]
NewBaseQuerier builds a full BaseCRUD but returns only r3.Querier (Get, List), giving a compile-time guarantee that the caller cannot write.
func RequireMutableColumns ¶
RequireMutableColumns enforces the schema's Mutable capability for an explicit Patch column set, returning a typed ErrInvalidPatchField for the first non-mutable column - the capability layer over the structural floor from StructMeta.ValidatePatchColumns. Skipped under the write-guard bypass or a zero schema.
func RunPreloads ¶
func RunPreloads( ctx context.Context, executor SQLExecutor, meta *StructMeta, flavor Flavor, entitiesPtr any, preloads r3.Preloads, ) error
RunPreloads loads the requested relations for entities (a *[]T, e.g. *[]City), setting the relation fields in place. Has-many: WHERE fk IN (parent PKs), grouped by FK into each parent's slice field. Belongs-to: WHERE pk IN (parent FKs), assigned to each parent's pointer field.
func ScanAggregateRows ¶ added in v0.0.3
func ScanAggregateRows(rows *sql.Rows) ([]r3.AggregateRow, error)
ScanAggregateRows scans a dynamic-column aggregate result set into r3.AggregateRow maps keyed by result column name. []byte values normalize to string (MySQL returns text and decimals as []byte); everything else stays backend-native, coerced on read by the AggregateRow accessors. Shared by the raw-SQL engine and the ORM drivers. The caller owns rows (including Close).
func ScanEntities ¶
func ScanEntities[T any](rows *sql.Rows, meta *StructMeta) ([]T, error)
ScanEntities scans sql.Rows into a slice of T using the given StructMeta.
func SetTimeColumns ¶
func SetTimeColumns(meta StructMeta, entityPtr any, cols []string, t time.Time)
SetTimeColumns sets the named columns on the entity to t. Only time.Time and *time.Time fields are written; any other shape is left to the DB default.
func WriteColumns ¶
WriteColumns returns the subset of cols the op may write under the schema. The capability check is skipped (all cols returned) under the write-guard bypass or a zero schema. The structural floor (PK never in cols; computed attributes have no column) is the caller's, so it can't be widened here.
Types ¶
type BaseCRUD ¶
type BaseCRUD[T any, ID comparable] struct { DefaultsManager // Executor is the *sql.DB or *sql.Tx used for all queries. Executor SQLExecutor Meta StructMeta Schema r3.Schema // logical schema for read validation and write-shaping Flavor Flavor Config r3.Config Raw *BaseRaw[T, ID] // contains filtered or unexported fields }
BaseCRUD is a generic r3.CRUD[T, ID] over database/sql, using reflection-based struct scanning and the r3 SQL dialect. Drivers embed it and may override methods (e.g. MySQL overrides Create, lacking RETURNING).
func NewBaseCRUD ¶
func NewBaseCRUD[T any, ID comparable](db *sql.DB, flavor Flavor, opts ...r3.Option) *BaseCRUD[T, ID]
NewBaseCRUD builds a BaseCRUD for db and flavor. Columns map via `db:"col"` tags; the PK is tagged `db:"id,pk"` (defaults to "id"). Options configure framework-level behavior (e.g. r3.WithConfig).
func (*BaseCRUD[T, ID]) Aggregate ¶ added in v0.0.3
func (r *BaseCRUD[T, ID]) Aggregate(ctx context.Context, qarg ...r3.Query) ([]r3.AggregateRow, error)
Aggregate computes grouped aggregates over the matching records. See r3.Aggregator for the semantics.
func (*BaseCRUD[T, ID]) BeginTx ¶
BeginTx starts a transaction and returns an r3.TxCRUD running all ops within it; the caller must Commit or Rollback. Errors if this BaseCRUD is already inside a transaction (holds no *sql.DB).
func (*BaseCRUD[T, ID]) Create ¶
Create inserts a record and returns it with the auto-generated PK populated, via RETURNING (Postgres, SQLite 3.35+). MySQL drivers override this.
func (*BaseCRUD[T, ID]) Delete ¶
Delete removes a record by ID. With a soft-delete column (tagged `r3:"soft_delete"`) it sets that column to the current timestamp; otherwise it hard-deletes.
func (*BaseCRUD[T, ID]) HardDelete ¶
HardDelete permanently removes a record, ignoring soft-delete.
func (*BaseCRUD[T, ID]) Patch ¶
Patch updates only the columns named by fields; entity must have its PK set. Returns the full entity after the update.
func (*BaseCRUD[T, ID]) PatchWhere ¶ added in v0.0.4
func (r *BaseCRUD[T, ID]) PatchWhere( ctx context.Context, filters r3.Filters, entity T, fields r3.Fields, ) (int64, error)
PatchWhere sets fields (from entity's values) on every row matching filters and returns the affected-row count - the multi-row Patch: same column validation and capability gating (ValidatePatchColumns + RequireMutableColumns), managed updated_at bump, and write-guard bypass, but rows are selected by filters.
Filters are schema-validated (an unknown or non-filterable field is a typed error, as in List). Soft-deleted rows are excluded by default. Relationship ("has") filters are not supported and return an error.
func (*BaseCRUD[T, ID]) Restore ¶
Restore clears the soft-delete column, un-deleting a record. Errors if the model has no soft-delete column.
func (*BaseCRUD[T, ID]) SQLDB ¶
SQLDB returns the underlying *sql.DB for advanced use. Panics inside a transaction (use the transaction's CRUD instead).
func (*BaseCRUD[T, ID]) Update ¶
Update modifies the record identified by its primary key and returns the row as persisted.
Only Mutable columns are written, so a full Update cannot clobber created_at or resurrect a soft-deleted row; a model with no mutable columns makes it a verified no-op. The returned entity carries the stored value of every column the write skipped, so a caller-built partial model does not report those as zeroed. Bypass the write guard for audited system/worker writes (see r3.WithoutWriteGuard).
func (*BaseCRUD[T, ID]) Upsert ¶ added in v0.0.4
Upsert inserts entity, or updates the colliding row on a conflict against the conflict target (the PK by default), via Flavor.UpsertClause (Postgres/SQLite ON CONFLICT ... DO UPDATE, MySQL ON DUPLICATE KEY UPDATE).
The insert branch obeys Creatable and stamps created_at/updated_at; the update branch obeys Mutable (a restricted UpdateOnConflict set is rejected like Patch) and bumps updated_at. The write-guard bypass skips the capability check as in Create/Patch. The stored row comes back via RETURNING where supported, else a re-fetch by the conflict target.
type BaseRaw ¶
type BaseRaw[T any, ID any] struct { Executor SQLExecutor // *sql.DB or *sql.Tx Meta StructMeta }
BaseRaw executes arbitrary SQL with automatic StructMeta-based struct scanning.
func NewBaseRaw ¶
func NewBaseRaw[T any, ID comparable](executor SQLExecutor, meta StructMeta) *BaseRaw[T, ID]
NewBaseRaw creates a BaseRaw.
type Defaults
deprecated
Defaults is an alias for r3.Defaults.
Deprecated: Use r3.Defaults directly.
func NewDefaults
deprecated
func NewDefaults() Defaults
NewDefaults returns Defaults with reasonable default queries.
Deprecated: Use r3.NewDefaults directly.
type DefaultsManager
deprecated
type DefaultsManager = r3.DefaultsManager
DefaultsManager is an alias for r3.DefaultsManager.
Deprecated: Use r3.DefaultsManager directly.
func NewDefaultsManager
deprecated
func NewDefaultsManager() DefaultsManager
NewDefaultsManager creates a DefaultsManager with reasonable defaults.
Deprecated: Use r3.NewDefaultsManager directly.
type Flavor ¶
type Flavor struct {
// Placeholder controls the parameter placeholder style (? vs $N).
Placeholder PlaceholderStyle
// SupportsRETURNING reports whether INSERT supports RETURNING (Postgres,
// SQLite 3.35+). When false, Create() falls back to INSERT + last-insert-id +
// SELECT.
SupportsRETURNING bool
// TimestampFunc is the current-timestamp SQL expression used by soft-delete to
// set deleted_at: "NOW()" (Postgres), "CURRENT_TIMESTAMP" (MySQL),
// "datetime('now')" (SQLite).
TimestampFunc string
// IdentifierQuote is this backend's identifier quote char; the engine rewrites
// the dialect's ANSI double quotes to it (see QuoteIdentifiers). Empty = ANSI
// double quotes (Postgres, SQLite). MySQL needs backticks: without ANSI_QUOTES
// mode it reads `"col"` as a string literal, silently breaking every filter and
// sort.
IdentifierQuote string
// UsesOnConflictClause selects the upsert dialect. True = Postgres/SQLite
// `ON CONFLICT (cols) DO UPDATE SET ...` (explicit target; EXCLUDED is the
// would-be-inserted row). False = MySQL `ON DUPLICATE KEY UPDATE col =
// VALUES(col)` (target is any unique key, so cols are ignored).
UsesOnConflictClause bool
// BucketExpr renders a flavor-specific truncation of col (an already
// ANSI-quoted time column reference) to a calendar unit, for a time-bucket
// GROUP BY key. nil means the flavor has no bucket support, so a bucket query
// errors loudly (see [Flavor.DateTruncExpr]) instead of silently returning
// un-bucketed rows. Set on the predefined flavors; ISO-Monday weeks.
BucketExpr func(col string, unit r3.BucketUnit) (string, error)
}
Flavor describes the SQL dialect differences between backends, passed to NewBaseCRUD to configure SQL generation.
func (Flavor) ConvertPlaceholders ¶
ConvertPlaceholders rewrites `?` placeholders in clause to `$1, $2, ...` from startIdx, returning the result and the next free index. No-op (but still advances the index) for `?`-style flavors.
func (Flavor) DateTruncExpr ¶ added in v0.1.7
DateTruncExpr renders the flavor's truncation of col to unit, or a loud r3.ErrBucketNotSupported when the flavor has no bucket support wired.
func (Flavor) Placeholders ¶
Placeholders builds a placeholder list: "$1, $2, $3" from startIdx (dollar) or "?, ?, ?" (question).
func (Flavor) QuoteIdentifiers ¶
QuoteIdentifiers rewrites the dialect's ANSI double-quoted identifiers (`"col"`, `"tbl"."col"`) to this flavor's quote char; a no-op for ANSI flavors (Postgres, SQLite).
Safe on a fully assembled query: values are always bound as parameters, never inlined, so the only double quotes in the SQL are identifier quotes. A doubled embedded quote (`""`, the dialect's escape) maps to the doubled target quote, which is also correct - so escaping survives. (Identifiers are validated to contain no quotes, so this never actually occurs.)
func (Flavor) SetExprs ¶
SetExprs builds an UPDATE SET body: "col1 = $1, col2 = $2, ..." or "col1 = ?, col2 = ?, ...".
func (Flavor) UpsertClause ¶ added in v0.0.4
UpsertClause builds an upsert INSERT's trailing on-conflict clause: `ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col, ...` (Postgres/SQLite) or `ON DUPLICATE KEY UPDATE col = VALUES(col), ...` (MySQL). conflictCols is the target (MySQL ignores it, keying off any unique index). Empty updateCols yields DO NOTHING (a self-assign no-op on MySQL), leaving the existing row untouched.
type PlaceholderStyle ¶
type PlaceholderStyle int
PlaceholderStyle controls how SQL parameter placeholders are generated.
const ( // PlaceholderQuestion uses `?` placeholders (MySQL, SQLite). PlaceholderQuestion PlaceholderStyle = iota // PlaceholderDollar uses `$1, $2, ...` numbered placeholders (PostgreSQL). PlaceholderDollar )
type PreparedAggregateQuery ¶ added in v0.0.3
type PreparedAggregateQuery struct {
// PreparedListQuery carries WHERE/joins, pagination, and the merged Query. Its
// Sorts are already restricted to group fields and aggregate aliases (see
// r3.Query.AggregateSorts); cursor pagination doesn't apply to grouped rows.
PreparedListQuery
// SelectList is the full select list in declaration order: quoted group columns
// first (each aliased to its raw field name, so result keys are stable across
// backends), then aggregate expressions with their aliases.
SelectList []string
// GroupBy is the group-by expression list (empty = whole-set row): quoted
// plain columns followed by rendered time-bucket expressions.
GroupBy []string
// BucketExprs holds just the rendered time-bucket group expressions, aligned
// with Query.Buckets. It is a subset of GroupBy, surfaced separately so an ORM
// driver (which groups by raw field names to let the ORM quote them) can add
// the bucket expressions without re-deriving them.
BucketExprs []string
// Having is the HAVING clause with args (zero when no Having filters). Aliases
// are already lowered to their aggregate expressions - Postgres disallows
// select-list aliases in HAVING.
Having r3sql.SQLClause
}
PreparedAggregateQuery is the aggregate sibling of PreparedListQuery, consumed by any driver (database/sql, GORM, Bun, go-pg). WHERE comes from the embedded PreparedListQuery; the aggregate-specific parts are the select list, GROUP BY, HAVING, and alias-aware ORDER BY.
func PrepareAggregateQuery ¶ added in v0.0.3
func PrepareAggregateQuery( dm *DefaultsManager, schema r3.Schema, flavor Flavor, qarg ...r3.Query, ) (PreparedAggregateQuery, error)
PrepareAggregateQuery merges defaults with args, validates against the schema (a zero r3.Schema does structural-only validation), and converts to SQL-ready aggregate components.
func PrepareMergedAggregateQuery ¶ added in v0.0.3
func PrepareMergedAggregateQuery(schema r3.Schema, q r3.Query, flavor Flavor) (PreparedAggregateQuery, error)
PrepareMergedAggregateQuery builds the SQL aggregate components for an already-merged query, exposed separately (like PrepareMergedListQuery) so a driver can transform it first (e.g. lower relationship filters). flavor renders time-bucket group keys; a flavor without bucket support returns r3.ErrBucketNotSupported only when the query actually declares buckets.
type PreparedListQuery ¶
type PreparedListQuery struct {
// Clauses is the SQL WHERE clauses with their args and joins.
Clauses r3sql.SQLClauses
// Sorts is the SQL ORDER BY expressions.
Sorts []r3sql.SQLSort
// IsPaginated reports offset-based pagination; Limit/Offset are then set.
IsPaginated bool
Limit int
Offset int
// IsCursorPaginated reports cursor/keyset pagination. When true, use
// CursorClause/CursorLimit and no OFFSET or COUNT query.
IsCursorPaginated bool
// CursorClause is the keyset WHERE clause (empty for the first page).
CursorClause r3sql.SQLClause
// CursorLimit is the row limit for cursor pagination.
CursorLimit int
// CursorBackward marks a "before" cursor. Backward keyset pagination scans in
// reversed sort order (so LIMIT takes the rows immediately preceding the
// cursor), then the caller reverses the slice back. See OrderBySorts.
CursorBackward bool
// Query is the merged r3.Query (defaults + args), for Preloads, IncludeTrashed,
// and other driver-specific fields.
Query r3.Query
}
PreparedListQuery holds the SQL components derived from an r3.Query - filters, sorts, and pagination converted into SQL-ready pieces that any driver (GORM, Bun, go-pg, database/sql) can consume, so the r3-to-SQL translation lives once.
func PrepareListQuery ¶
func PrepareListQuery(dm *DefaultsManager, qarg ...r3.Query) (PreparedListQuery, error)
PrepareListQuery merges defaults with args, then converts filters, sorts, and pagination into SQL components. Drivers call it once at the start of List().
func PrepareMergedListQuery ¶
func PrepareMergedListQuery(q r3.Query) (PreparedListQuery, error)
PrepareMergedListQuery builds the SQL components for an already-merged query - the half of PrepareListQuery after the merge, exposed so a driver can transform the query first (e.g. lower relationship "has" filters into key-set In filters).
It applies no value codecs; a driver whose schema declares codecs must call PrepareMergedListQuerySchema to encode codec'd filter/cursor args to stored form.
func PrepareMergedListQuerySchema ¶ added in v0.0.7
PrepareMergedListQuerySchema is PrepareMergedListQuery with schema-driven value codecs applied: filter args and decoded cursor keys on codec'd attributes are encoded to stored form so predicates compare against stored column values. A zero schema (no codecs) behaves exactly like PrepareMergedListQuery.
func (*PreparedListQuery) Joins ¶
func (p *PreparedListQuery) Joins() []r3sql.SQLColumn
Joins returns the deduplicated list of SQL joins from the clauses.
func (*PreparedListQuery) OrderBySorts ¶
func (p *PreparedListQuery) OrderBySorts() ([]r3sql.SQLSort, error)
OrderBySorts returns the main SELECT's ORDER BY. Forward queries use Sorts; a backward ("before") cursor reverses each direction, so the rows preceding the cursor are scanned under LIMIT and reversed back by the caller (see CursorBackward).
type RelationKind ¶
type RelationKind = r3tag.RelationKind
RelationKind is an alias of r3tag.RelationKind.
type RelationMeta ¶
type RelationMeta struct {
FieldName string // Go field name (matched against PreloadSpec.Name)
FieldIndex int // struct field index for reflection-based assignment
Kind RelationKind // has-many, belongs-to, or many-to-many
FKColumn string // FK column (on the "many" side, or left FK for M2M)
RefColumn string // right FK column in join table (M2M only)
JoinTable string // join table name (M2M only)
// OrderColumn is an integer join-table column persisting the slice order
// (M2M only): sync writes each element's index, preload orders by it.
// Empty means order is not persisted.
OrderColumn string
// WhereColumn and WhereValue scope an M2M relation to one slice of its join
// table (`where:<column>=<value>`, or [r3.RelationWhere]): every join-table
// read filters on the pair and every write supplies it, so two relations can
// share a join table. Empty WhereColumn spans the whole join table.
WhereColumn string
WhereValue string
Owned bool // children are lifecycle-bound to parent (has-many only)
TargetMeta StructMeta // metadata for the related entity type
TargetType reflect.Type // target element type (not slice/ptr)
}
RelationMeta describes a struct field that represents a relation.
func RelationMetaFromSpec ¶ added in v0.0.5
func RelationMetaFromSpec(spec r3.RelationSpec) RelationMeta
RelationMetaFromSpec converts a core r3.RelationSpec (a relation declared by table and column names) into a RelationMeta. The target Go type is unknown - the point of an explicit spec - so TargetType is nil and FieldIndex is -1: the relation is filterable and aggregatable but has no struct field to preload into.
type SQLExecutor ¶
type SQLExecutor interface {
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
SQLExecutor is the common interface of *sql.DB and *sql.Tx, letting BaseCRUD run transparently in normal and transactional modes.
type StructMeta ¶
type StructMeta struct {
TableName string // e.g. "cities"
Columns []string // column names in order
Fields []int // struct field index per column
PKColumn string // primary key column (defaults to "id")
PKField int // index into Columns/Fields for the PK entry
// SoftDeleteColumn is the soft-delete column (e.g. "deleted_at"), detected via
// `r3:"soft_delete"`. Empty means soft-delete is off.
SoftDeleteColumn string
// Relations holds relations detected via `r3` tags, e.g.
// `r3:"rel:has-many,fk:city_id"`.
Relations []RelationMeta
}
StructMeta is the reflected metadata for a struct type T, used by BaseCRUD and BaseRaw to build queries and scan results.
func GetStructMeta ¶
func GetStructMeta[T any]() StructMeta
GetStructMeta derives table name and columns from T.
Tag priority: `r3` first, `db` as fallback; a "-" value in either is ignored. Pointer-to-basic types are nullable columns; slices, maps, and struct fields (except time.Time) are relation fields.
func WithDeclaredRelations ¶ added in v0.0.5
func WithDeclaredRelations(meta StructMeta, specs []r3.RelationSpec) StructMeta
WithDeclaredRelations returns a copy of meta with specs added to its Relations. A declared relation whose name matches a tag-reflected one replaces it (explicit spec wins). The input meta is never mutated (shared and cached); a nil/empty specs returns meta unchanged.
func (*StructMeta) CopyColumnFields ¶ added in v0.2.0
func (m *StructMeta) CopyColumnFields(dstPtr, src any)
CopyColumnFields overwrites dstPtr's column-backed fields with src's, leaving every other field (relations, association pointers, `-`-tagged fields) untouched.
Drivers use it to fold a freshly re-read row into the entity they are about to return: the columns then carry DB truth (values the write omitted, DB-stamped timestamps, defaults, triggers) while associations stay as the caller supplied them. A re-read does not load associations, and a nil relation means "not loaded" throughout r3 - so copying the row wholesale would silently drop them.
func (*StructMeta) FieldIndicesExcludingColumns ¶ added in v0.4.0
func (m *StructMeta) FieldIndicesExcludingColumns(excludedCols []string) ([]string, []int)
FieldIndicesExcludingColumns is the subtractive counterpart of StructMeta.FieldIndicesForColumns: every column EXCEPT those named, in declaration order. The PK is always kept, exactly as the additive form always adds it. An empty exclusion list selects everything.
func (*StructMeta) FieldIndicesForColumns ¶
func (m *StructMeta) FieldIndicesForColumns(selectedCols []string) ([]string, []int)
FieldIndicesForColumns returns the Columns/Fields subset (columns and their field indices, in order) matching selectedCols; all of them when empty.
func (*StructMeta) FieldValues ¶
func (m *StructMeta) FieldValues(entity any) []any
FieldValues extracts the column values from a struct in the same order as Columns.
func (*StructMeta) FieldValuesForColumns ¶
func (m *StructMeta) FieldValuesForColumns(entity any, columns []string) []any
FieldValuesForColumns extracts an entity's values for the given columns, in order. Columns absent from the struct are silently skipped (validate first).
func (*StructMeta) NonPKColumns ¶
func (m *StructMeta) NonPKColumns() []string
NonPKColumns returns all column names except the primary key.
func (*StructMeta) NonPKFieldValues ¶
func (m *StructMeta) NonPKFieldValues(entity any) []any
NonPKFieldValues extracts column values excluding the PK, for INSERT/UPDATE.
func (*StructMeta) PKValue ¶
func (m *StructMeta) PKValue(entity any) any
PKValue extracts the primary key value from an entity.
func (*StructMeta) ProjectionColumns ¶ added in v0.4.0
func (m *StructMeta) ProjectionColumns(q r3.Query) ([]string, []int)
ProjectionColumns resolves a query's projection into the columns to SELECT and their struct field indices: the additive r3.Query.Fields list, the subtractive r3.Query.ExcludeFields list, or every column when neither is set.
It resolves rather than validates: the raw SQL engine runs r3.Schema.ValidateQuery first, which rejects both forms at once and an excluded name matching no attribute. A driver without that step wants StructMeta.SelectColumns, which validates as it resolves.
func (*StructMeta) ScanDest ¶
func (m *StructMeta) ScanDest(entityPtr any) []any
ScanDest returns a slice of pointers to the struct fields, suitable for sql.Row.Scan(). entityPtr must be a pointer to the entity.
func (*StructMeta) ScanDestForFieldIndices ¶ added in v0.4.0
func (m *StructMeta) ScanDestForFieldIndices(entityPtr any, fieldIndices []int) []any
ScanDestForFieldIndices returns scan destinations for the given struct field indices, in order - the half of StructMeta.ProjectionColumns after the column lookup, so a caller that already resolved a projection does not resolve it twice.
func (*StructMeta) SelectColumns ¶ added in v0.4.0
func (m *StructMeta) SelectColumns(q r3.Query) ([]string, error)
SelectColumns resolves a query's projection into an explicit column list for a driver that builds its reads through an ORM's Select/Column call rather than through StructMeta.ProjectionColumns. It returns nil when the query requests no projection, so the driver leaves the ORM's own column choice alone.
The additive form passes the caller's names through to the ORM, which resolves them, plus the primary key. The subtractive form is resolved here against the model's columns, and an excluded name matching no column is an error: it would drop nothing and return the full row, the one mistake the exclusion form exists to prevent.
func (*StructMeta) SetPKValue ¶
func (m *StructMeta) SetPKValue(entityPtr, val any)
SetPKValue sets the primary key value on an entity (via pointer).
func (*StructMeta) ValidatePatchColumns ¶
func (m *StructMeta) ValidatePatchColumns(columns []string) ([]string, error)
ValidatePatchColumns is the structural floor for Patch: it rejects an empty set, or any unknown, primary-key, or soft-delete column. On success it returns columns unchanged.