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 int, 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 any, src any)
- 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) ScanDest(entityPtr any) []any
- func (m *StructMeta) ScanDestForColumns(entityPtr any, selectedCols []string) []any
- func (m *StructMeta) SetPKValue(entityPtr any, 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 { // Executor is the *sql.DB or *sql.Tx used for all queries. Executor SQLExecutor DefaultsManager 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
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 any, 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) 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) 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) ScanDestForColumns ¶
func (m *StructMeta) ScanDestForColumns(entityPtr any, selectedCols []string) []any
ScanDestForColumns returns scan destinations for selectedCols only; like ScanDest (all columns) when empty.
func (*StructMeta) SetPKValue ¶
func (m *StructMeta) SetPKValue(entityPtr any, 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.