enginesql

package
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package enginesql provides the SQL engine for r3.

It serves two roles:

  1. 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).

  2. 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

View Source
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

View Source
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

func ColumnsString(cols []string) string

ColumnsString joins column names with commas (SQL-specific formatting).

func FieldsToColumns deprecated

func FieldsToColumns(fields r3.Fields) []string

FieldsToColumns converts r3.Fields to a []string of column names.

Deprecated: Use r3.FieldsToStrings directly.

func FinalizeCount deprecated

func FinalizeCount[T any](entities []T, paginatedCount int64, isPaginated bool) ([]T, int64)

FinalizeCount returns (entities, totalCount) with the correct total. If pagination was not active, totalCount is simply len(entities).

Deprecated: Use r3.FinalizeCount directly.

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

func RequireMutableColumns(ctx context.Context, schema r3.Schema, cols []string) error

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

func WriteColumns(ctx context.Context, schema r3.Schema, cols []string, op r3.WriteOp) []string

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

func (r *BaseCRUD[T, ID]) BeginTx(ctx context.Context) (r3.TxCRUD[T, ID], error)

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]) Count

func (r *BaseCRUD[T, ID]) Count(ctx context.Context, qarg ...r3.Query) (int64, error)

Count returns the number of records matching the query's filters.

func (*BaseCRUD[T, ID]) Create

func (r *BaseCRUD[T, ID]) Create(ctx context.Context, entity T) (T, error)

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

func (r *BaseCRUD[T, ID]) Delete(ctx context.Context, id ID) error

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]) Get

func (r *BaseCRUD[T, ID]) Get(ctx context.Context, id ID, qarg ...r3.Query) (T, error)

Get retrieves a record by its ID.

func (*BaseCRUD[T, ID]) HardDelete

func (r *BaseCRUD[T, ID]) HardDelete(ctx context.Context, id ID) error

HardDelete permanently removes a record, ignoring soft-delete.

func (*BaseCRUD[T, ID]) List

func (r *BaseCRUD[T, ID]) List(ctx context.Context, qarg ...r3.Query) ([]T, int64, error)

List retrieves records based on the provided query parameters.

func (*BaseCRUD[T, ID]) Patch

func (r *BaseCRUD[T, ID]) Patch(ctx context.Context, entity T, fields r3.Fields) (T, error)

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

func (r *BaseCRUD[T, ID]) Restore(ctx context.Context, id ID) error

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

func (r *BaseCRUD[T, ID]) SQLDB() *sql.DB

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

func (r *BaseCRUD[T, ID]) Update(ctx context.Context, entity T) (T, error)

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

func (r *BaseCRUD[T, ID]) Upsert(ctx context.Context, entity T, opts ...r3.UpsertOption) (T, error)

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

func (r *BaseRaw[T, ID]) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

Exec executes a raw SQL query that does not return rows (INSERT, UPDATE, DELETE).

func (*BaseRaw[T, ID]) Query

func (r *BaseRaw[T, ID]) Query(ctx context.Context, query string, args ...any) ([]T, error)

Query executes a raw SQL query and scans results into a slice of T.

func (*BaseRaw[T, ID]) QueryRow

func (r *BaseRaw[T, ID]) QueryRow(ctx context.Context, query string, args ...any) (T, error)

QueryRow executes a raw SQL query expected to return a single row and scans it into T.

type Defaults deprecated

type Defaults = r3.Defaults

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

func (f Flavor) ConvertPlaceholders(clause string, startIdx int) (string, int)

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

func (f Flavor) Placeholders(count int, startIdx int) string

Placeholders generates a placeholder string for count parameters. For PlaceholderDollar starting from startIdx: "$1, $2, $3" For PlaceholderQuestion: "?, ?, ?".

func (Flavor) QuoteIdentifiers

func (f Flavor) QuoteIdentifiers(sql string) string

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

func (f Flavor) SetExprs(cols []string, startIdx int) string

SetExprs generates "col1 = $1, col2 = $2, ..." or "col1 = ?, col2 = ?, ..." for UPDATE SET clauses.

func (Flavor) UpsertClause added in v0.0.4

func (f Flavor) UpsertClause(conflictCols, updateCols []string) string

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.

func (Flavor) WhereEq

func (f Flavor) WhereEq(column string, nextIdx int) string

WhereEq generates "column = $N" or "column = ?" depending on the placeholder style. nextIdx is the 1-based parameter index (used only for dollar-style).

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

func PrepareMergedAggregateQuery(schema r3.Schema, q r3.Query) (PreparedAggregateQuery, error)

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).

Jump to

Keyboard shortcuts

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