add some documentation
This commit is contained in:
parent
d39dfb948b
commit
33c47b2aa8
17
diamond.go
17
diamond.go
@ -12,13 +12,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LevelQuery enables logging of SQL queries if passed to Config.LogLevel
|
||||
const LevelQuery = slog.Level(-6)
|
||||
const defaultKey = "default"
|
||||
|
||||
type Config struct {
|
||||
DryRun bool
|
||||
LogLevel slog.Level
|
||||
LogTo io.Writer
|
||||
DryRun bool // when true, queries will not run on the underlying database
|
||||
LogLevel slog.Level // controls the level of information logged; defaults to slog.LevelInfo if not set
|
||||
LogTo io.Writer // where to write log output to; defaults to os.Stdout
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
@ -34,6 +35,7 @@ type Engine struct {
|
||||
connStr string
|
||||
}
|
||||
|
||||
// Models - parse and register one or more types as persistable models
|
||||
func (e *Engine) Models(v ...any) {
|
||||
emm := makeModelMap(v...)
|
||||
for k := range emm.Map {
|
||||
@ -45,6 +47,8 @@ func (e *Engine) Models(v ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Model - createes a Query and sets its model to
|
||||
// the one corresponding to the type of `val`
|
||||
func (e *Engine) Model(val any) *Query {
|
||||
qq := &Query{
|
||||
engine: e,
|
||||
@ -57,10 +61,12 @@ func (e *Engine) Model(val any) *Query {
|
||||
return qq.setModel(val)
|
||||
}
|
||||
|
||||
// QueryRaw - wrapper for the Query method of pgxpool.Pool
|
||||
func (e *Engine) QueryRaw(sql string, args ...any) (pgx.Rows, error) {
|
||||
return e.conn.Query(e.ctx, sql, args...)
|
||||
}
|
||||
|
||||
// Migrate - non-destructive; run migrations to update the underlying schema, WITHOUT dropping tables beforehand
|
||||
func (e *Engine) Migrate() error {
|
||||
failedMigrations := make(map[string]*Model)
|
||||
var err error
|
||||
@ -83,6 +89,8 @@ func (e *Engine) Migrate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// MigrateDropping - destructive migration; DROP the necessary tables if they exist,
|
||||
// then recreate them to match your models' schema
|
||||
func (e *Engine) MigrateDropping() error {
|
||||
for _, m := range e.modelMap.Map {
|
||||
sql := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE;", m.TableName)
|
||||
@ -109,10 +117,13 @@ func (e *Engine) logSql(msg, sql string) {
|
||||
e.logger.Log(e.ctx, LevelQuery, msg, "sql", sql)
|
||||
}
|
||||
|
||||
// Disconnect - closes and disposes of this Engine's connection pool.
|
||||
func (e *Engine) Disconnect() {
|
||||
e.conn.Close()
|
||||
}
|
||||
|
||||
// Open - creates a new connection according to `connString`
|
||||
// and returns a brand new Engine to run FUCK operations on.
|
||||
func Open(connString string, cfg *Config) (*Engine, error) {
|
||||
if cfg == nil {
|
||||
cfg = &Config{
|
||||
|
@ -2,11 +2,14 @@ package orm
|
||||
|
||||
import "time"
|
||||
|
||||
// Document - embed this into your structs anonymously to specify
|
||||
// model parameters like table name via struct tags
|
||||
type Document struct {
|
||||
Created time.Time `json:"createdAt" tstype:"Date"`
|
||||
Modified time.Time `json:"modifiedAt" tstype:"Date"`
|
||||
}
|
||||
|
||||
// SaveOptions - unused (for now)
|
||||
type SaveOptions struct {
|
||||
SetTimestamps bool
|
||||
}
|
||||
|
@ -3,5 +3,5 @@ package orm
|
||||
import "fmt"
|
||||
|
||||
var ErrNoConditionOnDeleteOrUpdate = fmt.Errorf("refusing to delete/update with no conditions specified.\n"+
|
||||
" (hint: call `.Where(%s)` or `.Where(%s)` to do so anyways)",
|
||||
" (hint: call `.WhereRaw(%s)` or `.WhereRaw(%s)` to do so anyways)",
|
||||
`"true"`, `"1 = 1"`)
|
||||
|
22
field.go
22
field.go
@ -8,17 +8,17 @@ import (
|
||||
|
||||
// Field - represents a field with a valid SQL type in a Model
|
||||
type Field struct {
|
||||
Name string
|
||||
ColumnName string
|
||||
ColumnType string
|
||||
Type reflect.Type
|
||||
Original reflect.StructField
|
||||
Model *Model
|
||||
Index int
|
||||
AutoIncrement bool
|
||||
PrimaryKey bool
|
||||
Nullable bool
|
||||
embeddedFields map[string]*Field
|
||||
Name string // the name of this field as it appears in its Model's type definition
|
||||
ColumnName string // this field's snake_cased column name as it appears in database
|
||||
ColumnType string // the SQL type of this field's column (bigint, bigserial, text, ...)
|
||||
Type reflect.Type // the reflect.Type of the struct field this Field represents
|
||||
Original reflect.StructField // the raw struct field, as obtained by using reflect.Type.Field or reflect.Type.FieldByName
|
||||
Model *Model // the Model this field belongs to
|
||||
Index int // the index at which Original appears in its struct
|
||||
AutoIncrement bool // whether this field's column is an auto-incrementing column
|
||||
PrimaryKey bool // true if this field's column is a primary key
|
||||
Nullable bool // true if this field's column can be NULL
|
||||
embeddedFields map[string]*Field // mapping of column names to Field pointers that correspond to the surrounding struct's fields
|
||||
}
|
||||
|
||||
func (f *Field) isAnonymous() bool {
|
||||
|
31
model.go
31
model.go
@ -4,15 +4,16 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Model - an intermediate representation of a Go struct
|
||||
type Model struct {
|
||||
Name string
|
||||
Type reflect.Type
|
||||
Relationships map[string]*Relationship
|
||||
IDField string
|
||||
Fields map[string]*Field
|
||||
FieldsByColumnName map[string]*Field
|
||||
TableName string
|
||||
embeddedIsh bool
|
||||
Name string // the name, almost always the name of the underlying Type
|
||||
Type reflect.Type // the Go type this model represents
|
||||
Relationships map[string]*Relationship // a mapping of struct field names to Relationship pointers
|
||||
IDField string // the name of the field containing this model's ID
|
||||
Fields map[string]*Field // mapping of struct field names to Field pointers
|
||||
FieldsByColumnName map[string]*Field // mapping of database column names to Field pointers
|
||||
TableName string // the name of the table where this model's data is stored. defaults to a snake_cased version of the type/struct name if not provided explicitly via tag
|
||||
embeddedIsh bool // INTERNAL - whether this model is: 1) contained in another type and 2) wasn't explicitly passed to the `Models` method (i.e., it can't "exist" on its own)
|
||||
}
|
||||
|
||||
func (m *Model) addField(field *Field) {
|
||||
@ -63,17 +64,3 @@ func (m *Model) needsPrimaryKey(val reflect.Value) bool {
|
||||
_, pk := m.getPrimaryKey(val)
|
||||
return pk == nil || reflect.ValueOf(pk).IsZero()
|
||||
}
|
||||
|
||||
func (m *Model) columnsWith(rel *Relationship) (cols []string, err error) {
|
||||
for _, f := range m.Fields {
|
||||
if f.ColumnType != "" {
|
||||
cols = append(cols, f.ColumnName)
|
||||
}
|
||||
}
|
||||
for _, r2 := range m.Relationships {
|
||||
if r2.Type == ManyToOne {
|
||||
cols = append(cols, pascalToSnakeCase(r2.joinField()))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func makeModelMap(models ...any) *internalModelMap {
|
||||
modelMap := &internalModelMap{
|
||||
Map: make(map[string]*Model),
|
||||
}
|
||||
//modelMap := make(map[string]*Model)
|
||||
//internalModelMap := make(map[string]*Model)
|
||||
for _, model := range models {
|
||||
minfo := parseModel(model)
|
||||
modelMap.Mux.Lock()
|
||||
|
39
query.go
39
query.go
@ -9,17 +9,19 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Query - contains the state and other details
|
||||
// pertaining to the current FUCK operation (Find, Update/Create, Kill [Delete])
|
||||
type Query struct {
|
||||
engine *Engine
|
||||
model *Model
|
||||
tx pgx.Tx
|
||||
ctx context.Context
|
||||
populationTree map[string]any
|
||||
wheres map[string][]any
|
||||
joins []string
|
||||
orders []string
|
||||
limit int
|
||||
offset int
|
||||
engine *Engine // the Engine instance that created this Query
|
||||
model *Model // the primary Model this Query pertains to
|
||||
tx pgx.Tx // the transaction for insert, update and delete operations
|
||||
ctx context.Context // does nothing, but is needed by some pgx functions
|
||||
populationTree map[string]any // a tree-like map representing the dot-separated paths of fields to populate
|
||||
wheres map[string][]any // a mapping of where clauses to a list of their arguments
|
||||
joins []string // slice of tables to join on before executing Find. useful for hwne you have Where clauses referencing fields/columns in other structs/tables
|
||||
orders []string // slice of `ORDER BY` clauses
|
||||
limit int // argument to a LIMIT clause, if non-zero
|
||||
offset int // unused (for now)
|
||||
}
|
||||
|
||||
func (q *Query) setModel(val any) *Query {
|
||||
@ -36,11 +38,15 @@ func (q *Query) cleanupTx() {
|
||||
q.tx = nil
|
||||
}
|
||||
|
||||
// Order - add an `ORDER BY` clause to the current Query.
|
||||
// Only applicable for Find queries
|
||||
func (q *Query) Order(order string) *Query {
|
||||
q.orders = append(q.orders, order)
|
||||
return q
|
||||
}
|
||||
|
||||
// Limit - limit resultset to at most `limit` results.
|
||||
// does nothing if `limit` <= 0 or the final operation isn't Find
|
||||
func (q *Query) Limit(limit int) *Query {
|
||||
if limit > -1 {
|
||||
q.limit = limit
|
||||
@ -48,6 +54,8 @@ func (q *Query) Limit(limit int) *Query {
|
||||
return q
|
||||
}
|
||||
|
||||
// Offset - skip to the nth result, where n = `offset`.
|
||||
// does nothing if `offset` <= 0 or the final operation isn't Find
|
||||
func (q *Query) Offset(offset int) *Query {
|
||||
if offset > -1 {
|
||||
q.offset = offset
|
||||
@ -55,21 +63,30 @@ func (q *Query) Offset(offset int) *Query {
|
||||
return q
|
||||
}
|
||||
|
||||
// Where - add a `WHERE` clause to this query.
|
||||
// struct field names can be passed to this method,
|
||||
// and they will be automatically converted
|
||||
func (q *Query) Where(cond string, args ...any) *Query {
|
||||
q.processWheres(cond, "eq", args...)
|
||||
return q
|
||||
}
|
||||
|
||||
// WhereRaw - add a `WHERE` clause to this query, except `cond` is passed as-is.
|
||||
func (q *Query) WhereRaw(cond string, args ...any) *Query {
|
||||
q.wheres[cond] = args
|
||||
return q
|
||||
}
|
||||
|
||||
// In - add a `WHERE ... IN(...)` clause to this query
|
||||
func (q *Query) In(cond string, args ...any) *Query {
|
||||
q.processWheres(cond, "in", args...)
|
||||
return q
|
||||
}
|
||||
|
||||
// Join - join the current model's table with the table
|
||||
// representing the type of struct field named `field`.
|
||||
// Must be called before Where if referencing other
|
||||
// structs/types to avoid errors
|
||||
func (q *Query) Join(field string) *Query {
|
||||
var clauses []string
|
||||
parts := strings.Split(field, ".")
|
||||
@ -208,6 +225,8 @@ func (q *Query) processWheres(cond string, exprKind string, args ...any) {
|
||||
q.wheres[tq] = args
|
||||
}
|
||||
|
||||
// buildSQL - aggregates the information in this Query into a pgq.SelectBuilder.
|
||||
// it returns a slice of column names as well to avoid issues with scanning
|
||||
func (q *Query) buildSQL() (cols []string, anonymousCols map[string][]string, finalSb sb.SelectBuilder, err error) {
|
||||
var inParents []any
|
||||
anonymousCols = make(map[string][]string)
|
||||
|
@ -10,6 +10,9 @@ import (
|
||||
|
||||
const PopulateAll = "~~~ALL~~~"
|
||||
|
||||
// Populate - allows you to pre-load embedded structs/slices within the current model.
|
||||
// use dots between field names to specify nested paths. use the PopulateAll constant to populate all
|
||||
// relationships non-recursively
|
||||
func (q *Query) Populate(fields ...string) *Query {
|
||||
if q.populationTree == nil {
|
||||
q.populationTree = make(map[string]any)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Find - transpiles this query into SQL and places the result in `dest`
|
||||
func (q *Query) Find(dest any) error {
|
||||
dstVal := reflect.ValueOf(dest)
|
||||
if dstVal.Kind() != reflect.Ptr {
|
||||
@ -63,10 +64,13 @@ func (q *Query) Find(dest any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save - create or update `val` in the database
|
||||
func (q *Query) Save(val any) error {
|
||||
return q.saveOrCreate(val, false)
|
||||
}
|
||||
|
||||
// Create - like Save, but hints to the query processor that you want to insert, not update.
|
||||
// useful if you're importing data and want to keep the IDs intact.
|
||||
func (q *Query) Create(val any) error {
|
||||
return q.saveOrCreate(val, true)
|
||||
}
|
||||
@ -116,6 +120,10 @@ func (q *Query) UpdateRaw(values map[string]any) (int64, error) {
|
||||
return ctag.RowsAffected(), q.tx.Commit(q.ctx)
|
||||
}
|
||||
|
||||
// Delete - delete one or more entities matching previous conditions specified
|
||||
// by methods like Where, WhereRaw, or In. will refuse to execute if no
|
||||
// conditions were specified for safety reasons. to override this, call
|
||||
// WhereRaw("true") or WhereRaw("1 = 1") before this method.
|
||||
func (q *Query) Delete() (int64, error) {
|
||||
var err error
|
||||
var subQuery sb.SelectBuilder
|
||||
|
@ -7,30 +7,39 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RelationshipType - used to distinguish a Relationship between various
|
||||
// common entity relationship types
|
||||
type RelationshipType int
|
||||
|
||||
const (
|
||||
HasOne RelationshipType = iota
|
||||
HasMany
|
||||
BelongsTo
|
||||
ManyToOne
|
||||
ManyToOne // the other side of a HasMany relationship
|
||||
ManyToMany
|
||||
)
|
||||
|
||||
// Relationship - intermediate representation of how two types
|
||||
// relate to each other. i.e., if struct A embeds struct B,
|
||||
// a Relationship will be created for those two while parsing the Model for A.
|
||||
type Relationship struct {
|
||||
Type RelationshipType
|
||||
JoinTable string
|
||||
Model *Model
|
||||
FieldName string
|
||||
Idx int
|
||||
RelatedType reflect.Type
|
||||
RelatedModel *Model
|
||||
Kind reflect.Kind // field kind (struct, slice, ...)
|
||||
m2mInverse *Relationship
|
||||
Nullable bool
|
||||
OriginalField reflect.StructField
|
||||
Type RelationshipType // the type of this relationship (see RelationshipType)
|
||||
JoinTable string // the name of the join table, if specified explicitly via struct tag, otherwise blank
|
||||
Model *Model // the primary Model which contains this relationship
|
||||
FieldName string // the name of the struct field with this relationship
|
||||
Idx int // the index of the struct field with this relationship
|
||||
RelatedType reflect.Type // the reflect.Type for the struct field named by FieldName
|
||||
RelatedModel *Model // the Model representing the type of the embedded slice/struct
|
||||
Kind reflect.Kind // field kind (struct, slice, ...)
|
||||
m2mInverse *Relationship // the "inverse" side of an explicit ManyToMany relationship
|
||||
Nullable bool // whether the foreign key for this relationship can have a nullable column
|
||||
OriginalField reflect.StructField // the original reflect.StructField object associated with this relationship
|
||||
}
|
||||
|
||||
// ComputeJoinTable - computes the name of the join table for ManyToMany relationships.
|
||||
// will return a snake_cased autogenerated name for unidirectional ManyToMany .
|
||||
// the "implicit" behavior is invoked upon one of the following conditions being met:
|
||||
// - the primary Relationship.Model
|
||||
func (r *Relationship) ComputeJoinTable() string {
|
||||
if r.JoinTable != "" {
|
||||
return r.JoinTable
|
||||
|
Loading…
x
Reference in New Issue
Block a user