si

package module
v0.0.0-...-3bc6125 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2024 License: MIT Imports: 8 Imported by: 0

README

SI

SI (Secret Ingredient), is a database tool to handle query generation and relation handling. It can also be used to save models in the database. The idea is to simplify the relation handling and to work only with model structs, instead of queries.

Its syntax is heavily based on the laravel eloquent, but very simplified and typed.

Configuration

Defining a simple model
type artist struct {
    // Note: `si.Model` must be the first declared thing on the struct.
    si.Model
    
    Name string `si:"DB_COLUMN_NAME"`
}

func (a artist) GetModel() si.Model {
    return a.Model
}

func (a artist) GetTable() string {
    return "artists"
}

A model must implement the Modeler interface.

It must also embed the si.Model as the first declared field on the model.

All exported fields on the model should have a matching column in the database with the naming as snake_case(FieldName). This can be overwritten with the si-tag (DB_COLUMN_NAME in the example above). The tag can be excluded.

Define Relationships
type Album struct {
    si.Model

    Name string
    Year int
    ArtistID uuid.UUID

    // Note: must be unexported.
    artist si.RelationData[Artist] `si:FIELD_NAME`
}

func (a Album) GetModel() si.Model {
    return a.Model
}

func (a Album) GetTable() string {
    return "albums"
}

func (a Album) Artist() si.Relation[Album, Artist] {
    return si.BelongsTo[Album, Artist](a, "artist", func(a *Album) *si.RelationData[Artist] {
        return &a.artist
    })
}

A relationship is defined by two things.

  • An unexported field with the type si.RelationData[To]
  • An exported function that returns a si.Relation[From, To]

The field is only for si:s internal use and should not be used or modified in any way. To get a relation you must use the function as a query builder.

Database setup

In order to be completely agnostic about the database, si uses these interfaces for database communication. This is based on the sql.DB, but can easily be implemented with whatever you want to use. A simple example of such an implementaiton can be found in sql_wrap.go

Usage

  • si.Query[T]() is the main entry point for retrieving data from the database.

    Examples

// Get alla albums that start with the letter 'a'.
albums, err := si.Query[Album]().Where("name", "ILIKE", "a%").OrderBy("name", true).Get(db)
// Get the Artist from an Album.
artist, err := albums[0].Artist().Find(db)

// Get all Artist, with all their albums. This will only execute two queries.
artists, err := si.Query[Artist]().With(func(a Artist, r []Artist) error {
    return a.Albums().Execute(db, r)
}).Get(db)
// Get the albums from an Artist, since irs already fetched from the database, it does not require a `db`, and there can be no error.
albums := artists[0].Albums().MustFind(nil)
  • si.Save(model) is used to create or update a model, with the values upon the model. To save relations, you must update the ID column, just as a normal column. This will not change what's stored in relation field if it is already loaded.

  • If you need to debug the generated queries, or get some silent errors, you can use si.SetLogger(...). This logger will be called with all the queries and their arguments that si generates, and might in some cases give some debugging messages.

    This example will print all queries.

si.SetLogger(func(a ...any) {
    fmt.Println(a...)
})

Example and tests

There are integration tests for all major functionalities in a separate repo The tests are put there, in its own repository, because I don't want the library itself to import packages that are only needed for the testing.

These are also a good example for how to use the library.

Comments
  • There is no mapping for a many-to-many relation yet. In order to achieve this anyway, you can make a model for the pivot-table and use the one-to-many relation in both directions to the other models.

Documentation

Index

Constants

View Source
const (
	INNER JoinType = "INNER"
	LEFT           = "LEFT"
	RIGHT          = "RIGHT"
	FULL           = "FULL"
)

Variables

View Source
var (
	ResourceNotFoundError = errors.New("resource not found")
)

Functions

func Save

func Save[T Modeler](db DB, m *T) error

Save a model to the database.

func SetLogger

func SetLogger(f func(a ...any))

SetLogger will set a logger function for debugging all queries.

func UseDeletedAt

func UseDeletedAt(enabled bool)

UseDeletedAt can disable or enable the usage of deleted_at in query generations.

Types

type DB

type DB interface {
	Query(query string, args ...any) (Rows, error)
	Exec(query string, args ...any) (any, error)
}

DB is based on `sql.DB`, but generalized with an implementation independent version of `Rows`.

func WrapDB

func WrapDB(db SqlDB) DB

