Documentation
¶
Index ¶
- Constants
- Variables
- func NewDatabase(db *sql.DB, schema dal.Schema, options DbOptions) dal.DB
- func NewSimpleSchema(idFieldName string) dal.Schema
- func RegisterSQLiteDecimalFunctions() error
- type DbOptions
- type Field
- type IdentifierCase
- type NativeJoinEligibility
- type NativeJoinHintFragments
- type NativeJoinHintTranslator
- type NativeStructuredQueryCompiler
- type PlaceholderDialect
- type Recordset
- type RecordsetType
- type TableNotFoundError
Constants ¶
const Version = "0.0.8"
Version indicates package version
Variables ¶
var ErrNoFieldsToWrite = errors.New("no fields to write")
ErrNoFieldsToWrite is wrapped by the error a key write returns when there is nothing to write, before any statement is sent: an Update or UpdateMulti with no updates; a Set or SetMulti of a record with no field that is not a column of the primary key; an Insert or InsertMulti of a record with neither a key ID nor a field. Test for it with errors.Is.
var ErrTableNotFound = errors.New("table not found")
ErrTableNotFound matches, through errors.Is, the error a read returns for a table the database does not have under that name: see TableNotFoundError.
var ErrUndeclaredNestedRecordset = errors.New("nested key without a declared recordset")
ErrUndeclaredNestedRecordset is wrapped by the error every key read and write returns for a key with a parent when no recordset is declared under its joined name in DbOptions.Recordsets. The statement is not sent. Test for it with errors.Is.
It is one rule for nested keys in every operation (Exists, Get, GetMulti, Insert, InsertMulti, Set, SetMulti, Update, UpdateMulti, Delete and DeleteMulti, on a database and on a transaction): a SQL recordset is a table, the table of a nested key is named by the collections of the key and of its parents joined with "_", the key's own collection first, and it is the declaration of that recordset that says the table exists and has the primary key the key's ID is looked up in. A key without a parent is not affected.
var ErrUnsafeName = errors.New("unsafe SQL name")
ErrUnsafeName is wrapped by the error a key read or write returns when a collection, field or primary-key name may not be written into SQL text. The statement is not sent. Test for it with errors.Is.
Names are the one thing of a key read or write that is not a bound parameter, so every name is checked before it enters SQL text:
- With a dialect whose identifier quoting has been reviewed (DbOptions.StructuredQueryDialect "sqlite"), a name must not be empty, contain a control character (NUL included) or invalid UTF-8, or be longer than 255 bytes. It is then written quoted with the quote character escaped, so a name with a space, a quote or a comment marker is an ordinary identifier.
- With no dialect, or one without reviewed quoting, a name must be a plain identifier: a letter or underscore followed by letters, digits or underscores, ASCII only, and no longer than 255 bytes. It is written as given, so the statement text of every name accepted is the text this package has always sent.
- With DbOptions.StructuredQueryDialect "postgres" a name must also be no longer than 63 bytes, which is all PostgreSQL keeps of an identifier: a longer name would address the table or column named by its first 63 bytes. The name of a nested key is the joined name, which is checked as one.
A name used to be pasted into the statement as written, so one that carried its own quoting ("Order Details" with the quote characters, [Order Details], a name between backticks) acted as a quoted identifier. It no longer does: with no dialect such a name is refused, and with "sqlite" it is one literal name, quote characters included, that finds no table. A caller passes the bare name (Order Details) with the "sqlite" dialect. With no dialect there is no way to reach a table or column that needs quoting until that engine has reviewed quoting.
Functions ¶
func NewDatabase ¶
NewDatabase creates a new instance of DALgo adapter to SQL database.
The returned dal.DB is sealed by dal.NewDB: every read-write transaction it starts hands the worker a transaction whose writes run the framework's BeforeSave validation and hooks before reaching this adapter's code.
func NewSimpleSchema ¶ added in v0.2.1
func RegisterSQLiteDecimalFunctions ¶ added in v0.27.0
func RegisterSQLiteDecimalFunctions() error
RegisterSQLiteDecimalFunctions registers exact decimal functions on modernc's process-wide SQLite driver. Call it before opening the first SQLite connection. Values are accepted as decimal strings, byte strings, or exact integer values; SQLite REAL values are rejected because their original decimal value is lost. Results are decimal strings so SQLite stores them without a binary-float round trip.
Scalar functions propagate NULL. decimal_cmp returns -1, 0, or 1. Division, rounding, and average use round-half-to-even. Scales must be between 0 and 38. decimal_sum and decimal_avg ignore NULL inputs and return NULL for no non-NULL rows.
Types ¶
type DbOptions ¶ added in v0.4.2
type DbOptions struct {
ID string
PrimaryKey []string
// ExactNumericValues keeps the database's text representation of NUMERIC
// values instead of converting them to float64 in record, recordset, and
// key reads. Key reads require map record data; struct targets are refused
// because their field types may force a lossy conversion. It is opt-in
// because existing callers rely on the legacy float64 representation.
// Drivers must expose NUMERIC as text or bytes; a float scan result is
// refused because its original decimal value cannot be recovered safely.
ExactNumericValues bool
// PreserveBinaryValues keeps PostgreSQL BYTEA values as byte slices in
// map-backed reads instead of applying the legacy []byte-to-string
// conversion. Other database types and the default behavior are unchanged.
PreserveBinaryValues bool
// PreserveNativeValues keeps the value the driver delivers for a SQLite
// result column as it is: an int64, float64, string, []byte or nil (NULL),
// without the affinity and text normalization the readers otherwise apply.
// It is opt-in; the zero value keeps the legacy behavior exactly.
//
// The option is supported only with StructuredQueryDialect "sqlite". Any
// other dialect, an empty one included, is refused before any statement,
// because this package does not claim the native value classes of another
// engine. It cannot be combined with ExactNumericValues, whose decimal text
// is not a native value; that pair is refused before any statement too.
// PreserveBinaryValues is a PostgreSQL option and is inert on SQLite, whose
// BLOB values are already native []byte.
//
// Record reads (Get, GetMulti and the records reader) into map data keep the
// driver's values, with a []byte copied before the driver can reuse its
// buffer. A read into struct data is refused before any statement, because a
// field type could force the very conversion the option exists to avoid.
// Recordset reads build every result column as recordset.Column[any] whose
// ValueType is interface{}, keeping the result's Name and DbType; one column
// may then hold any of the five SQLite storage classes, NULL included, in
// different rows of the same result.
//
// The compiled SQL, the declared column metadata and the keys of read
// records do not change: a key is still derived from the same value as
// before.
PreserveNativeValues bool
// Recordsets declares the recordsets (tables) of key reads and writes, by name,
// with the primary key each is looked up by.
//
// The recordset of a key without a parent is declared under the key's
// collection. The recordset of a nested key is declared under the collections
// of the key and of its parents joined with "_", the key's own collection
// first: the key orders/o1/lines/l5 addresses the table "lines_orders", and its
// recordset is declared under "lines_orders". A nested key whose joined name is
// not declared is refused in every operation, with an error wrapping
// ErrUndeclaredNestedRecordset, before any statement is sent.
//
// A parent's ID is in no statement. The key orders/o1/lines/l5 and the key
// orders/o2/lines/l5 address the same row of "lines_orders", and so does the
// key of a collection named "lines_orders" with the ID l5: a SQL recordset
// cannot tell the rows of different parents apart. Declare a nested recordset
// only for a table whose primary key identifies the row without its parent.
//
// Delete follows the declared primary key, as every other operation does. A
// key whose recordset is declared with no primary key, or with more than one
// column, is refused before any statement (the errors Update returns). The
// column ID is the default of Delete and DeleteMulti only where no recordset
// is declared for the key, a nil entry being none.
//
// A structured read finds the recordset of its base source under the name the
// query spells and, on a fold-lower mount (IdentifierCaseFoldLower), under that
// name folded to lower case, as the catalog lookup folds it; the primary key of the
// recordset keys the records of the read. With no recordset declared and no
// PrimaryKey, the typed PostgreSQL dialect keys them by the primary key the catalog
// reports for the source when it is one column, and the SQLite dialect does the same
// when that column holds one value per row and never NULL (an INTEGER or TEXT column
// that is NOT NULL or is the rowid alias, and holds no BLOB); every other read is keyed by ordinal, the
// position of the row in the result counted from 0 and written as decimal text: no
// primary key, a composite one, a view, a grouped or aggregated query, a SQLite join,
// and the legacy emitter, which has no catalog to ask. Within one read no two records
// have the same key; only a record with no key (a nil key, or a nil or empty ID) may
// repeat, and the reader makes none. No path gives the records one constant ID.
Recordsets map[string]*Recordset
// Placeholder controls how SQL parameter markers are emitted.
// The zero value (PlaceholderQuestion) uses "?" — compatible with
// SQLite, MySQL, and most other drivers. Set to PlaceholderDollar
// for PostgreSQL, which requires "$1", "$2", … positional markers.
Placeholder PlaceholderDialect
// StructuredQueryDialect names the SQL dialect whose identifier quoting has
// been reviewed. Empty preserves legacy emission; "sqlite", "postgres", "mysql",
// "oracle" and "sqlserver" are supported; any other value makes a structured read
// fail. It decides two things:
//
// - Structured reads: "sqlite" opts them into safe dialect-specific
// compilation, and so do "postgres", "mysql" (MySQL 8.0.17 and later),
// "oracle" (Oracle Database 23ai) and "sqlserver" (SQL Server 2022), the typed
// compiler's dialects, which read
// the catalog facts of every source first, on the connection the statement
// runs on, and compile with bound values only (see IdentifierCase and the
// README). The typed MySQL dialect writes "?" placeholders, the Oracle one
// ":1", ":2", ... and the SQL Server one "@p1", "@p2", ..., whatever
// Placeholder says.
// - The collection, field and primary-key names of every key read and write
// (Exists, Get, GetMulti, Insert, Set, SetMulti, Update, UpdateMulti,
// Delete, DeleteMulti): with "sqlite" a name is written quoted, and only an
// empty name, a control character, invalid UTF-8 or more than 255 bytes is
// refused; with an empty or any other value, "postgres", "mysql", "oracle" and
// "sqlserver" included (their reviewed quoting covers structured reads, not these), a name must be a plain
// identifier (letters, digits and underscores, not starting with a digit,
// at most 255 bytes), or the call fails with ErrUnsafeName and sends no
// statement.
//
// A name that carries its own quoting ("Order Details" with the quote
// characters, [Order Details]) is no longer a quoted identifier: with "sqlite"
// it is one literal name, quote characters included, and finds no table, so
// pass the bare name; with no dialect it is refused.
StructuredQueryDialect string
// NativeJoinHintTranslator optionally translates validated DALgo JOIN
// algorithm preferences into trusted, dialect-owned SQL fragments. The
// translator receives the complete relation tree so it can preserve each
// edge's independent preference order. Nil is the adapter's explicit ignore
// policy: a custom compiler receives no fragments. It also preserves the
// existing SQL byte stream, which is how SQLite ignores JOIN hints by default.
NativeJoinHintTranslator NativeJoinHintTranslator
// NativeJoinEligibility is the adapter's opt-in semantic proof for native
// JOIN execution with NativeStructuredQueryCompiler. Both fields are
// required for a non-SQLite native path; a nil hook preserves SQLite's
// transaction-local key preflight and declines other dialects.
NativeJoinEligibility NativeJoinEligibility
// NativeStructuredQueryCompiler emits a complete structured SQL query for a
// trusted adapter dialect. It receives translated JOIN hint fragments on the
// actual read path. Oracle and SQL Server have the typed "oracle" and "sqlserver" dialects.
NativeStructuredQueryCompiler NativeStructuredQueryCompiler
// IdentifierCase is read with StructuredQueryDialect "postgres" and picks how
// names are written: as the query spells them (the default), or folded to lower
// case. Any other value makes every structured read, join check and JoinFields
// call fail with an error that names it. With "mysql", "oracle" and "sqlserver"
// only the default (empty or IdentifierCaseExact) is accepted. See IdentifierCase.
IdentifierCase IdentifierCase
// IsAlreadyExists reports whether err — the raw error returned by the
// underlying database/sql driver for a failed INSERT — represents a
// duplicate-key violation (a unique or primary-key constraint failure).
// Detection is driver-specific — pgx's *pgconn.PgError with code
// "23505", go-sql-driver/mysql's *mysql.MySQLError with number 1062,
// modernc.org/sqlite's *sqlite.Error with an SQLITE_CONSTRAINT_* code —
// so dalgo2sql cannot recognize it on its own. The wrapping adapter
// (dalgo2postgres, dalgo2mysql, dalgo2sqlite, …) supplies this hook.
//
// The zero value (nil) is backward compatible: it preserves today's
// behavior exactly, and the raw driver error passes through unwrapped.
// When set and it reports true for an insert's error, dalgo2sql wraps
// that error with record.ErrRecordExists (see execInsert) so callers
// can test it with record.IsAlreadyExists — the driver error itself is
// preserved in the chain, never replaced, so existing callers matching
// on error text or type keep working.
IsAlreadyExists func(err error) bool
}
DbOptions provides database sqlOptions for DALgo - // TODO: document why & how to use
func (DbOptions) GetRecordsetByKey ¶ added in v0.4.2
type IdentifierCase ¶ added in v0.26.0
type IdentifierCase string
IdentifierCase says how a SQL dialect that quotes every name writes the names a query spells. It is read with StructuredQueryDialect "postgres"; "mysql", "oracle" and "sqlserver" accept only IdentifierCaseExact, and the other dialects ignore it. The zero value is IdentifierCaseExact.
A mount that folds case matches names by their lower-case form on both sides of every check, and the result of a query keeps the names the query asked for: a field written Total is selected as "total" and comes back as Total. No field mask or access check may be applied on such a mount unless the names it compares are folded first, or Total would pass a mask that names total.
const ( // IdentifierCaseExact writes every name as the query spells it, inside quotes, // so Album and album are two tables. It is the mode for databases whose objects // were created with quoted mixed-case names, and the default. IdentifierCaseExact IdentifierCase = "exact" // IdentifierCaseFoldLower writes every name lower-cased, inside quotes, as // dalgo2postgres's DDL stores them, so a database it created is read back with // any spelling of a name. IdentifierCaseFoldLower IdentifierCase = "fold-lower" )
type NativeJoinEligibility ¶ added in v0.19.0
type NativeJoinEligibility func(context.Context, dal.StructuredQuery) error
NativeJoinEligibility lets a concrete adapter validate whether its native compiler can preserve DALgo JOIN semantics for one complete query.
type NativeJoinHintFragments ¶ added in v0.19.0
type NativeJoinHintFragments struct {
AfterSelect string
JoinOperators map[string]string
AfterQuery string
HandledPaths []string
}
NativeJoinHintFragments identifies where a dialect places its trusted SQL JOIN hints. JoinOperators is keyed by DALgo structural paths such as "from.joins[0]" and is emitted between the JOIN type and JOIN keyword, so a SQL Server adapter can return "HASH" for `INNER HASH JOIN`. AfterSelect and AfterQuery support dialects such as Oracle that place optimizer hints after SELECT or at the end of the statement. HandledPaths must acknowledge every hinted edge as applied or intentionally ignored by adapter policy; a rejected preference is returned as an error. This prevents a configured translator from silently dropping a per-edge preference while using only a statement level fragment.
type NativeJoinHintTranslator ¶ added in v0.19.0
type NativeJoinHintTranslator interface {
TranslateNativeJoinHints(dal.FromSource) (NativeJoinHintFragments, error)
}
NativeJoinHintTranslator translates a complete, validated relation tree for one native SQL query. It is implemented by trusted database adapters, never from TugQL input. An error prevents SQL from being emitted.
type NativeStructuredQueryCompiler ¶ added in v0.19.0
type NativeStructuredQueryCompiler interface {
CompileNativeStructuredQuery(dal.StructuredQuery, NativeJoinHintFragments) (string, []any, error)
}
NativeStructuredQueryCompiler emits a complete structured query for a concrete adapter dialect. Its implementation owns parameter syntax, identifier quoting, and all dialect semantics.
type PlaceholderDialect ¶ added in v0.9.0
type PlaceholderDialect int
PlaceholderDialect selects how positional SQL parameters are formatted. The zero value (PlaceholderQuestion) is backward-compatible with all drivers that accept "?" — SQLite, MySQL, etc.
const ( // PlaceholderQuestion emits "?" for every parameter (default, SQLite/MySQL style). PlaceholderQuestion PlaceholderDialect = iota // PlaceholderDollar emits "$1", "$2", … (PostgreSQL style). PlaceholderDollar )
type Recordset ¶
type Recordset struct {
// contains filtered or unexported fields
}
Recordset hold recordset settings
func NewRecordset ¶ added in v0.0.9
func NewRecordset(name string, t RecordsetType, primaryKey []dal.FieldRef) *Recordset
func (*Recordset) PrimaryKey ¶
func (*Recordset) PrimaryKeyFieldNames ¶ added in v0.0.9
func (*Recordset) Type ¶
func (v *Recordset) Type() RecordsetType
type RecordsetType ¶
type RecordsetType = int
RecordsetType defines type of a database recordset
const ( // Table identifies a table in a database Table RecordsetType = iota )
type TableNotFoundError ¶ added in v0.26.0
type TableNotFoundError struct {
// Schema and Name are the source as the query wrote it; Schema is empty for a
// source without one.
Schema, Name string
// SuggestedSchema and SuggestedName are the nearest existing name, schema
// included when the query named one, as the database stores them; both are empty
// when no name is near.
SuggestedSchema, SuggestedName string
// CaseSensitive says the dialect matches names exactly, so a name that differs
// from an existing one only in case is a different name.
CaseSensitive bool
}
TableNotFoundError is the error a structured read returns, with the PostgreSQL dialect, for a source the database does not resolve under the name the query spells: no such table, view, materialized view, foreign table or partitioned table (a sequence is none of these), or a relation with no readable column. It names the table as the query wrote it, and the nearest name that does exist, when one is near enough to be a guess at what the caller meant.
errors.Is(err, ErrTableNotFound) matches it. The message names the table the query asked for, quoted, never anything else the query held.
func (*TableNotFoundError) Error ¶ added in v0.26.0
func (e *TableNotFoundError) Error() string
func (*TableNotFoundError) Is ¶ added in v0.26.0
func (e *TableNotFoundError) Is(target error) bool
Is makes errors.Is(err, ErrTableNotFound) true for every TableNotFoundError.
Source Files
¶
- database.go
- database_options.go
- default_schema.go
- deleter.go
- dialect_mysql.go
- dialect_oracle.go
- dialect_postgres.go
- dialect_sqlserver.go
- getter.go
- inserter.go
- native_join_hints.go
- nullable_column.go
- placeholder.go
- projection.go
- reader_base.go
- reader_records.go
- reader_recordset.go
- reader_sqlite_key.go
- recordset.go
- recordsets.go
- reserved_words.go
- setter.go
- sql.go
- sql_names.go
- sqlite_decimal.go
- sqlite_emit.go
- sqlite_join_eligibility.go
- sqlite_protected.go
- struct_scan.go
- transaction.go
- typed_catalog.go
- typed_dialect.go
- typed_dialects.go
- typed_placeholders.go
- typed_read.go
- typed_sql.go
- updater.go
- upserter.go
- version.go