├── LICENSE ├── README.md ├── dbtx.go ├── drivers ├── repo.pgx │ ├── dbtx.go │ ├── go.mod │ ├── go.sum │ ├── repo.go │ └── rows.go └── repo.sql │ ├── dbtx.go │ ├── driver.go │ ├── go.mod │ └── repo.go ├── errors.go ├── example ├── example.go ├── go.mod ├── go.sum └── models │ ├── post_model.go │ └── user_model.go ├── find.go ├── find_opts.go ├── go.mod ├── go.sum ├── model ├── columns.go ├── definition.go ├── model.go ├── op.go └── relation.go ├── queries ├── expressions.go ├── insert.go ├── joins.go ├── params.go ├── query.go ├── select.go ├── update.go └── where.go └── repository.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Lucas JACQUES 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MODB 2 | 3 | MODB is an experimental ORM for GO which doesn't require code generation or reflection. 4 | 5 | ## Defining models 6 | 7 | Models are defined declaratively: 8 | 9 | ```go 10 | type ( 11 | User struct { 12 | Id int 13 | Name string 14 | Age int 15 | Posts []Post 16 | } 17 | 18 | userColumns struct { 19 | Id model.TypedCol[User, int] 20 | Name model.TypedCol[User, string] 21 | Age model.TypedCol[User, int] 22 | } 23 | 24 | userRelations struct { 25 | Posts model.Relation 26 | } 27 | ) 28 | 29 | var ( 30 | UserModel = model.Define(model.ModelDefinition[User, userColumns, int]{ 31 | Table: "users", 32 | Schema: userColumns{ 33 | Id: model.AutoIncrement("id", func(u *User) *int { return &u.Id }), 34 | Name: model.Col("name", func(u *User) *string { return &u.Name }), 35 | Age: model.Col("age", func(u *User) *int { return &u.Age }), 36 | }, 37 | PK: func(s userColumns) model.TypedCol[User, int] { 38 | return s.Id 39 | }, 40 | }) 41 | 42 | UserRelations = &userRelations{ 43 | Posts: model.HasMany(UserModel.Cols().Id, PostModel.Cols().UserId, func(u *User) *[]Post { return &u.Posts }), 44 | } 45 | ) 46 | ``` 47 | 48 | 49 | ## Querying 50 | MODB use the repository pattern. From your model definition and your db (or a transaction) you can create a repository object. The repository is a generic object: 51 | 52 | ```go 53 | userRepo := repo.New(db, models.UserModel) 54 | postRepo := repo.New(db, models.PostModel) 55 | ``` 56 | 57 | 58 | Then you can call the repository methods. These methods are type-safe and inherit their types from the provided model: 59 | ```go 60 | err = userRepo.Insert(ctx, &models.User{ 61 | Name: "Lucas", 62 | Age: 25, 63 | }) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | err = postRepo.Insert(ctx, &models.Post{ 69 | UserId: 1, 70 | Title: "Hello", 71 | Body: "World", 72 | }) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | err = postRepo.Insert(ctx, &models.Post{ 78 | UserId: 1, 79 | Title: "Goodbye", 80 | Body: "World", 81 | }) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | user, err := userRepo.FindById(ctx, 1, repo.Preload(models.UserRelations.Posts)) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | for _, post := range user.Posts { 92 | println(post.Title) 93 | } 94 | ``` 95 | 96 | Output: 97 | ``` 98 | User: 99 | Lucas 100 | 25 101 | Posts: 102 | Hello 103 | Goodbye 104 | ``` 105 | 106 | 107 | > You can find a full example in the [example](example) folder. 108 | 109 | 110 | ## License 111 | 112 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /dbtx.go: -------------------------------------------------------------------------------- 1 | package modb 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/lucasjacques/modb/queries" 7 | ) 8 | 9 | type Scannable interface { 10 | Scan(dest ...any) error 11 | } 12 | 13 | type DBTX interface { 14 | Exec(ctx context.Context, query string, args ...any) (CommandTag, error) 15 | Query(ctx context.Context, query string, args ...any) (Rows, error) 16 | NewParamsSet() queries.ParamsSet 17 | } 18 | 19 | type CommandTag interface { 20 | RowsAffected() (int64, error) 21 | } 22 | 23 | type Rows interface { 24 | Scannable 25 | Err() error 26 | Close() error 27 | Next() bool 28 | } 29 | 30 | type Row interface { 31 | Scannable 32 | Err() error 33 | } 34 | -------------------------------------------------------------------------------- /drivers/repo.pgx/dbtx.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgconn" 8 | "github.com/lucasjacques/modb" 9 | "github.com/lucasjacques/modb/queries" 10 | ) 11 | 12 | type pgxdbtx interface { 13 | Exec(ctx context.Context, query string, args ...any) (pgconn.CommandTag, error) 14 | Query(ctx context.Context, query string, args ...any) (pgx.Rows, error) 15 | } 16 | 17 | type dbtx struct { 18 | conn pgxdbtx 19 | } 20 | 21 | func (d *dbtx) NewParamsSet() queries.ParamsSet { 22 | return &queries.Numbered{} 23 | } 24 | 25 | func (c *cmdTag) RowsAffected() (int64, error) { 26 | return c.tag.RowsAffected(), nil 27 | } 28 | 29 | type cmdTag struct { 30 | tag pgconn.CommandTag 31 | } 32 | 33 | func (d *dbtx) Exec(ctx context.Context, query string, args ...any) (modb.CommandTag, error) { 34 | tag, err := d.conn.Exec(ctx, query, args...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &cmdTag{tag: tag}, nil 40 | } 41 | 42 | func (d *dbtx) Query(ctx context.Context, query string, args ...any) (modb.Rows, error) { 43 | r, err := d.conn.Query(ctx, query, args...) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &rows{rows: r}, nil 49 | } 50 | 51 | func (d *dbtx) FQCN(table, column string) string { 52 | return `"` + table + `"."` + column + `"` 53 | } 54 | 55 | var _ modb.DBTX = (*dbtx)(nil) 56 | -------------------------------------------------------------------------------- /drivers/repo.pgx/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasjacques/modb/drivers/repo.pgx 2 | 3 | go 1.23.2 4 | 5 | replace github.com/lucasjacques/modb => ../.. 6 | 7 | require ( 8 | github.com/jackc/pgx/v5 v5.7.2 9 | github.com/lucasjacques/modb v0.0.0-00010101000000-000000000000 10 | ) 11 | 12 | require ( 13 | github.com/jackc/pgpassfile v1.0.0 // indirect 14 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 15 | golang.org/x/crypto v0.31.0 // indirect 16 | golang.org/x/text v0.21.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /drivers/repo.pgx/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 5 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 6 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 7 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 8 | github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= 9 | github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 10 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 11 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 18 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 19 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 20 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 21 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 22 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 23 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 24 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /drivers/repo.pgx/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "github.com/lucasjacques/modb" 5 | "github.com/lucasjacques/modb/model" 6 | ) 7 | 8 | func wrapDBTX(conn pgxdbtx) modb.DBTX { 9 | return &dbtx{ 10 | conn: conn, 11 | } 12 | } 13 | 14 | func Repo[M any, PK comparable, C model.Schema[M]](dbtx pgxdbtx, model model.TypedModelCols[M, PK, C]) *modb.ModelRepository[M, PK, C] { 15 | return modb.NewRepository(wrapDBTX(dbtx), model) 16 | } 17 | -------------------------------------------------------------------------------- /drivers/repo.pgx/rows.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/jackc/pgx/v5" 4 | 5 | type rows struct { 6 | rows pgx.Rows 7 | } 8 | 9 | // Err implements modb.Rows. 10 | func (r *rows) Err() error { 11 | return r.rows.Err() 12 | } 13 | 14 | func (r *rows) Close() error { 15 | r.rows.Close() 16 | return nil 17 | } 18 | 19 | func (r *rows) Next() bool { 20 | return r.rows.Next() 21 | } 22 | 23 | func (r *rows) Scan(dest ...any) error { 24 | return r.rows.Scan(dest...) 25 | } 26 | -------------------------------------------------------------------------------- /drivers/repo.sql/dbtx.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/lucasjacques/modb" 7 | "github.com/lucasjacques/modb/queries" 8 | ) 9 | 10 | type dbtx struct { 11 | c stddbtx 12 | } 13 | 14 | var _ modb.DBTX = (*dbtx)(nil) 15 | 16 | func (d *dbtx) Exec(ctx context.Context, query string, args ...any) (modb.CommandTag, error) { 17 | return d.c.ExecContext(ctx, query, args...) 18 | } 19 | 20 | func (d *dbtx) Query(ctx context.Context, query string, args ...any) (modb.Rows, error) { 21 | return d.c.QueryContext(ctx, query, args...) 22 | } 23 | 24 | func (d *dbtx) NewParamsSet() queries.ParamsSet { 25 | return &queries.QuestionMark{} 26 | } 27 | 28 | func (d *dbtx) FQCN(table, column string) string { 29 | return `"` + table + `"."` + column + `"` 30 | } 31 | 32 | func wrapDBTX(conn stddbtx) modb.DBTX { 33 | return &dbtx{ 34 | c: conn, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /drivers/repo.sql/driver.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | type stddbtx interface { 9 | ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 10 | QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 11 | } 12 | -------------------------------------------------------------------------------- /drivers/repo.sql/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasjacques/modb/drivers/sqlrepo 2 | 3 | go 1.23.2 4 | 5 | replace github.com/lucasjacques/modb => ../.. 6 | 7 | require github.com/lucasjacques/modb v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /drivers/repo.sql/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "github.com/lucasjacques/modb" 5 | "github.com/lucasjacques/modb/model" 6 | ) 7 | 8 | func New[M any, PK comparable, C any](dbtx stddbtx, model model.TypedModelCols[M, PK, C]) *modb.ModelRepository[M, PK, C] { 9 | return modb.NewRepository(wrapDBTX(dbtx), model) 10 | } 11 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package modb 2 | 3 | import "errors" 4 | 5 | var ErrNotFound = errors.New("not found") 6 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "modbexample/models" 8 | 9 | "github.com/lucasjacques/modb" 10 | "github.com/lucasjacques/modb/drivers/repo.sql" 11 | _ "modernc.org/sqlite" 12 | ) 13 | 14 | func main() { 15 | db, err := sql.Open("sqlite", ":memory:") 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | _, err = db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | _, err = db.Exec("CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT, body TEXT);") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | ctx := context.Background() 31 | 32 | userRepo := repo.New(db, models.UserModel) 33 | postRepo := repo.New(db, models.PostModel) 34 | 35 | err = userRepo.Insert(ctx, &models.User{ 36 | Name: "Lucas", 37 | Age: 25, 38 | }) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | err = postRepo.Insert(ctx, &models.Post{ 44 | UserId: 1, 45 | Title: "Hello", 46 | Body: "World", 47 | }) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | err = postRepo.Insert(ctx, &models.Post{ 53 | UserId: 1, 54 | Title: "Goodbye", 55 | Body: "World", 56 | }) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | user, err := userRepo.FindById(ctx, 1, modb.Preload(models.UserRelations.Posts)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | fmt.Println("User:") 67 | fmt.Println(user.Name) 68 | fmt.Println(user.Age) 69 | fmt.Println("Posts:") 70 | 71 | for _, post := range user.Posts { 72 | fmt.Println(post.Title) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module modbexample 2 | 3 | go 1.23.2 4 | 5 | replace github.com/lucasjacques/modb => .. 6 | 7 | replace github.com/lucasjacques/modb/drivers/repo.sql => ../drivers/repo.sql 8 | 9 | require ( 10 | github.com/lucasjacques/modb v0.0.0-00010101000000-000000000000 11 | github.com/lucasjacques/modb/drivers/repo.sql v0.0.0-00010101000000-000000000000 12 | modernc.org/sqlite v1.34.5 13 | ) 14 | 15 | require ( 16 | github.com/dustin/go-humanize v1.0.1 // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/ncruces/go-strftime v0.1.9 // indirect 20 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 21 | golang.org/x/sys v0.22.0 // indirect 22 | modernc.org/libc v1.55.3 // indirect 23 | modernc.org/mathutil v1.6.0 // indirect 24 | modernc.org/memory v1.8.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 2 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 3 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 4 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 8 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 9 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 10 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 11 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 12 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 13 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 14 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 15 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 17 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 18 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 19 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 20 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= 21 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 22 | modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= 23 | modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= 24 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 25 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 26 | modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= 27 | modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 28 | modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= 29 | modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= 30 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 31 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 32 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 33 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 34 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 35 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 36 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 37 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 38 | modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= 39 | modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= 40 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 41 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 42 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 43 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 44 | -------------------------------------------------------------------------------- /example/models/post_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/lucasjacques/modb/model" 5 | ) 6 | 7 | type ( 8 | Post struct { 9 | Id int 10 | UserId int 11 | Title string 12 | Body string 13 | User User 14 | } 15 | 16 | postSchema struct { 17 | Id model.TypedCol[Post, int] 18 | UserId model.TypedCol[Post, int] 19 | Title model.TypedCol[Post, string] 20 | Body model.TypedCol[Post, string] 21 | } 22 | 23 | postRelations struct { 24 | User model.Relation 25 | } 26 | ) 27 | 28 | var ( 29 | PostModel = model.Define(model.ModelDefinition[Post, postSchema, int]{ 30 | Table: "posts", 31 | Schema: postSchema{ 32 | Id: model.AutoIncrement("id", func(p *Post) *int { return &p.Id }), 33 | UserId: model.Col("user_id", func(p *Post) *int { return &p.UserId }), 34 | Title: model.Col("title", func(p *Post) *string { return &p.Title }), 35 | Body: model.Col("body", func(p *Post) *string { return &p.Body }), 36 | }, 37 | PK: func(s postSchema) model.TypedCol[Post, int] { 38 | return s.Id 39 | }, 40 | }) 41 | 42 | PostRelations = &postRelations{ 43 | User: model.BelongsTo(PostModel.Cols().UserId, UserModel.Cols().Id, func(p *Post) *User { return &p.User }), 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /example/models/user_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/lucasjacques/modb/model" 5 | ) 6 | 7 | type ( 8 | User struct { 9 | Id int 10 | Name string 11 | Age int 12 | Posts []Post 13 | } 14 | 15 | userColumns struct { 16 | Id model.TypedCol[User, int] 17 | Name model.TypedCol[User, string] 18 | Age model.TypedCol[User, int] 19 | } 20 | 21 | userRelations struct { 22 | Posts model.Relation 23 | } 24 | ) 25 | 26 | var ( 27 | UserModel = model.Define(model.ModelDefinition[User, userColumns, int]{ 28 | Table: "users", 29 | Schema: userColumns{ 30 | Id: model.AutoIncrement("id", func(u *User) *int { return &u.Id }), 31 | Name: model.Col("name", func(u *User) *string { return &u.Name }), 32 | Age: model.Col("age", func(u *User) *int { return &u.Age }), 33 | }, 34 | PK: func(s userColumns) model.TypedCol[User, int] { 35 | return s.Id 36 | }, 37 | }) 38 | 39 | UserRelations = &userRelations{ 40 | Posts: model.HasMany(UserModel.Cols().Id, PostModel.Cols().UserId, func(u *User) *[]Post { return &u.Posts }), 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /find.go: -------------------------------------------------------------------------------- 1 | package modb 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/lucasjacques/modb/model" 7 | "github.com/lucasjacques/modb/queries" 8 | ) 9 | 10 | func (r *ModelRepository[M, PK, C]) getColumns(columns []model.Column) []string { 11 | cols := make([]string, 0, len(columns)) 12 | for _, col := range columns { 13 | cols = append(cols, col.FQCN()) 14 | } 15 | return cols 16 | } 17 | 18 | type queryOptions struct { 19 | limit int 20 | load []model.Relation 21 | where queries.Expr 22 | } 23 | 24 | func (m *ModelRepository[M, PK, C]) query(ctx context.Context, opts *queryOptions) ([]M, error) { 25 | table, columns := m.model.GetTable(), m.getColumns(m.model.GetColumns()) 26 | var joins []*queries.Join 27 | var eagerLoads []model.OneToOne 28 | var prefetches []model.OneToMany 29 | for _, rel := range opts.load { 30 | switch r := rel.(type) { 31 | case model.OneToOne: 32 | eagerLoads = append(eagerLoads, r) 33 | case model.OneToMany: 34 | prefetches = append(prefetches, r) 35 | } 36 | } 37 | 38 | for _, eagerLoad := range eagerLoads { 39 | eagerModel := eagerLoad.ForeignDef() 40 | eagerTable, eagerCols := eagerModel.GetTable(), m.getColumns(eagerModel.GetColumns()) 41 | join := queries.InnerJoin(eagerTable).On(queries.EQ(eagerLoad.ForeignKey(), eagerLoad.LocalKey())) 42 | joins = append(joins, join) 43 | columns = append(columns, eagerCols...) 44 | } 45 | 46 | query := queries.NewQuery(m.db.NewParamsSet()).Select(table, columns...) 47 | for _, join := range joins { 48 | query = query.Join(join) 49 | } 50 | 51 | if opts.where != nil { 52 | query = query.Where(opts.where) 53 | } 54 | 55 | if opts.limit > 0 { 56 | query = query.Limit(opts.limit) 57 | } 58 | 59 | str, values := query.Build() 60 | 61 | rows, err := m.db.Query(ctx, str, values...) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | defer rows.Close() 67 | 68 | var models []M 69 | for rows.Next() { 70 | var dests []any 71 | modelDests := m.model.NewDests() 72 | dests = append(dests, modelDests...) 73 | 74 | var eagerDests [][]any 75 | for _, eagerLoad := range eagerLoads { 76 | dest := eagerLoad.ForeignDef().NewDests() 77 | dests = append(dests, dest...) 78 | eagerDests = append(eagerDests, dest) 79 | } 80 | 81 | err := rows.Scan(dests...) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | model, err := m.model.FromDestsTyped(modelDests) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | for i, eagerLoad := range eagerLoads { 92 | eagerModel, err := eagerLoad.ForeignDef().FromDests(eagerDests[i]) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | eagerLoad.Set(model, eagerModel) 98 | } 99 | 100 | models = append(models, *model) 101 | } 102 | 103 | err = m.makePrefetches(ctx, models, prefetches) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return models, nil 109 | } 110 | 111 | func multiValuesExpr(values []any) []queries.Expr { 112 | exprs := make([]queries.Expr, 0, len(values)) 113 | for _, value := range values { 114 | exprs = append(exprs, queries.Value(value)) 115 | } 116 | 117 | return exprs 118 | } 119 | 120 | func (m *ModelRepository[M, PK, S]) makePrefetches(ctx context.Context, results []M, prefetches []model.OneToMany) error { 121 | for _, prefetch := range prefetches { 122 | var invals []any 123 | for _, model := range results { 124 | value, err := prefetch.LocalKey().ValueFromModel(&model) 125 | if err != nil { 126 | return err 127 | } 128 | invals = append(invals, value) 129 | } 130 | 131 | prefetchQuery := queries.NewQuery(m.db.NewParamsSet()). 132 | Select(prefetch.ForeignDef().GetTable(), m.getColumns(prefetch.ForeignDef().GetColumns())...). 133 | Where(queries.IN(prefetch.ForeignKey(), multiValuesExpr(invals))) 134 | 135 | str, values := prefetchQuery.Build() 136 | 137 | rows, err := m.db.Query(ctx, str, values...) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | defer rows.Close() 143 | 144 | var prefetchModels []any 145 | for rows.Next() { 146 | dests := prefetch.ForeignDef().NewDests() 147 | err := rows.Scan(dests...) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | prefetchModel, err := prefetch.ForeignDef().FromDests(dests) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | prefetchModels = append(prefetchModels, prefetchModel) 158 | } 159 | prefetchIndex := 0 160 | for i := range results { 161 | fk, err := prefetch.LocalKey().ValueFromModel(&results[i]) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | for prefetchIndex < len(prefetchModels) { 167 | prefetchModel := prefetchModels[prefetchIndex] 168 | prefetchFk, err := prefetch.ForeignKey().ValueFromModel(prefetchModel) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | if fk == prefetchFk { 174 | prefetch.Append(&results[i], prefetchModel) 175 | prefetchIndex++ 176 | } else { 177 | break 178 | } 179 | 180 | } 181 | 182 | } 183 | 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func (m *ModelRepository[M, PK, S]) FindById(ctx context.Context, id PK, opts ...FindOpt) (*M, error) { 190 | findOps := buildFindOpts(opts) 191 | findOps.where = queries.EQ(m.model.PrimaryKey(), queries.Value(id)) 192 | results, err := m.query(ctx, findOps) 193 | 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | if len(results) == 0 { 199 | return nil, ErrNotFound 200 | } 201 | 202 | return &results[0], nil 203 | } 204 | 205 | func (m *ModelRepository[M, PK, S]) Find(ctx context.Context, opts ...FindOpt) ([]M, error) { 206 | findOps := buildFindOpts(opts) 207 | return m.query(ctx, findOps) 208 | } 209 | 210 | func (m *ModelRepository[M, PK, C]) FindOne(ctx context.Context, opts ...FindOpt) (*M, error) { 211 | findOps := buildFindOpts(opts) 212 | 213 | findOps.limit = 1 214 | 215 | results, err := m.query(ctx, findOps) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | if len(results) == 0 { 221 | return nil, ErrNotFound 222 | } 223 | 224 | return &results[0], nil 225 | } 226 | -------------------------------------------------------------------------------- /find_opts.go: -------------------------------------------------------------------------------- 1 | package modb 2 | 3 | import ( 4 | "github.com/lucasjacques/modb/model" 5 | "github.com/lucasjacques/modb/queries" 6 | ) 7 | 8 | func buildFindOpts(opts []FindOpt) *queryOptions { 9 | var findOpts queryOptions 10 | for _, opt := range opts { 11 | opt(&findOpts) 12 | } 13 | return &findOpts 14 | } 15 | 16 | type FindOpt func(*queryOptions) 17 | 18 | func Preload(r model.Relation) FindOpt { 19 | return func(opts *queryOptions) { 20 | opts.load = append(opts.load, r) 21 | } 22 | } 23 | 24 | func Limit(limit int) FindOpt { 25 | return func(opts *queryOptions) { 26 | opts.limit = limit 27 | } 28 | } 29 | 30 | func Where(expr queries.Expr) FindOpt { 31 | return func(opts *queryOptions) { 32 | opts.where = expr 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasjacques/modb 2 | 3 | go 1.23.2 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucas-jacques/modb/a69ed036704d89780a277dfad9f4a0ad73879f23/go.sum -------------------------------------------------------------------------------- /model/columns.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/lucasjacques/modb/queries" 9 | ) 10 | 11 | type column[M any, V any] struct { 12 | table string 13 | name string 14 | innerModel TypedModel[M] 15 | ref func(*M) *V 16 | omitOnInsert bool 17 | omitOnUpdate bool 18 | } 19 | 20 | // New implements TypedCol. 21 | func (c *column[M, V]) New() *V { 22 | return new(V) 23 | } 24 | 25 | // ShouldOmit implements TypedCol. 26 | func (c *column[M, V]) ShouldOmit(m any, op Operation) bool { 27 | switch op { 28 | case OpInsert: 29 | return c.omitOnInsert 30 | case OpUpdate: 31 | return c.omitOnUpdate 32 | default: 33 | return false 34 | } 35 | } 36 | 37 | var _ TypedCol[any, any] = (*column[any, any])(nil) 38 | 39 | func (c *column[M, V]) setTable(table string) { 40 | c.table = table 41 | } 42 | 43 | func (c *column[M, V]) SetInnerModel(innerModel TypedModel[M]) { 44 | c.innerModel = innerModel 45 | } 46 | 47 | func (c *column[M, V]) GetInnerModel() TypedModel[M] { 48 | return c.innerModel 49 | } 50 | 51 | func (c *column[M, V]) ForDest(m any) (any, error) { 52 | model, ok := m.(*M) 53 | if !ok { 54 | return nil, ErrWrongModelType 55 | } 56 | 57 | return c.ref(model), nil 58 | } 59 | 60 | // Implement modb.Column[M, V] 61 | 62 | func (c *column[M, V]) GetTable() string { 63 | return c.table 64 | } 65 | 66 | func (c *column[M, V]) GetName() string { 67 | return c.name 68 | } 69 | 70 | func (c *column[M, V]) GetOmitOnInsert() bool { 71 | return c.omitOnInsert 72 | } 73 | 74 | func (c *column[M, V]) GetOmitOnUpdate() bool { 75 | return c.omitOnUpdate 76 | } 77 | 78 | func (c *column[M, V]) NewDest() any { 79 | return new(V) 80 | } 81 | 82 | func (c *column[M, V]) SetValueOnModel(m any, v any) error { 83 | ptr, ok := v.(*V) 84 | if !ok { 85 | return ErrWrongValueType 86 | } 87 | 88 | model, ok := m.(*M) 89 | if !ok { 90 | return ErrWrongModelType 91 | } 92 | 93 | ref := c.ref(model) 94 | if ref == nil { 95 | return errors.New("invalid model") 96 | } 97 | 98 | if ptr == nil { 99 | return nil 100 | } 101 | 102 | *ref = *ptr 103 | return nil 104 | } 105 | 106 | var ErrWrongValueType = fmt.Errorf("invalid value type") 107 | 108 | var ErrWrongModelType = fmt.Errorf("invalid model type") 109 | 110 | func (c *column[M, V]) ValueFromModel(m any) (any, error) { 111 | model, ok := m.(*M) 112 | if !ok { 113 | fmt.Println("model", reflect.TypeOf(m).String()) 114 | return nil, ErrWrongModelType 115 | } 116 | 117 | return *c.ref(model), nil 118 | } 119 | 120 | func (c *column[M, V]) ValueFromModelTyped(m *M) (any, error) { 121 | return *c.ref(m), nil 122 | } 123 | 124 | func (c *column[M, V]) GetValue(m *M) (V, error) { 125 | return *c.ref(m), nil 126 | } 127 | 128 | // Options 129 | 130 | func (c *column[M, V]) OmitOnInsert() *column[M, V] { 131 | c.omitOnInsert = true 132 | return c 133 | } 134 | 135 | func (c *column[M, V]) OmitOnUpdate() *column[M, V] { 136 | c.omitOnUpdate = true 137 | return c 138 | } 139 | 140 | func (c *column[M, V]) FQCN() string { 141 | return `"` + c.table + `".` + `"` + c.name + `"` 142 | } 143 | 144 | func (c *column[M, V]) Build(queries.ParamsSet) (string, []any) { 145 | return c.FQCN(), nil 146 | } 147 | 148 | func Col[M, V any](name string, ref func(*M) *V) *column[M, V] { 149 | return &column[M, V]{name: name, ref: ref} 150 | } 151 | 152 | func PrimaryKey[M any, V any](name string, ref func(*M) *V) *column[M, V] { 153 | return &column[M, V]{ 154 | name: name, 155 | ref: ref, 156 | omitOnUpdate: true, 157 | } 158 | } 159 | 160 | type autoincrement interface { 161 | ~int | int64 | ~uint | ~uint64 | ~int32 | ~uint32 162 | } 163 | 164 | func AutoIncrement[M any, V autoincrement](name string, ref func(*M) *V) *column[M, V] { 165 | return &column[M, V]{ 166 | name: name, 167 | ref: ref, 168 | omitOnInsert: true, 169 | omitOnUpdate: true, 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /model/definition.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type ModelDefinition[Model any, Schema any, PK comparable] struct { 8 | Table string 9 | Schema Schema 10 | PK func(Schema) TypedCol[Model, PK] 11 | } 12 | 13 | func Define[M any, PK comparable, S any](def ModelDefinition[M, S, PK]) TypedModelCols[M, PK, S] { 14 | m := &model[M, PK, S]{ 15 | table: def.Table, 16 | primaryKey: def.PK(def.Schema), 17 | schema: def.Schema, 18 | } 19 | 20 | schemaType := reflect.TypeOf(def.Schema) 21 | schemaValue := reflect.ValueOf(def.Schema) 22 | 23 | if schemaType.Kind() != reflect.Struct { 24 | panic("Schema must be a struct") 25 | } 26 | 27 | columns := make([]Column, schemaType.NumField()) 28 | for i := 0; i < schemaType.NumField(); i++ { 29 | field := schemaType.Field(i) 30 | // check if the field implement Column 31 | if !field.Type.Implements(reflect.TypeFor[ModelCol[M]]()) { 32 | continue 33 | } 34 | column := schemaValue.Field(i).Interface().(ModelCol[M]) 35 | column.SetInnerModel(m) 36 | column.setTable(def.Table) 37 | columns[i] = column 38 | 39 | } 40 | 41 | m.columns = columns 42 | 43 | return m 44 | } 45 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/lucasjacques/modb/queries" 5 | ) 6 | 7 | type Schema[M any] interface { 8 | Cols() []ModelCol[M] 9 | } 10 | 11 | type ModelCols[M any, PK comparable] interface { 12 | Schema[M] 13 | PrimaryKey() TypedCol[M, PK] 14 | } 15 | 16 | type Column interface { 17 | GetName() string 18 | GetTable() string 19 | NewDest() any 20 | ForDest(m any) (any, error) 21 | ShouldOmit(m any, op Operation) bool 22 | SetValueOnModel(m any, v any) error 23 | ValueFromModel(m any) (any, error) 24 | FQCN() string 25 | Build(queries.ParamsSet) (string, []any) 26 | setTable(string) 27 | } 28 | 29 | type ModelCol[M any] interface { 30 | Column 31 | ValueFromModelTyped(m *M) (any, error) 32 | GetInnerModel() TypedModel[M] 33 | SetInnerModel(m TypedModel[M]) 34 | } 35 | 36 | type TypedCol[M any, V any] interface { 37 | ModelCol[M] 38 | New() *V 39 | } 40 | 41 | type ValueTypedCol[V any] interface { 42 | Column 43 | New() *V 44 | } 45 | 46 | type Model interface { 47 | GetTable() string 48 | GetPrimaryKey() Column 49 | GetColumns() []Column 50 | NewDests() []any 51 | FromDests(dests []any) (any, error) 52 | } 53 | 54 | type TypedModel[M any] interface { 55 | Model 56 | New() *M 57 | FromDestsTyped(dests []any) (*M, error) 58 | } 59 | 60 | type TypedModelCols[M any, PK comparable, C any] interface { 61 | TypedModel[M] 62 | PrimaryKey() TypedCol[M, PK] 63 | Cols() C 64 | } 65 | 66 | func New[M any, C ModelCols[M, PK], PK comparable](table string, columns C) TypedModelCols[M, PK, C] { 67 | 68 | m := &model[M, PK, C]{ 69 | table: table, 70 | primaryKey: columns.PrimaryKey(), 71 | schema: columns, 72 | } 73 | 74 | for _, col := range columns.Cols() { 75 | col.setTable(table) 76 | col.SetInnerModel(m) 77 | } 78 | 79 | return m 80 | } 81 | 82 | type model[M any, PK comparable, C any] struct { 83 | table string 84 | schema C 85 | primaryKey TypedCol[M, PK] 86 | columns []Column 87 | } 88 | 89 | var _ Model = (*model[any, int, Schema[any]])(nil) 90 | 91 | func (m *model[M, PK, C]) GetTable() string { 92 | return m.table 93 | } 94 | 95 | func (m *model[M, PK, C]) Cols() C { 96 | return m.schema 97 | } 98 | 99 | func (m *model[M, PK, C]) GetColumns() []Column { 100 | return m.columns 101 | } 102 | 103 | func (m *model[M, PK, C]) PrimaryKey() TypedCol[M, PK] { 104 | return m.primaryKey 105 | } 106 | 107 | func (m *model[M, PK, C]) GetPrimaryKey() Column { 108 | return m.primaryKey 109 | } 110 | 111 | func (m *model[M, PK, C]) New() *M { 112 | return new(M) 113 | } 114 | 115 | func (m *model[M, PK, C]) NewDests() []any { 116 | var dests []any 117 | cols := m.GetColumns() 118 | for _, col := range cols { 119 | dests = append(dests, col.NewDest()) 120 | } 121 | return dests 122 | } 123 | 124 | func (m *model[M, PK, C]) FromDests(dests []any) (any, error) { 125 | model, err := m.FromDestsTyped(dests) 126 | if err != nil { 127 | return nil, err 128 | } 129 | return model, nil 130 | } 131 | 132 | func (m *model[M, PK, C]) FromDestsTyped(dests []any) (*M, error) { 133 | model := m.New() 134 | for i, col := range m.GetColumns() { 135 | if err := col.SetValueOnModel(model, dests[i]); err != nil { 136 | return nil, err 137 | } 138 | } 139 | return model, nil 140 | } 141 | -------------------------------------------------------------------------------- /model/op.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Operation uint8 4 | 5 | const ( 6 | OpInsert Operation = iota 7 | OpUpdate 8 | OpSelect 9 | OpDelete 10 | ) 11 | -------------------------------------------------------------------------------- /model/relation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Relation interface { 4 | ForeignDef() Model 5 | LocalKey() Column 6 | } 7 | 8 | type oneToOne[M, R any] struct { 9 | self TypedModel[M] 10 | rel TypedModel[R] 11 | foreignKey Column 12 | localKey Column 13 | ref func(*M) *R 14 | } 15 | 16 | func (oo *oneToOne[M, R]) set(m *M, r *R) { 17 | ref := oo.ref(m) 18 | *ref = *r 19 | } 20 | 21 | func (oo *oneToOne[M, R]) Set(m any, r any) { 22 | model, ok := m.(*M) 23 | if !ok { 24 | panic("invalid model type") 25 | } 26 | 27 | rel, ok := r.(*R) 28 | if !ok { 29 | panic("invalid model type") 30 | } 31 | 32 | oo.set(model, rel) 33 | } 34 | func (oo *oneToOne[M, R]) WithLocalKey(localKey ModelCol[M]) *oneToOne[M, R] { 35 | oo.localKey = localKey 36 | return oo 37 | } 38 | 39 | func (oo *oneToOne[M, R]) ForeignKey() Column { 40 | return oo.foreignKey 41 | } 42 | 43 | func (oo *oneToOne[M, R]) LocalKey() Column { 44 | return oo.localKey 45 | } 46 | 47 | func (oo *oneToOne[M, R]) ForeignDef() Model { 48 | return oo.rel 49 | } 50 | 51 | func (oo *oneToOne[M, R]) Ref(m any) any { 52 | model, ok := m.(*M) 53 | if !ok { 54 | panic("invalid model type") 55 | } 56 | 57 | return oo.ref(model) 58 | } 59 | 60 | type oneToMany[M, R any] struct { 61 | self TypedModel[M] 62 | rel TypedModel[R] 63 | foreignKey Column 64 | localKey Column 65 | ref func(*M) *[]R 66 | } 67 | 68 | func (om *oneToMany[M, R]) Append(m any, r any) { 69 | model, ok := m.(*M) 70 | if !ok { 71 | panic("invalid model type") 72 | } 73 | 74 | rel, ok := r.(*R) 75 | if !ok { 76 | panic("invalid model type") 77 | } 78 | 79 | *om.ref(model) = append(*om.ref(model), *rel) 80 | } 81 | 82 | func (om *oneToMany[M, R]) ForeignKey() Column { 83 | return om.foreignKey 84 | } 85 | 86 | func (om *oneToMany[M, R]) LocalKey() Column { 87 | return om.localKey 88 | } 89 | 90 | func (om *oneToMany[M, R]) ForeignDef() Model { 91 | return om.rel 92 | } 93 | 94 | type OneToMany interface { 95 | Append(m any, r any) 96 | LocalKey() Column 97 | ForeignKey() Column 98 | ForeignDef() Model 99 | } 100 | 101 | type OneToOne interface { 102 | Set(any, any) 103 | LocalKey() Column 104 | ForeignKey() Column 105 | ForeignDef() Model 106 | } 107 | 108 | func BelongsTo[M, R any](localKey ModelCol[M], foreignKey ModelCol[R], ref func(m *M) *R) OneToOne { 109 | return &oneToOne[M, R]{ 110 | self: localKey.GetInnerModel(), 111 | rel: foreignKey.GetInnerModel(), 112 | localKey: localKey, 113 | foreignKey: foreignKey, 114 | ref: ref, 115 | } 116 | } 117 | 118 | func HasOne[M, R any](localKey ModelCol[M], foreignKey ModelCol[R], ref func(m *M) *R) OneToOne { 119 | return &oneToOne[M, R]{ 120 | self: localKey.GetInnerModel(), 121 | rel: foreignKey.GetInnerModel(), 122 | localKey: localKey, 123 | foreignKey: foreignKey, 124 | ref: ref, 125 | } 126 | } 127 | 128 | func HasMany[M, R any](localKey ModelCol[M], foreignKey ModelCol[R], ref func(m *M) *[]R) OneToMany { 129 | return &oneToMany[M, R]{ 130 | self: localKey.GetInnerModel(), 131 | rel: foreignKey.GetInnerModel(), 132 | localKey: localKey, 133 | foreignKey: foreignKey, 134 | ref: func(m *M) *[]R { return ref(m) }, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /queries/expressions.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | type Expr interface { 4 | Build(ParamsSet) (string, []any) 5 | } 6 | 7 | type value struct { 8 | val any 9 | } 10 | 11 | func (v *value) Build(p ParamsSet) (string, []any) { 12 | return p.Next(), []any{v.val} 13 | } 14 | 15 | func Value(val any) Expr { 16 | return &value{val} 17 | } 18 | -------------------------------------------------------------------------------- /queries/insert.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Insert struct { 8 | placeholders ParamsSet 9 | into string 10 | columns []string 11 | values [][]any 12 | } 13 | 14 | func (i *Insert) Values(values []any) *Insert { 15 | i.values = append(i.values, values) 16 | return i 17 | } 18 | 19 | func buildInsertPlaceholder(s ParamsSet, count int) string { 20 | builder := strings.Builder{} 21 | builder.WriteString("(") 22 | for i := 0; i < count; i++ { 23 | builder.WriteString(s.Next()) 24 | if i < count-1 { 25 | builder.WriteString(", ") 26 | } 27 | } 28 | builder.WriteString(")") 29 | 30 | return builder.String() 31 | } 32 | 33 | func (i *Insert) Build() (string, []any) { 34 | var values []any 35 | var parts []string 36 | 37 | parts = append(parts, "INSERT INTO", i.into) 38 | 39 | parts = append(parts, "("+strings.Join(i.columns, ", ")+")") 40 | parts = append(parts, "VALUES") 41 | 42 | for _, valueSet := range i.values { 43 | parts = append(parts, buildInsertPlaceholder(i.placeholders, len(valueSet))) 44 | values = append(values, valueSet...) 45 | } 46 | 47 | return strings.Join(parts, " "), values 48 | } 49 | -------------------------------------------------------------------------------- /queries/joins.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Join struct { 8 | join string // JOIN, LEFT JOIN, RIGHT JOIN, etc. 9 | table string 10 | on Expr 11 | } 12 | 13 | func (j *Join) Build(p ParamsSet) (string, []any) { 14 | var parts []string 15 | var params []any 16 | 17 | parts = append(parts, j.join, j.table) 18 | 19 | if j.on != nil { 20 | sql, values := j.on.Build(p) 21 | parts = append(parts, "ON", sql) 22 | params = append(params, values...) 23 | } 24 | 25 | return strings.Join(parts, " "), params 26 | } 27 | 28 | func LeftJoin(table string) *Join { 29 | return &Join{join: "LEFT JOIN", table: table} 30 | } 31 | 32 | func RightJoin(table string) *Join { 33 | return &Join{join: "RIGHT JOIN", table: table} 34 | } 35 | 36 | func InnerJoin(table string) *Join { 37 | return &Join{join: "INNER JOIN", table: table} 38 | } 39 | 40 | type raw string 41 | 42 | func (r raw) Build(_ ParamsSet) (string, []any) { 43 | return string(r), nil 44 | } 45 | 46 | func Raw(expr string) Expr { 47 | return raw(expr) 48 | } 49 | 50 | func (j *Join) On(expr Expr) *Join { 51 | j.on = expr 52 | return j 53 | } 54 | -------------------------------------------------------------------------------- /queries/params.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import "strconv" 4 | 5 | type ParamsSet interface { 6 | Next() string 7 | } 8 | 9 | type Numbered struct { 10 | count int 11 | } 12 | 13 | func (n *Numbered) Next() string { 14 | n.count++ 15 | return "$" + strconv.Itoa(n.count) 16 | } 17 | 18 | // "?" placeholder 19 | type QuestionMark struct{} 20 | 21 | func (q *QuestionMark) Next() string { 22 | return "?" 23 | } 24 | -------------------------------------------------------------------------------- /queries/query.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | type Query struct { 4 | params ParamsSet 5 | } 6 | 7 | func (q *Query) Select(table string, columns ...string) *Select { 8 | return &Select{ 9 | params: q.params, 10 | table: table, 11 | columns: columns, 12 | } 13 | } 14 | 15 | func (q *Query) Insert(into string, columns []string) *Insert { 16 | return &Insert{ 17 | placeholders: q.params, 18 | into: into, 19 | columns: columns, 20 | } 21 | } 22 | 23 | func (q *Query) Update(table string) *Update { 24 | return &Update{ 25 | params: q.params, 26 | table: table, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /queries/select.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type Select struct { 9 | params ParamsSet 10 | values []any 11 | columns []string 12 | table string 13 | where string 14 | limit string 15 | selectFor string 16 | joins []*Join 17 | } 18 | 19 | func NewQuery(placeholders ParamsSet) *Query { 20 | return &Query{params: placeholders} 21 | } 22 | 23 | func (q *Select) Build() (string, []any) { 24 | var values []any 25 | var parts []string 26 | 27 | parts = append(parts, "SELECT") 28 | 29 | if len(q.columns) > 0 { 30 | parts = append(parts, strings.Join(q.columns, ", ")) 31 | } 32 | 33 | parts = append(parts, "FROM", q.table) 34 | 35 | for _, j := range q.joins { 36 | sql, params := j.Build(q.params) 37 | 38 | parts = append(parts, sql) 39 | values = append(values, params...) 40 | } 41 | 42 | if q.where != "" { 43 | parts = append(parts, "WHERE", q.where) 44 | values = append(values, q.values...) 45 | } 46 | 47 | return strings.Join(parts, " "), values 48 | } 49 | 50 | func (q *Select) Select(table string, columns ...string) *Select { 51 | q.columns = columns 52 | q.table = table 53 | return q 54 | } 55 | 56 | func (q *Select) Limit(limit int) *Select { 57 | q.limit = "LIMIT " + strconv.Itoa(limit) 58 | return q 59 | 60 | } 61 | 62 | func (q *Select) Where(where Expr) *Select { 63 | var values []any 64 | 65 | q.where, values = where.Build(q.params) 66 | q.values = append(q.values, values...) 67 | return q 68 | } 69 | 70 | type SelectForOpt string 71 | 72 | const ( 73 | SkipLocked SelectForOpt = "SKIP LOCKED" 74 | NoWait SelectForOpt = "NOWAIT" 75 | ) 76 | 77 | func (s *Select) ForUpdate(opts ...SelectForOpt) *Select { 78 | s.selectFor = "FOR UPDATE" 79 | if len(opts) > 0 { 80 | s.selectFor += " " + string(opts[0]) 81 | } 82 | 83 | return s 84 | } 85 | 86 | func (s *Select) ForShare(opts ...SelectForOpt) *Select { 87 | s.selectFor = "FOR SHARE" 88 | if len(opts) > 0 { 89 | s.selectFor += " " + string(opts[0]) 90 | } 91 | 92 | return s 93 | } 94 | 95 | func (s *Select) ForKeyShare(opts ...SelectForOpt) *Select { 96 | s.selectFor = "FOR KEY SHARE" 97 | if len(opts) > 0 { 98 | s.selectFor += " " + string(opts[0]) 99 | } 100 | return s 101 | } 102 | 103 | func (q *Select) Join(j *Join) *Select { 104 | q.joins = append(q.joins, j) 105 | return q 106 | } 107 | -------------------------------------------------------------------------------- /queries/update.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Update struct { 8 | params ParamsSet 9 | values []Expr 10 | columns []string 11 | table string 12 | where Expr 13 | } 14 | 15 | func (u *Update) Set(column string, value Expr) *Update { 16 | u.columns = append(u.columns, column) 17 | u.values = append(u.values, value) 18 | return u 19 | } 20 | 21 | func (u *Update) Where(where Expr) *Update { 22 | u.where = where 23 | return u 24 | } 25 | 26 | func (u *Update) Build() (string, []any) { 27 | builder := strings.Builder{} 28 | 29 | builder.WriteString("UPDATE ") 30 | builder.WriteString(u.table) 31 | 32 | builder.WriteString(" SET ") 33 | 34 | var values []any 35 | 36 | for i, col := range u.columns { 37 | if i > 0 { 38 | builder.WriteString(", ") 39 | } 40 | 41 | builder.WriteString(col) 42 | builder.WriteString(" = ") 43 | 44 | part, colValues := u.values[i].Build(u.params) 45 | builder.WriteString(part) 46 | 47 | values = append(values, colValues...) 48 | } 49 | 50 | if u.where != nil { 51 | builder.WriteString(" WHERE ") 52 | 53 | part, whereValues := u.where.Build(u.params) 54 | builder.WriteString(part) 55 | 56 | values = append(values, whereValues...) 57 | } 58 | 59 | return builder.String(), values 60 | } 61 | -------------------------------------------------------------------------------- /queries/where.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type simpleCond struct { 8 | right Expr 9 | op string 10 | left Expr 11 | } 12 | 13 | func newSimpleCond(left Expr, op string, right Expr) simpleCond { 14 | return simpleCond{left: left, op: op, right: right} 15 | } 16 | 17 | type logicalExpr struct { 18 | conds []Expr 19 | operator string 20 | } 21 | 22 | func (w logicalExpr) Build(params ParamsSet) (string, []any) { 23 | var parts []string 24 | var values []any 25 | 26 | for _, cond := range w.conds { 27 | part, condValues := cond.Build(params) 28 | parts = append(parts, part) 29 | values = append(values, condValues...) 30 | } 31 | 32 | return "(" + strings.Join(parts, " "+w.operator+" ") + ")", values 33 | } 34 | 35 | func And(conds ...Expr) logicalExpr { 36 | return logicalExpr{conds: conds, operator: "AND"} 37 | } 38 | 39 | func Or(conds ...Expr) logicalExpr { 40 | return logicalExpr{conds: conds, operator: "OR"} 41 | } 42 | 43 | func EQ(left Expr, right Expr) simpleCond { 44 | return newSimpleCond(left, "=", right) 45 | } 46 | 47 | func NE(left Expr, right Expr) simpleCond { 48 | return newSimpleCond(left, "!=", right) 49 | } 50 | 51 | func GT(left Expr, right Expr) simpleCond { 52 | return newSimpleCond(left, ">", right) 53 | } 54 | 55 | func GTE(left Expr, right Expr) simpleCond { 56 | return newSimpleCond(left, ">=", right) 57 | } 58 | 59 | func LT(left Expr, right Expr) simpleCond { 60 | return newSimpleCond(left, "<", right) 61 | } 62 | 63 | func LTE(left Expr, right Expr) simpleCond { 64 | return newSimpleCond(left, "<=", right) 65 | } 66 | 67 | func (w simpleCond) Build(params ParamsSet) (string, []any) { 68 | leftPart, leftValues := w.left.Build(params) 69 | rightPart, rightValues := w.right.Build(params) 70 | 71 | b := strings.Builder{} 72 | b.WriteString("(") 73 | b.WriteString(leftPart) 74 | b.WriteString(" ") 75 | b.WriteString(w.op) 76 | b.WriteString(" ") 77 | b.WriteString(rightPart) 78 | b.WriteString(")") 79 | 80 | return b.String(), append(leftValues, rightValues...) 81 | } 82 | 83 | type inCond struct { 84 | col Expr 85 | vals []Expr 86 | } 87 | 88 | func (w inCond) Build(params ParamsSet) (string, []any) { 89 | var rightParts []string 90 | var values []any 91 | 92 | leftPart, leftValues := w.col.Build(params) 93 | 94 | values = append(values, leftValues...) 95 | 96 | for _, val := range w.vals { 97 | part, valValues := val.Build(params) 98 | rightParts = append(rightParts, part) 99 | values = append(values, valValues...) 100 | } 101 | 102 | return "(" + leftPart + " IN (" + strings.Join(rightParts, ", ") + "))", values 103 | } 104 | 105 | func IN(expr Expr, values []Expr) inCond { 106 | return inCond{col: expr, vals: values} 107 | } 108 | -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package modb 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/lucasjacques/modb/model" 7 | "github.com/lucasjacques/modb/queries" 8 | ) 9 | 10 | func NewRepository[M any, PK comparable, C any](db DBTX, model model.TypedModelCols[M, PK, C]) *ModelRepository[M, PK, C] { 11 | return &ModelRepository[M, PK, C]{ 12 | db: db, 13 | model: model, 14 | } 15 | } 16 | 17 | type ModelRepository[M any, PK comparable, C any] struct { 18 | db DBTX 19 | model model.TypedModelCols[M, PK, C] 20 | } 21 | 22 | func (r *ModelRepository[M, PK, S]) Insert(ctx context.Context, m *M) error { 23 | table := r.model.GetTable() 24 | cols := r.model.GetColumns() 25 | 26 | columns := make([]string, 0, len(cols)) 27 | values := make([]any, 0, len(cols)) 28 | 29 | for _, col := range cols { 30 | if col.ShouldOmit(r, model.OpInsert) { 31 | continue 32 | } 33 | 34 | columns = append(columns, col.GetName()) 35 | value, err := col.ValueFromModel(m) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | values = append(values, value) 41 | } 42 | 43 | str, values := queries.NewQuery(r.db.NewParamsSet()).Insert(table, columns).Values(values).Build() 44 | _, err := r.db.Exec(ctx, str, values...) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (r *ModelRepository[M, PK, C]) Update(ctx context.Context, m *M) error { 53 | table := r.model.GetTable() 54 | 55 | query := queries.NewQuery(r.db.NewParamsSet()).Update(table) 56 | 57 | for _, col := range r.model.GetColumns() { 58 | if col.ShouldOmit(m, model.OpUpdate) { 59 | continue 60 | } 61 | 62 | value, err := col.ValueFromModel(m) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | query.Set(col.GetName(), queries.Value(value)) 68 | } 69 | 70 | pk, err := r.model.PrimaryKey().ValueFromModel(m) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | sql, values := query.Where(queries.EQ(r.model.PrimaryKey(), queries.Value(pk))).Build() 76 | _, err = r.db.Exec(ctx, sql, values...) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | --------------------------------------------------------------------------------