type DBWrap

type DBWrap struct {
	// contains filtered or unexported fields
}

func (*DBWrap) Exec

func (db *DBWrap) Exec(query string, args ...any) (any, error)

func (*DBWrap) Query

func (db *DBWrap) Query(query string, args ...any) (Rows, error)

type JoinConf

type JoinConf struct {
	JoinType JoinType
	Table    string
	Alias    string
	// Extra condition?
	Condition []filter // func(q *Q[T]) *Q[T]
}

type JoinType

type JoinType string

type Model

type Model struct {
	ID        *uuid.UUID `si:"id"`
	CreatedAt time.Time  `si:"created_at"`
	UpdatedAt *time.Time `si:"updated_at"`
	DeletedAt *time.Time `si:"deleted_at"`
}

type ModelConfig

type ModelConfig[T Modeler] struct {
	Table string
}

type Modeler

type Modeler interface {
	GetModel() Model
	GetTable() string
}

type Q

type Q[T Modeler] struct {
	// contains filtered or unexported fields
}

func Query

func Query[T Modeler]() *Q[T]

Query will start a query. Main starting point for retrieving objects.

func (*Q[T]) Find

func (q *Q[T]) Find(db DB, id ...uuid.UUID) (*T, error)

Find will return the one element in the query result. This will be successful IFF there was one result. The variadic parameter `id` is used to make it optional. If present, only the first element is used.

func (*Q[T]) First

func (q *Q[T]) First(db DB) (*T, error)

First will execute the query and return the first element of the result

func (*Q[T]) Get

func (q *Q[T]) Get(db DB) ([]T, error)

Get will Execute the query and return a list of the result.

func (*Q[T]) GroupBy

func (q *Q[T]) GroupBy(field string) *Q[T]

func (*Q[T]) Having

func (q *Q[T]) Having(column, op string, value any) *Q[T]

func (*Q[T]) HavingF

func (q *Q[T]) HavingF(f func(q *Q[T]) *Q[T]) *Q[T]

func (*Q[T]) Join

func (q *Q[T]) Join(f func(t T) *JoinConf) *Q[T]

Join adds a join on the query. Can be used with `join` a `Relation` to automate the condition.

func (*Q[T]) MustFind

func (q *Q[T]) MustFind(db DB, id ...uuid.UUID) *T

MustFind is same as Find, but will panic on error.

func (*Q[T]) MustFirst

func (q *Q[T]) MustFirst(db DB) *T

MustFirst is same as First, but will panic on error.

func (*Q[T]) MustGet

func (q *Q[T]) MustGet(db DB) []T

MustGet is same as Get, but will panic on error.

func (*Q[T]) OrHaving

func (q *Q[T]) OrHaving(column, op string, value any) *Q[T]

func (*Q[T]) OrHavingF

func (q *Q[T]) OrHavingF(f func(q *Q[T]) *Q[T]) *Q[T]

func (*Q[T]) OrWhere

func (q *Q[T]) OrWhere(column, op string, value any) *Q[T]

OrWhere adds a condition, separated by `OR`

func (*Q[T]) OrWhereF

func (q *Q[T]) OrWhereF(f func(q *Q[T]) *Q[T]) *Q[T]

OrWhereF add a condition in parentheses, separated by `OR`

func (*Q[T]) OrderBy

func (q *Q[T]) OrderBy(column string, asc bool) *Q[T]

OrderBy adds an order to the query.

func (*Q[T]) Select

func (q *Q[T]) Select(selects []string, selectScan func(scan func(...any))) *Q[T]

func (*Q[T]) Skip

func (q *Q[T]) Skip(number int) *Q[T]

Skip will remove the first `number`of the result.

func (*Q[T]) Take

func (q *Q[T]) Take(number int) *Q[T]

Take will limit the result to the given number.

func (*Q[T]) Where

func (q *Q[T]) Where(column, op string, value any) *Q[T]

Where adds a condition, separated by `AND`

func (*Q[T]) WhereF

func (q *Q[T]) WhereF(f func(q *Q[T]) *Q[T]) *Q[T]

WhereF add a condition in parentheses, separated by `AND`

func (*Q[T]) With

func (q *Q[T]) With(f func(m T, r []T) error) *Q[T]

With will retrieve a relation, while getting the main object(s).

func (*Q[T]) WithDeleted

func (q *Q[T]) WithDeleted() *Q[T]

