Documentation
¶
Overview ¶
Package enginesql provides the SQL engine for r3.
It serves two roles:
Full CRUD via BaseCRUD: a generic r3.CRUD implementation using database/sql and reflection-based struct scanning. Raw SQL drivers (pq, pgx, mysql, sqlite3) embed BaseCRUD and only configure it via Flavor (placeholder style, RETURNING support, timestamp function).
Shared query preparation via PreparedListQuery: ORM-based drivers (GORM, Bun, go-pg) do NOT embed BaseCRUD — they use their own ORM API for query execution. However, they share PreparedListQuery to convert r3 filters, sorts, and pagination into SQL-ready components, avoiding duplicated translation logic.
Key types:
- BaseCRUD: full r3.CRUD backed by database/sql
- BaseRaw: escape hatch for arbitrary SQL queries
- StructMeta: reflection-based struct metadata (columns, PK, relations, soft-delete)
- Flavor: database-specific SQL variations (placeholders, RETURNING, timestamps)
- PreparedListQuery: pre-computed SQL clauses from an r3.Query, shared by all SQL drivers
This package is public so that 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) 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) 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 represents a one-to-many relationship (e.g. City has many Translations). RelHasMany = r3tag.RelHasMany // RelBelongsTo represents a many-to-one relationship (e.g. Location belongs to City). RelBelongsTo = r3tag.RelBelongsTo // RelManyToMany represents a many-to-many relationship via a join table. RelManyToMany = r3tag.RelManyToMany )
Variables ¶
var ( FlavorPostgres = Flavor{ Placeholder: PlaceholderDollar, SupportsRETURNING: true, TimestampFunc: "NOW()", UsesOnConflictClause: true, } FlavorSQLite = Flavor{ Placeholder: PlaceholderQuestion, SupportsRETURNING: true, TimestampFunc: "datetime('now')", UsesOnConflictClause: true, } FlavorMySQL = Flavor{ Placeholder: PlaceholderQuestion, SupportsRETURNING: false, TimestampFunc: "CURRENT_TIMESTAMP", IdentifierQuote: "`", } )
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 system-managed timestamp columns present on the model that the engine writes for the given op: created_at AND updated_at on Create, updated_at on Update/Patch. These are written by the system with server time even though they are read-only to API callers — the Creatable/Mutable caps mean "no caller may set this", not "never written". The soft-delete column is excluded here: it is managed by Delete/Restore, not by Create/Update.
func NewBaseQuerier ¶
func NewBaseQuerier[T any, ID comparable](db *sql.DB, flavor Flavor, opts ...r3.Option) r3.Querier[T, ID]
NewBaseQuerier creates a read-only repository backed by database/sql. It returns r3.Querier, providing a compile-time guarantee that the caller cannot perform write operations.
Internally this constructs a full BaseCRUD but only exposes the Querier interface (Get, List).
func RequireMutableColumns ¶
RequireMutableColumns enforces the schema's Mutable capability for an explicit Patch column set, returning a typed ErrInvalidPatchField for the first column that may not be mutated. It is the capability layer on top of the structural floor already checked by StructMeta.ValidatePatchColumns (unknown column, PK, soft-delete). The check is 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 executes preload queries for the given entities based on the requested preload names and the relation metadata in StructMeta.
For has-many relations: collects parent PKs, queries child table WHERE fk IN (...), groups results by FK, and assigns them to the parent's slice field.
For belongs-to relations: collects FK values from parents, queries target table WHERE pk IN (...), and assigns results to the parent's pointer field.
entities must be a pointer to a slice (e.g. *[]City). The function modifies entities in place by setting their relation fields.
func ScanAggregateRows ¶ added in v0.0.3
func ScanAggregateRows(rows *sql.Rows) ([]r3.AggregateRow, error)
ScanAggregateRows scans a dynamic-column result set (as produced by an aggregate SELECT) into r3.AggregateRow maps keyed by the result column names. []byte values are normalized to string (MySQL returns text and decimals as []byte); everything else stays backend-native — the AggregateRow accessors coerce on read. 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 database default.
func WriteColumns ¶
WriteColumns returns the subset of cols that the given write op may write under the schema. The capability check is skipped — all cols are returned — when the write guard is bypassed (audited system/worker write) or no schema is present (back-compat for schema-less callers). The structural floor is enforced separately by the caller (the PK is never in cols; computed attributes have no column at all), so it can never be widened here.
Types ¶
type BaseCRUD ¶
type BaseCRUD[T any, ID comparable] struct { // Executor is the SQL executor used for all queries. It is either a *sql.DB // or a *sql.Tx, both of which implement [SQLExecutor]. 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 CRUD repository backed by database/sql. It implements the full r3.CRUD[T, ID] interface using reflection-based struct scanning and the r3 SQL dialect for filters, sorts, and pagination.
Database-specific drivers embed this type and may override individual methods (e.g. MySQL overrides Create because it lacks RETURNING support).
func NewBaseCRUD ¶
func NewBaseCRUD[T any, ID comparable](db *sql.DB, flavor Flavor, opts ...r3.Option) *BaseCRUD[T, ID]
NewBaseCRUD creates a new BaseCRUD with the given database connection and flavor. Models should use `db:"column_name"` struct tags for column mapping. The primary key field should be tagged with `db:"id,pk"` (defaults to "id").
Accepts optional r3.Option values for framework-level configuration (e.g. r3.WithConfig for custom naming conventions or default page size).
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 records matching the query. See r3.Aggregator for the query semantics.
func (*BaseCRUD[T, ID]) BeginTx ¶
BeginTx starts a database transaction and returns an r3.TxCRUD that executes all CRUD operations within that transaction. The caller must call Commit or Rollback on the returned TxCRUD.
This method requires the BaseCRUD to hold a *sql.DB (i.e., not already inside a transaction). Returns an error if called on a transactional BaseCRUD.
func (*BaseCRUD[T, ID]) Create ¶
Create inserts a new record and returns it with the auto-generated PK populated. This implementation uses RETURNING and works for PostgreSQL and SQLite 3.35+. MySQL drivers should override this method.
func (*BaseCRUD[T, ID]) Delete ¶
Delete removes a record by its ID. If the model has a soft-delete column (tagged with `r3:"soft_delete"`), it performs a soft-delete by setting the column to the current timestamp. Otherwise, it performs a hard delete.
func (*BaseCRUD[T, ID]) HardDelete ¶
HardDelete permanently removes a record, ignoring soft-delete.
func (*BaseCRUD[T, ID]) Patch ¶
Patch performs a partial update, modifying only the columns specified by fields. The entity must have its primary key 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 the given fields (to entity's values) on every row matching filters and returns the affected-row count. It is the multi-row analogue of Patch: the column set is validated and capability-gated identically (ValidatePatchColumns + RequireMutableColumns), managed updated_at is bumped, and the write guard bypass is honored — but rows are selected by filters instead of by primary key.
Filters are validated against the schema (an unknown or non-filterable field is a typed error, as in List). Soft-deleted rows are excluded by default, so a bulk update never touches trashed rows. Relationship ("has") filters are not supported here and return an error.
func (*BaseCRUD[T, ID]) Restore ¶
Restore clears the soft-delete column, un-deleting a soft-deleted record. Returns an error if the model has no soft-delete column.
func (*BaseCRUD[T, ID]) SQLDB ¶
SQLDB returns the underlying *sql.DB for advanced usage (opening new connections, starting transactions manually, etc.). Panics if BaseCRUD is operating inside a transaction (use the transaction's CRUD instead).
func (*BaseCRUD[T, ID]) Update ¶
Update modifies an existing record identified by its primary key.
Only Mutable columns are written (schema-shaped): a full Update can no longer clobber created_at or resurrect a soft-deleted row. A model with no mutable columns makes Update a verified no-op. The write guard can be bypassed 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 when it conflicts on the conflict target (the primary key by default). It is the reference SQL upsert for the raw drivers, emitting flavor-appropriate SQL through 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 on-conflict update branch obeys Mutable (a restricted UpdateOnConflict set is rejected exactly like Patch when it names a non-mutable column) and bumps updated_at. The write guard bypass (r3.WithoutWriteGuard) skips the capability check the same way Create/Patch do. The stored row is returned via RETURNING where supported, and re-fetched by the conflict target otherwise.
type BaseRaw ¶
type BaseRaw[T any, ID any] struct { // Executor is the SQL executor (either *sql.DB or *sql.Tx). Executor SQLExecutor Meta StructMeta }
BaseRaw is a database/sql wrapper that allows executing arbitrary queries with automatic struct scanning based on StructMeta.
func NewBaseRaw ¶
func NewBaseRaw[T any, ID comparable](executor SQLExecutor, meta StructMeta) *BaseRaw[T, ID]
NewBaseRaw creates a new BaseRaw instance.
func (*BaseRaw[T, ID]) Exec ¶
Exec executes a raw SQL query that does not return rows (INSERT, UPDATE, DELETE).
type Defaults
deprecated
Defaults is an alias for r3.Defaults. Kept here for backward compatibility with existing drivers.
Deprecated: Use r3.Defaults directly.
func NewDefaults
deprecated
func NewDefaults() Defaults
NewDefaults returns Defaults initialized with reasonable default queries.
Deprecated: Use r3.NewDefaults directly.
type DefaultsManager
deprecated
type DefaultsManager = r3.DefaultsManager
DefaultsManager is an alias for r3.DefaultsManager. Kept here for backward compatibility with existing drivers.
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 indicates whether the database supports the RETURNING clause
// in INSERT statements (PostgreSQL, SQLite 3.35+).
// When false, Create() uses INSERT + last-insert-id + SELECT.
SupportsRETURNING bool
// TimestampFunc is the SQL expression for the current timestamp.
// Used by soft-delete to set the deleted_at column.
// Examples: "NOW()" (PostgreSQL), "CURRENT_TIMESTAMP" (MySQL), "datetime('now')" (SQLite).
TimestampFunc string
// IdentifierQuote is the character this backend uses to quote SQL identifiers.
// The dialect layer emits ANSI double quotes (`"col"`); the engine rewrites
// them to this character when assembling the final query (see QuoteIdentifiers).
// Empty means ANSI double quotes (Postgres, SQLite). MySQL needs backticks,
// because without ANSI_QUOTES mode it reads `"col"` as a string literal — which
// silently breaks every filter and sort built from the dialect.
IdentifierQuote string
// UsesOnConflictClause selects the upsert dialect. When true the backend
// speaks Postgres/SQLite `ON CONFLICT (cols) DO UPDATE SET ...` (the conflict
// target is explicit and EXCLUDED refers to the would-be-inserted row). When
// false it speaks MySQL `ON DUPLICATE KEY UPDATE col = VALUES(col)` (the
// conflict target is any unique key, so the columns are ignored).
UsesOnConflictClause bool
}
Flavor describes the SQL dialect differences between database backends. It is passed to NewBaseCRUD to configure SQL generation.
func (Flavor) ConvertPlaceholders ¶
ConvertPlaceholders converts `?` placeholders in clause to `$1, $2, ...` starting from startIdx. Returns the converted string and the next available index. This is a no-op when the flavor uses `?` placeholders.
func (Flavor) Placeholders ¶
Placeholders generates a placeholder string for count parameters. For PlaceholderDollar starting from startIdx: "$1, $2, $3" For PlaceholderQuestion: "?, ?, ?".
func (Flavor) QuoteIdentifiers ¶
QuoteIdentifiers rewrites the ANSI double-quoted identifiers emitted by the dialect layer (`"col"`, `"tbl"."col"`) to this flavor's identifier quote. It is a no-op for flavors that use ANSI double quotes (Postgres, SQLite).
It is safe to apply to a fully assembled query: values are always bound as parameters (`?` / `$N`), never inlined, so the only double quotes in generated SQL are identifier quotes. The dialect escapes an embedded quote by doubling it (`""`), and a plain ReplaceAll maps that to the doubled target quote — which is also the correct escape — so escaping is preserved. (In practice identifiers are validated to contain no quotes, so escaping never actually occurs.)
func (Flavor) SetExprs ¶
SetExprs generates "col1 = $1, col2 = $2, ..." or "col1 = ?, col2 = ?, ..." for UPDATE SET clauses.
func (Flavor) UpsertClause ¶ added in v0.0.4
UpsertClause builds the trailing on-conflict clause of an upsert INSERT for this flavor: `ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col, ...` for Postgres/SQLite, `ON DUPLICATE KEY UPDATE col = VALUES(col), ...` for MySQL. conflictCols is the conflict target (ignored by MySQL, which keys off any unique index). An empty updateCols yields DO NOTHING (a self-assign no-op on MySQL) so a conflict leaves 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 the WHERE clauses/joins, pagination, and the
// merged Query. Its Sorts are already restricted to group fields and
// aggregate aliases (see r3.Query.AggregateSorts); cursor pagination does
// not 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 quoted group-by column list (empty = whole-set row).
GroupBy []string
// Having is the HAVING clause with args (zero when no Having filters).
// Aliases are already lowered to their aggregate expressions (portable —
// PostgreSQL does not allow select-list aliases in HAVING).
Having r3sql.SQLClause
}
PreparedAggregateQuery holds pre-computed SQL components for an Aggregate call, the aggregate sibling of PreparedListQuery. Any driver (database/sql, GORM, Bun, go-pg) consumes the same pieces: WHERE comes from the embedded PreparedListQuery, the aggregate-specific parts are the select list, GROUP BY columns, HAVING clause, and alias-aware ORDER BY.
func PrepareAggregateQuery ¶ added in v0.0.3
func PrepareAggregateQuery( dm *DefaultsManager, schema r3.Schema, qarg ...r3.Query, ) (PreparedAggregateQuery, error)
PrepareAggregateQuery merges defaults with user query args, validates the merged query against the schema (pass a zero r3.Schema for structural-only validation), and converts it into SQL-ready aggregate components.
func PrepareMergedAggregateQuery ¶ added in v0.0.3
PrepareMergedAggregateQuery builds the SQL aggregate components for an already-merged query. Like PrepareMergedListQuery, it is exposed separately so a driver can transform the merged query (e.g. lower relationship filters) before the clauses are built.
type PreparedListQuery ¶
type PreparedListQuery struct {
// Clauses is the list of SQL WHERE clauses with their args and joins.
Clauses r3sql.SQLClauses
// Sorts is the list of SQL ORDER BY expressions.
Sorts []r3sql.SQLSort
// IsPaginated indicates whether offset-based pagination is active.
IsPaginated bool
// Limit and Offset are set when IsPaginated is true.
Limit int
Offset int
// IsCursorPaginated indicates whether cursor/keyset pagination is active.
// When true, CursorClause contains the keyset WHERE condition and CursorLimit
// contains the LIMIT. No OFFSET or COUNT query should be used.
IsCursorPaginated bool
// CursorClause is the keyset WHERE clause (may be empty for the first page).
CursorClause r3sql.SQLClause
// CursorLimit is the maximum number of results for cursor pagination.
CursorLimit int
// CursorBackward indicates a "before" cursor. Backward keyset pagination
// must scan in the reversed sort order (so LIMIT takes the rows immediately
// preceding the cursor) and then reverse the result slice back to the
// requested order. See OrderBySorts.
CursorBackward bool
// Query is the merged r3.Query (defaults + user args) for access to
// Preloads, IncludeTrashed, and other fields that are driver-specific.
Query r3.Query
}
PreparedListQuery holds pre-computed SQL components derived from an r3.Query. It is the result of converting r3 filters, sorts, and pagination into SQL-ready pieces that any driver (GORM, Bun, go-pg, database/sql) can consume.
This eliminates the duplicated r3-to-SQL conversion logic across drivers.
func PrepareListQuery ¶
func PrepareListQuery(dm *DefaultsManager, qarg ...r3.Query) (PreparedListQuery, error)
PrepareListQuery merges defaults with user query args, then converts filters, sorts, and pagination into SQL-ready components.
Drivers call this once at the start of List() and then consume the result using their ORM-specific APIs.
func PrepareMergedListQuery ¶
func PrepareMergedListQuery(q r3.Query) (PreparedListQuery, error)
PrepareMergedListQuery builds the SQL components for an already-merged query (defaults + user args). It is the half of PrepareListQuery after the merge, exposed so a driver can transform the merged query — e.g. lower relationship ("has") filters into key-set In filters — before the clauses are built.
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 ORDER BY expressions to apply to the main SELECT.
For forward queries it is just Sorts. For a backward ("before") cursor it returns the sorts with reversed direction: the rows immediately preceding the cursor must be scanned in the opposite order under LIMIT, then reversed back to the requested order by the caller (see CursorBackward).
type RelationKind ¶
type RelationKind = r3tag.RelationKind
RelationKind describes the type of relationship between two entities. Alias of r3tag.RelationKind.
type RelationMeta ¶
type RelationMeta struct {
FieldName string // Go struct 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 // foreign key column name (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)
Owned bool // if true, children are lifecycle-bound to parent (has-many only)
TargetMeta StructMeta // metadata for the related entity type
TargetType reflect.Type // reflect.Type of the target entity (element type, not slice/ptr)
}
RelationMeta holds metadata about a struct field that represents a relation.
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 between *sql.DB and *sql.Tx. Both types satisfy this interface, allowing BaseCRUD to operate transparently in both normal and transactional modes.
type StructMeta ¶
type StructMeta struct {
TableName string // e.g. "cities"
Columns []string // column names in order, e.g. ["id", "name", "country_name", ...]
Fields []int // corresponding struct field indices for each column
PKColumn string // primary key column name (defaults to "id")
PKField int // index into Columns/Fields for the PK entry
// SoftDeleteColumn is the column name used for soft-delete (e.g. "deleted_at").
// Empty string means soft-delete is not enabled.
// Detected via the `r3:"soft_delete"` struct tag.
SoftDeleteColumn string
// Relations holds metadata about related entities detected via `r3` struct tags.
// Example tags: `r3:"rel:has-many,fk:city_id"`, `r3:"rel:belongs-to,fk:city_id"`.
Relations []RelationMeta
}
StructMeta holds reflection-based metadata about a struct type T. It is used by BaseCRUD and BaseRaw to build SQL queries and scan results.
func GetStructMeta ¶
func GetStructMeta[T any]() StructMeta
GetStructMeta derives table name and column info from a generic type T.
Tag priority: `r3` tag is checked first, `db` tag is used as fallback. Fields with tag value "-" (in either tag) are ignored. Pointer-to-basic types are kept (nullable columns); slices, maps, and struct fields (except time.Time) are treated as relation fields.
func (*StructMeta) FieldIndicesForColumns ¶
func (m *StructMeta) FieldIndicesForColumns(selectedCols []string) ([]string, []int)
FieldIndicesForColumns returns the subset indices into Columns/Fields that match the given column names. It also returns the matching column names (preserving order). If selectedCols is empty, returns all indices.
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 field values from an entity for the given column names. The returned values are in the same order as the provided columns. Columns that don't exist in the struct are silently skipped (validate beforehand).
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 only the specified columns. If selectedCols is empty, behaves like ScanDest (all columns).
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 checks that the given columns are valid for a Patch operation. It returns an error if:
- columns is empty
- any column does not exist in the struct
- any column is the primary key
- any column is the soft-delete column
On success, it returns the validated column list (same as input).