add some documentation

This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2025-07-14 17:24:30 -04:00
parent d39dfb948b
commit 33c47b2aa8
Signed by: tablet
GPG Key ID: 924A5F6AF051E87C
10 changed files with 100 additions and 60 deletions

View File

@ -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{

View File

@ -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
}

View File

@ -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"`)

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

View File

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

View File

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

View File

@ -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

View File

@ -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