WithDeleted will ignore the deleted timestamp.

type Raw

type Raw string

type Relation

type Relation[F, T Modeler] struct {
	// contains filtered or unexported fields
}

Relation is the configuration for a relationship between two objects. F is `from` and T is `to`, so the relation is defined as seen from ´F´.

func BelongsTo

func BelongsTo[F, T Modeler](model F, refFieldName, fieldName string, relationDataFunc func(f *F) *RelationData[T]) *Relation[F, T]

BelongsTo is a relationship where the object in question (F) has a reference to the other object (T) Example: | F | | T | |------| |----| | ID | | | | T_ID | --> | ID |

func HasMany

func HasMany[F, T Modeler](model F, refFieldName, fieldName string, relationDataFunc func(f *F) *RelationData[T]) *Relation[F, T]

HasMany is a relationship where there are MULTIPLE other objects (T) that points to this one (F). Example: | F | | T | |------| |------| | | | ID | | ID | <-- | F_ID |

func HasOne

func HasOne[F, T Modeler](model F, refFieldName, fieldName string, relationDataFunc func(f *F) *RelationData[T]) *Relation[F, T]

HasOne is a relationship where there are ONE other objects (T) that points to this one (F). Example: | F | | T | |------| |------| | | | ID | | ID | <-- | F_ID |

func (*Relation[F, T]) Execute

func (r *Relation[F, T]) Execute(db DB, result []F) error

func (*Relation[F, T]) Find

func (r *Relation[F, T]) Find(db DB, id ...uuid.UUID) (*T, error)

func (*Relation[F, T]) First

func (r *Relation[F, T]) First(db DB) (*T, error)

func (*Relation[F, T]) Get

func (r *Relation[F, T]) Get(db DB) ([]T, error)

func (*Relation[F, T]) Join

func (r *Relation[F, T]) Join(joinType JoinType) *JoinConf

func (*Relation[F, T]) MustFind

func (r *Relation[F, T]) MustFind(db DB, id ...uuid.UUID) *T

func (*Relation[F, T]) MustFirst

func (r *Relation[F, T]) MustFirst(db DB) *T

func (*Relation[F, T]) MustGet

func (r *Relation[F, T]) MustGet(db DB) []T

func (*Relation[F, T]) OrWhere

func (r *Relation[F, T]) OrWhere(column, op string, value any) *Relation[F, T]

func (*Relation[F, T]) OrWhereF

func (r *Relation[F, T]) OrWhereF(f func(q *Q[T]) *Q[T]) *Relation[F, T]

func (*Relation[F, T]) OrderBy

func (r *Relation[F, T]) OrderBy(column string, asc bool) *Relation[F, T]

func (*Relation[F, T]) Skip

func (r *Relation[F, T]) Skip(number int) *Relation[F, T]

func (*Relation[F, T]) Take

func (r *Relation[F, T]) Take(number int) *Relation[F, T]

func (*Relation[F, T]) Unload

func (r *Relation[F, T]) Unload() *Relation[F, T]

func (*Relation[F, T]) Where

func (r *Relation[F, T]) Where(column, op string, value any) *Relation[F, T]

func (*Relation[F, T]) WhereF

func (r *Relation[F, T]) WhereF(f func(q *Q[T]) *Q[T]) *Relation[F, T]

func (*Relation[F, T]) With

func (r *Relation[F, T]) With(f func(m T, r []T) error) *Relation[F, T]

func (*Relation[F, T]) WithDeleted

func (r *Relation[F, T]) WithDeleted() *Relation[F, T]

type RelationData

type RelationData[T Modeler] struct {
	// contains filtered or unexported fields
}

type Rows

type Rows interface {
	Next() bool
	Scan(dest ...any) error
	Close() error
}

type RowsWrap

type RowsWrap struct {
	// contains filtered or unexported fields
}

func (*RowsWrap) Close

func (w *RowsWrap) Close() error

func (*RowsWrap) Next

func (w *RowsWrap) Next() bool

func (*RowsWrap) Scan

func (w *RowsWrap) Scan(a ...any) error

type SqlDB

type SqlDB interface {
	Query(query string, args ...any) (*sql.Rows, error)
	Exec(query string, args ...any) (sql.Result, error)
}

SqlDB is implemented by both `sql.DB` and `sql.Tx`.

Jump to

Keyboard shortcuts

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