├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── LICENCE ├── Makefile ├── README.md ├── benchmark ├── bench_test.go ├── go.mod └── go.sum ├── binder.go ├── binder_test.go ├── configurators.go ├── connection.go ├── dialect.go ├── field.go ├── go.mod ├── go.sum ├── orm.go ├── orm_test.go ├── query.go ├── query_test.go ├── schema.go ├── schema_test.go └── timestamps.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - ubuntu-latest 16 | go: 17 | - '1.18' 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v2 26 | with: 27 | stable: false 28 | check-latest: true 29 | go-version: ${{ matrix.go }} 30 | 31 | - name: Coveralls 32 | env: 33 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | go install github.com/mattn/goveralls@latest 36 | goveralls -service=github -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '32 16 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https:// docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https:// git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea/* 2 | cover.out 3 | **db -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GoLobby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -coverprofile=cover.out ./... 3 | go tool cover -html=cover.out 4 | 5 | bench: 6 | cd benchmark/ && go test -v -bench=. -benchmem -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/golobby/orm/?status.svg)](https://godoc.org/github.com/golobby/orm) 2 | [![CI](https://github.com/golobby/orm/actions/workflows/ci.yml/badge.svg)](https://github.com/golobby/orm/actions/workflows/ci.yml) 3 | [![CodeQL](https://github.com/golobby/orm/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/golobby/orm/actions/workflows/codeql-analysis.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/golobby/orm)](https://goreportcard.com/report/github.com/golobby/orm) 5 | [![Coverage Status](https://coveralls.io/repos/github/golobby/orm/badge.svg?r=1)](https://coveralls.io/github/golobby/orm?branch=master) 6 | 7 | # Golobby ORM 8 | 9 | GoLobby ORM is a lightweight yet powerful, fast, customizable, type-safe object-relational mapper for the Go programming language. 10 | 11 | ## Table Of Contents 12 | * [Features](#features) 13 | + [Introduction](#introduction) 14 | + [Creating a new Entity](#creating-a-new-entity) 15 | - [Conventions](#conventions) 16 | * [Timestamps](#timestamps) 17 | * [Column names](#column-names) 18 | * [Primary Key](#primary-key) 19 | + [Initializing ORM](#initializing-orm) 20 | + [Fetching an entity from a database](#fetching-an-entity-from-a-database) 21 | + [Saving entities or Insert/Update](#saving-entities-or-insert-update) 22 | + [Using raw SQL](#using-raw-sql) 23 | + [Deleting entities](#deleting-entities) 24 | + [Relationships](#relationships) 25 | - [HasMany](#hasmany) 26 | - [HasOne](#hasone) 27 | - [BelongsTo](#belongsto) 28 | - [BelongsToMany](#belongstomany) 29 | - [Saving with relation](#saving-with-relation) 30 | + [Query Builder](#query-builder) 31 | - [Finishers](#finishers) 32 | * [All](#all) 33 | * [Get](#get) 34 | * [Update](#update) 35 | * [Delete](#delete) 36 | - [Select](#select) 37 | * [Column names](#column-names-1) 38 | * [Table](#table) 39 | * [Where](#where) 40 | * [Order By](#order-by) 41 | * [Limit](#limit) 42 | * [Offset](#offset) 43 | * [First, Latest](#first-latest) 44 | - [Update](#update) 45 | * [Where](#where-1) 46 | * [Table](#table-1) 47 | * [Set](#set) 48 | - [Delete](#delete) 49 | * [Table](#table-2) 50 | * [Where](#where-2) 51 | + [Database Validations](#database-validations) 52 | * [License](#license) 53 | 54 | ## Introduction 55 | GoLobby ORM is an object-relational mapper (ORM) that makes it enjoyable to interact with your database. 56 | When using Golobby ORM, each database table has a corresponding "Entity" to interact with that table using elegant APIs. 57 | 58 | ## Features 59 | - Elegant and easy-to-use APIs with the help of Generics. 60 | - Type-safety. 61 | - Using reflection at startup to be fast during runtime. 62 | - No code generation! 63 | - Query builder for various query types. 64 | - Binding query results to entities. 65 | - Supports different kinds of relationship/Association types: 66 | - One to one 67 | - One to Many 68 | - Many to Many 69 | 70 | ## Performance 71 | You can run performance benchmark against `GORM` using 72 | ```bash 73 | make bench 74 | ``` 75 | here are results from my laptop 76 | ``` 77 | goos: darwin 78 | goarch: arm64 79 | pkg: github.com/golobby/orm/benchmark 80 | BenchmarkGolobby 81 | BenchmarkGolobby-8 235956 4992 ns/op 2192 B/op 66 allocs/op 82 | BenchmarkGorm 83 | BenchmarkGorm-8 54498 21308 ns/op 7208 B/op 147 allocs/op 84 | PASS 85 | ok github.com/golobby/orm/benchmark 3.118s 86 | ``` 87 | 88 | ## Quick Start 89 | 90 | The following example demonstrates how to use the GoLobby ORM. 91 | 92 | ```go 93 | package main 94 | 95 | import "github.com/golobby/orm" 96 | 97 | // User entity 98 | type User struct { 99 | ID int64 100 | FirstName string 101 | LastName string 102 | Email string 103 | orm.Timestamps 104 | } 105 | 106 | // It will be called by ORM to setup entity. 107 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 108 | // Specify related database table for the entity. 109 | e.Table("users") 110 | } 111 | 112 | func main() { 113 | // Setup ORM 114 | err := orm.Initialize(orm.ConnectionConfig{ 115 | // Name: "default", // Optional. Specify connection names if you have more than on database. 116 | Driver: "sqlite3", // Database type. Currently supported sqlite3, mysql, mariadb, postgresql. 117 | ConnectionString: ":memory:", // Database DSN. 118 | DatabaseValidations: true, // Validates your database tables and each table schema 119 | }) 120 | 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | // Find user by primary key (ID) 126 | user, err := orm.Find[User](1) 127 | 128 | // Update entity 129 | user.Email = "jack@mail.com" 130 | 131 | // Save entity 132 | orm.Save(&user) 133 | } 134 | ``` 135 | 136 | ### Creating a new Entity 137 | Let's create a new `Entity` to represent `User` in our application. 138 | 139 | ```go 140 | package main 141 | 142 | import "github.com/golobby/orm" 143 | 144 | type User struct { 145 | ID int64 146 | Name string 147 | LastName string 148 | Email string 149 | orm.Timestamps 150 | } 151 | 152 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 153 | e.Table("users"). 154 | Connection("default") // You can omit connection name if you only have one. 155 | 156 | } 157 | ``` 158 | As you see, our user entity is nothing else than a simple struct and two methods. 159 | Entities in GoLobby ORM are implementations of `Entity` interface, which defines two methods: 160 | - ConfigureEntity: configures table, fields, and also relations to other entities. 161 | #### Conventions 162 | We have standard conventions and we encourage you to follow, but if you want to change them for any reason you can use `Field` method to customize how ORM 163 | inferres meta data from your `Entity`. 164 | 165 | ##### Column names 166 | GoLobby ORM for each struct field(except slice, arrays, maps, and other nested structs) assumes a respective column named using snake case syntax. 167 | If you want a custom column name, you should specify it in `ConfigureEntity` method using `Field()` method. 168 | ```go 169 | package main 170 | 171 | type User struct { 172 | Name string 173 | } 174 | 175 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 176 | e.Field("Name").ColumnName("custom_name_for_column") 177 | 178 | e.Table("users") 179 | } 180 | ``` 181 | ##### Timestamps 182 | for having `created_at`, `updated_at`, `deleted_at` timestamps in your entities you can embed `orm.Timestamps` struct in your entity, 183 | ```go 184 | type User struct { 185 | ID int64 186 | Name string 187 | LastName string 188 | Email string 189 | orm.Timestamps 190 | } 191 | 192 | ``` 193 | Also, if you want custom names for them, you can do it like this. 194 | ```go 195 | type User struct { 196 | ID int64 197 | Name string 198 | LastName string 199 | Email string 200 | MyCreatedAt sql.NullTime 201 | MyUpdatedAt sql.NullTime 202 | MyDeletedAt sql.NullTime 203 | } 204 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 205 | e.Field("MyCreatedAt").IsCreatedAt() // this will make ORM to use MyCreatedAt as created_at column 206 | e.Field("MyUpdatedAt").IsUpdatedAt() // this will make ORM to use MyUpdatedAt as created_at column 207 | e.Field("MyDeletedAt").IsDeletedAt() // this will make ORM to use MyDeletedAt as created_at column 208 | 209 | e.Table("users") 210 | } 211 | ``` 212 | As always you use `Field` method for configuring how ORM behaves to your struct field. 213 | 214 | ##### Primary Key 215 | GoLobby ORM assumes that each entity has a primary key named `id`; if you want a custom primary key called, you need to specify it in entity struct. 216 | ```go 217 | package main 218 | 219 | type User struct { 220 | PK int64 221 | } 222 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 223 | e.Field("PK").IsPrimaryKey() // this will make ORM use PK field as primary key. 224 | e.Table("users") 225 | } 226 | ``` 227 | 228 | ### Initializing ORM 229 | After creating our entities, we need to initialize GoLobby ORM. 230 | ```go 231 | package main 232 | 233 | import "github.com/golobby/orm" 234 | 235 | func main() { 236 | orm.Initialize(orm.ConnectionConfig{ 237 | // Name: "default", You should specify connection name if you have multiple connections 238 | Driver: "sqlite3", 239 | ConnectionString: ":memory:", 240 | }) 241 | } 242 | ``` 243 | After this step, we can start using ORM. 244 | ### Fetching an entity from a database 245 | GoLobby ORM makes it trivial to fetch entities from a database using its primary key. 246 | ```go 247 | user, err := orm.Find[User](1) 248 | ``` 249 | `orm.Find` is a generic function that takes a generic parameter that specifies the type of `Entity` we want to query and its primary key value. 250 | You can also use custom queries to get entities from the database. 251 | ```go 252 | 253 | user, err := orm.Query[User]().Where("id", 1).First() 254 | user, err := orm.Query[User]().WherePK(1).First() 255 | ``` 256 | GoLobby ORM contains a powerful query builder, which you can use to build `Select`, `Update`, and `Delete` queries, but if you want to write a raw SQL query, you can. 257 | ```go 258 | users, err := orm.QueryRaw[User](`SELECT * FROM users`) 259 | ``` 260 | 261 | ### Saving entities or Insert/Update 262 | GoLobby ORM makes it easy to persist an `Entity` to the database using `Save` method, it's an UPSERT method, if the primary key field is not zero inside the entity 263 | it will go for an update query; otherwise, it goes for the insert. 264 | ```go 265 | // this will insert entity into the table 266 | err := orm.Save(&User{Name: "Amirreza"}) // INSERT INTO users (name) VALUES (?) , "Amirreza" 267 | ``` 268 | ```go 269 | // this will update entity with id = 1 270 | orm.Save(&User{ID: 1, Name: "Amirreza2"}) // UPDATE users SET name=? WHERE id=?, "Amirreza2", 1 271 | ``` 272 | Also, you can do custom update queries using query builder or raw SQL again as well. 273 | ```go 274 | res, err := orm.Query[User]().Where("id", 1).Update(orm.KV{"name": "amirreza2"}) 275 | ``` 276 | 277 | ### Using raw SQL 278 | 279 | ```go 280 | _, affected, err := orm.ExecRaw[User](`UPDATE users SET name=? WHERE id=?`, "amirreza", 1) 281 | ``` 282 | ### Deleting entities 283 | It is also easy to delete entities from a database. 284 | ```go 285 | err := orm.Delete(user) 286 | ``` 287 | You can also use query builder or raw SQL. 288 | ```go 289 | _, affected, err := orm.Query[Post]().WherePK(1).Delete() 290 | 291 | _, affected, err := orm.Query[Post]().Where("id", 1).Delete() 292 | 293 | ``` 294 | ```go 295 | _, affected, err := orm.ExecRaw[Post](`DELETE FROM posts WHERE id=?`, 1) 296 | ``` 297 | ### Relationships 298 | GoLobby ORM makes it easy to have entities that have relationships with each other. Configuring relations is using `ConfigureEntity` method, as you will see. 299 | #### HasMany 300 | ```go 301 | type Post struct {} 302 | 303 | func (p Post) ConfigureEntity(e *orm.EntityConfigurator) { 304 | e.Table("posts").HasMany(&Comment{}, orm.HasManyConfig{}) 305 | } 306 | ``` 307 | As you can see, we are defining a `Post` entity that has a `HasMany` relation with `Comment`. You can configure how GoLobby ORM queries `HasMany` relation with `orm.HasManyConfig` object; by default, it will infer all fields for you. 308 | Now you can use this relationship anywhere in your code. 309 | ```go 310 | comments, err := orm.HasMany[Comment](post).All() 311 | ``` 312 | `HasMany` and other related functions in GoLobby ORM return `QueryBuilder`, and you can use them like other query builders and create even more 313 | complex queries for relationships. for example, you can start a query to get all comments of a post made today. 314 | ```go 315 | todayComments, err := orm.HasMany[Comment](post).Where("created_at", "CURDATE()").All() 316 | ``` 317 | #### HasOne 318 | Configuring a `HasOne` relation is like `HasMany`. 319 | ```go 320 | type Post struct {} 321 | 322 | func (p Post) ConfigureEntity(e *orm.EntityConfigurator) { 323 | e.Table("posts").HasOne(&HeaderPicture{}, orm.HasOneConfig{}) 324 | } 325 | ``` 326 | As you can see, we are defining a `Post` entity that has a `HasOne` relation with `HeaderPicture`. You can configure how GoLobby ORM queries `HasOne` relation with `orm.HasOneConfig` object; by default, it will infer all fields for you. 327 | Now you can use this relationship anywhere in your code. 328 | ```go 329 | picture, err := orm.HasOne[HeaderPicture](post) 330 | ``` 331 | `HasOne` also returns a query builder, and you can create more complex queries for relations. 332 | #### BelongsTo 333 | ```go 334 | type Comment struct {} 335 | 336 | func (c Comment) ConfigureEntity(e *orm.EntityConfigurator) { 337 | e.Table("comments").BelongsTo(&Post{}, orm.BelongsToConfig{}) 338 | } 339 | ``` 340 | As you can see, we are defining a `Comment` entity that has a `BelongsTo` relation with `Post` that we saw earlier. You can configure how GoLobby ORM queries `BelongsTo` relation with `orm.BelongsToConfig` object; by default, it will infer all fields for you. 341 | Now you can use this relationship anywhere in your code. 342 | ```go 343 | post, err := orm.BelongsTo[Post](comment).First() 344 | ``` 345 | #### BelongsToMany 346 | ```go 347 | type Post struct {} 348 | 349 | func (p Post) ConfigureEntity(e *orm.EntityConfigurator) { 350 | e.Table("posts").BelongsToMany(&Category{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"}) 351 | } 352 | 353 | type Category struct{} 354 | 355 | func(c Category) ConfigureEntity(r *orm.EntityConfigurator) { 356 | e.Table("categories").BelongsToMany(&Post{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"}) 357 | } 358 | 359 | ``` 360 | We are defining a `Post` entity and a `Category` entity with a `many2many` relationship; as you can see, we must configure the IntermediateTable name, which GoLobby ORM cannot infer. 361 | Now you can use this relationship anywhere in your code. 362 | ```go 363 | categories, err := orm.BelongsToMany[Category](post).All() 364 | ``` 365 | #### Saving with relation 366 | You may need to save an entity that has some kind of relationship with another entity; in that case, you can use `Add` method. 367 | ```go 368 | orm.Add(post, comments...) // inserts all comments passed in and also sets all post_id to the primary key of the given post. 369 | orm.Add(post, categories...) // inserts all categories and also insert intermediate post_categories records. 370 | ``` 371 | 372 | ### Query Builder 373 | GoLobby ORM contains a powerful query builder to help you build complex queries with ease. QueryBuilder is accessible from `orm.Query[Entity]` method 374 | which will create a new query builder for you with given type parameter. 375 | Query builder can build `SELECT`,`UPDATE`,`DELETE` queries for you. 376 | 377 | #### Finishers 378 | Finishers are methods on QueryBuilder that will some how touch database, so use them with caution. 379 | ##### All 380 | All will generate a `SELECT` query from QueryBuilder, execute it on database and return results in a slice of OUTPUT. It's useful for queries that have multiple results. 381 | ```go 382 | posts, err := orm.Query[Post]().All() 383 | ``` 384 | ##### Get 385 | Get will generate a `SELECT` query from QueryBuilder, execute it on database and return results in an instance of type parameter `OUTPUT`. It's useful for when you know your query has single result. 386 | ```go 387 | post, err := orm.Query[Post]().First().Get() 388 | ``` 389 | ##### Update 390 | Update will generate an `UPDATE` query from QueryBuilder and executes it, returns rows affected by query and any possible error. 391 | ```go 392 | rowsAffected, err := orm.Query[Post]().WherePK(1).Set("body", "body jadid").Update() 393 | ``` 394 | ##### Delete 395 | Delete will generate a `DELETE` query from QueryBuilder and executes it, returns rows affected by query and any possible error. 396 | ```go 397 | rowsAffected, err := orm.Query[Post]().WherePK(1).Delete() 398 | ``` 399 | #### Select 400 | Let's start with `Select` queries. 401 | Each `Select` query consists of following: 402 | ```sql 403 | SELECT [column names] FROM [table name] WHERE [cond1 AND/OR cond2 AND/OR ...] ORDER BY [column] [ASC/DESC] LIMIT [N] OFFSET [N] GROUP BY [col] 404 | ``` 405 | Query builder has methods for constructing each part, of course not all of these parts are necessary. 406 | ##### Column names 407 | for setting column names to select use `Select` method as following: 408 | ```go 409 | orm.Query[Post]().Select("id", "title") 410 | ``` 411 | ##### Table 412 | for setting table name for select use `Table` method as following: 413 | ```go 414 | orm.Query[Post]().Table("users") 415 | ``` 416 | ##### Where 417 | for adding where conditions based on what kind of where you want you can use any of following: 418 | ```go 419 | orm.Query[Post]().Where("name", "amirreza") // Equal mode: WHERE name = ?, ["amirreza"] 420 | orm.Query[Post]().Where("age", "<", 19) // Operator mode: WHERE age < ?, [19] 421 | orm.Query[Post]().WhereIn("id", 1,2,3,4,5) // WhereIn: WHERE id IN (?,?,?,?,?), [1,2,3,4,5] 422 | ``` 423 | You can also chain these together. 424 | ```go 425 | orm.Query[Post](). 426 | Where("name", "amirreza"). 427 | AndWhere("age", "<", 10). 428 | OrWhere("id", "!=", 1) 429 | // WHERE name = ? AND age < ? OR id != ?, ["amirreza", 10, 1] 430 | ``` 431 | ##### Order By 432 | You can set order by of query using `OrderBy` as following. 433 | ```go 434 | orm.Query[Post]().OrderBy("id", orm.ASC) // ORDER BY id ASC 435 | orm.Query[Post]().OrderBy("id", orm.DESC) // ORDER BY id DESC 436 | ``` 437 | 438 | ##### Limit 439 | You can set limit setting of query using `Limit` as following 440 | ```go 441 | orm.Query[Post]().Limit1(1) // LIMIT 1 442 | ``` 443 | 444 | ##### Offset 445 | You can set limit setting of query using `Offset` as following 446 | ```go 447 | orm.Query[Post]().Offset(1) // OFFSET 1 448 | ``` 449 | 450 | ##### First, Latest 451 | You can use `First`, `Latest` method which are also executers of query as you already seen to get first or latest record. 452 | ```go 453 | orm.Query[Post]().First() // SELECT * FROM posts ORDER BY id ASC LIMIT 1 454 | orm.Query[Post]().Latest() // SELECT * FROM posts ORDER BY id DESC LIMIT 1 455 | ``` 456 | #### Update 457 | Each `Update` query consists of following: 458 | ```sql 459 | UPDATE [table name] SET [col=val] WHERE [cond1 AND/OR cond2 AND/OR ...] 460 | ``` 461 | ##### Where 462 | Just like select where stuff, same code. 463 | 464 | ##### Table 465 | Same as select. 466 | 467 | ##### Set 468 | You can use `Set` method to set value. 469 | ```go 470 | orm.Query[Message](). 471 | Where("id", 1). 472 | Set("read", true, "seen", true). 473 | Update() // UPDATE posts SET read=?, seen=? WHERE id = ?, [true, true, 1] 474 | ``` 475 | 476 | #### Delete 477 | Each `Delete` query consists of following: 478 | ```sql 479 | DELETE FROM [table name] WHERE [cond1 AND/OR cond2 AND/OR ...] 480 | ``` 481 | ##### Table 482 | Same as Select and Update. 483 | ##### Where 484 | Same as Select and Update. 485 | ### Database Validations 486 | Golobby ORM can validate your database state and compare it to your entities and if your database and code are not in sync give you error. 487 | Currently there are two database validations possible: 488 | 1. Validate all necessary tables exists. 489 | 2. Validate all tables contain necessary columns. 490 | You can enable database validations feature by enabling `DatabaseValidations` flag in your ConnectionConfig. 491 | ```go 492 | return orm.SetupConnections(orm.ConnectionConfig{ 493 | Name: "default", 494 | DB: db, 495 | Dialect: orm.Dialects.SQLite3, 496 | Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}}, 497 | DatabaseValidations: true, 498 | }) 499 | ``` 500 | ## License 501 | GoLobby ORM is released under the [MIT License](http://opensource.org/licenses/mit-license.php). 502 | -------------------------------------------------------------------------------- /benchmark/bench_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/golobby/orm" 7 | "gorm.io/driver/sqlite" 8 | "gorm.io/gorm" 9 | "testing" 10 | ) 11 | 12 | type User struct { 13 | ID int64 `json:"id"` 14 | Username string `json:"username"` 15 | } 16 | 17 | var ( 18 | db *sql.DB 19 | gormDB *gorm.DB 20 | ) 21 | 22 | func setupGolobby() { 23 | var err error 24 | db, err = sql.Open("sqlite3", ":memory:") 25 | if err != nil { 26 | panic(err) 27 | } 28 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username text)`) 29 | if err != nil { 30 | panic(err) 31 | } 32 | err = orm.SetupConnections(orm.ConnectionConfig{ 33 | Name: "default", 34 | DB: db, 35 | Dialect: orm.Dialects.SQLite3, 36 | Entities: []orm.Entity{User{}}, 37 | DatabaseValidations: true, 38 | }) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | } 44 | 45 | func setupGORM() { 46 | var err error 47 | gormDB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) 48 | if err != nil { 49 | panic(err) 50 | } 51 | err = gormDB.AutoMigrate(&User2{}) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | func (u User) ConfigureEntity(e *orm.EntityConfigurator) { 58 | e.Table("users") 59 | } 60 | 61 | func BenchmarkGolobby(t *testing.B) { 62 | setupGolobby() 63 | t.ResetTimer() 64 | for i := 0; i < t.N; i++ { 65 | var user User 66 | user.Username = "amir" + fmt.Sprint(i) 67 | err := orm.Insert(&user) 68 | if err != nil { 69 | panic(err) 70 | } 71 | } 72 | } 73 | 74 | func BenchmarkStdSQL(t *testing.B) { 75 | setupGolobby() 76 | t.ResetTimer() 77 | for i := 0; i < t.N; i++ { 78 | var user User 79 | user.Username = "amir" + fmt.Sprint(i) 80 | _, err := db.Exec(`INSERT INTO users (username) VALUES (?)`, user.Username) 81 | if err != nil { 82 | panic(err) 83 | } 84 | } 85 | } 86 | 87 | type User2 struct { 88 | gorm.Model 89 | ID int64 `json:"id"` 90 | Username string `json:"username"` 91 | } 92 | 93 | func BenchmarkGorm(t *testing.B) { 94 | setupGORM() 95 | t.ResetTimer() 96 | for i := 0; i < t.N; i++ { 97 | var user User2 98 | user.Username = "amir" + fmt.Sprint(i) 99 | err := gormDB.Create(&user).Error 100 | if err != nil { 101 | panic(err) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/golobby/orm/benchmark 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/golobby/orm v1.2.2 7 | gorm.io/driver/sqlite v1.3.2 8 | gorm.io/gorm v1.23.5 9 | ) 10 | 11 | replace github.com/golobby/orm => ../ 12 | 13 | require ( 14 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 15 | github.com/gertd/go-pluralize v0.1.7 // indirect 16 | github.com/go-openapi/errors v0.19.8 // indirect 17 | github.com/go-openapi/strfmt v0.21.2 // indirect 18 | github.com/go-sql-driver/mysql v1.6.0 // indirect 19 | github.com/go-stack/stack v1.8.0 // indirect 20 | github.com/iancoleman/strcase v0.2.0 // indirect 21 | github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/jinzhu/now v1.1.5 // indirect 24 | github.com/lib/pq v1.10.4 // indirect 25 | github.com/mattn/go-runewidth v0.0.13 // indirect 26 | github.com/mattn/go-sqlite3 v1.14.13 // indirect 27 | github.com/mitchellh/mapstructure v1.3.3 // indirect 28 | github.com/oklog/ulid v1.3.1 // indirect 29 | github.com/rivo/uniseg v0.2.0 // indirect 30 | go.mongodb.org/mongo-driver v1.7.5 // indirect 31 | golang.org/x/sys v0.1.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 2 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= 3 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/gertd/go-pluralize v0.1.7 h1:RgvJTJ5W7olOoAks97BOwOlekBFsLEyh00W48Z6ZEZY= 9 | github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvjYgAahcqSDQ= 10 | github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= 11 | github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 12 | github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os= 13 | github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= 14 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 15 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 16 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 17 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 18 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 19 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 20 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 22 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 24 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 25 | github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= 26 | github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= 27 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 28 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 29 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 30 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 31 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 32 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 33 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 37 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 38 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 39 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 40 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 41 | github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 42 | github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= 43 | github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 44 | github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= 45 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 46 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 47 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 48 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 49 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 50 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 58 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 61 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 62 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 63 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 64 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 65 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 66 | go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= 67 | go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= 68 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 69 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 70 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 71 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 72 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 77 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 79 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 80 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 89 | gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg= 90 | gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= 91 | gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 92 | gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= 93 | gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 94 | -------------------------------------------------------------------------------- /binder.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "fmt" 7 | "reflect" 8 | "unsafe" 9 | ) 10 | 11 | // makeNewPointersOf creates a map of [field name] -> pointer to fill it 12 | // recursively. it will go down until reaches a driver.Valuer implementation, it will stop there. 13 | func (b *binder) makeNewPointersOf(v reflect.Value) interface{} { 14 | m := map[string]interface{}{} 15 | actualV := v 16 | for actualV.Type().Kind() == reflect.Ptr { 17 | actualV = actualV.Elem() 18 | } 19 | if actualV.Type().Kind() == reflect.Struct { 20 | for i := 0; i < actualV.NumField(); i++ { 21 | f := actualV.Field(i) 22 | if (f.Type().Kind() == reflect.Struct || f.Type().Kind() == reflect.Ptr) && !f.Type().Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) { 23 | f = reflect.NewAt(actualV.Type().Field(i).Type, unsafe.Pointer(actualV.Field(i).UnsafeAddr())) 24 | fm := b.makeNewPointersOf(f).(map[string]interface{}) 25 | for k, p := range fm { 26 | m[k] = p 27 | } 28 | } else { 29 | var fm *field 30 | fm = b.s.getField(actualV.Type().Field(i)) 31 | if fm == nil { 32 | fm = fieldMetadata(actualV.Type().Field(i), b.s.columnConstraints)[0] 33 | } 34 | m[fm.Name] = reflect.NewAt(actualV.Field(i).Type(), unsafe.Pointer(actualV.Field(i).UnsafeAddr())).Interface() 35 | } 36 | } 37 | } else { 38 | return v.Addr().Interface() 39 | } 40 | 41 | return m 42 | } 43 | 44 | // ptrsFor first allocates for all struct fields recursively until reaches a driver.Value impl 45 | // then it will put them in a map with their correct field name as key, then loops over cts 46 | // and for each one gets appropriate one from the map and adds it to pointer list. 47 | func (b *binder) ptrsFor(v reflect.Value, cts []*sql.ColumnType) []interface{} { 48 | ptrs := b.makeNewPointersOf(v) 49 | var scanInto []interface{} 50 | if reflect.TypeOf(ptrs).Kind() == reflect.Map { 51 | nameToPtr := ptrs.(map[string]interface{}) 52 | for _, ct := range cts { 53 | if nameToPtr[ct.Name()] != nil { 54 | scanInto = append(scanInto, nameToPtr[ct.Name()]) 55 | } 56 | } 57 | } else { 58 | scanInto = append(scanInto, ptrs) 59 | } 60 | 61 | return scanInto 62 | } 63 | 64 | type binder struct { 65 | s *schema 66 | } 67 | 68 | func newBinder(s *schema) *binder { 69 | return &binder{s: s} 70 | } 71 | 72 | // bind binds given rows to the given object at obj. obj should be a pointer 73 | func (b *binder) bind(rows *sql.Rows, obj interface{}) error { 74 | cts, err := rows.ColumnTypes() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | t := reflect.TypeOf(obj) 80 | v := reflect.ValueOf(obj) 81 | if t.Kind() != reflect.Ptr { 82 | return fmt.Errorf("obj should be a ptr") 83 | } 84 | // since passed input is always a pointer one deref is necessary 85 | t = t.Elem() 86 | v = v.Elem() 87 | if t.Kind() == reflect.Slice { 88 | // getting slice elemnt type -> slice[t] 89 | t = t.Elem() 90 | for rows.Next() { 91 | var rowValue reflect.Value 92 | // Since reflect.SetupConnections returns a pointer to the type, we need to unwrap it to get actual 93 | rowValue = reflect.New(t).Elem() 94 | // till we reach a not pointer type continue newing the underlying type. 95 | for rowValue.IsZero() && rowValue.Type().Kind() == reflect.Ptr { 96 | rowValue = reflect.New(rowValue.Type().Elem()).Elem() 97 | } 98 | newCts := make([]*sql.ColumnType, len(cts)) 99 | copy(newCts, cts) 100 | ptrs := b.ptrsFor(rowValue, newCts) 101 | err = rows.Scan(ptrs...) 102 | if err != nil { 103 | return err 104 | } 105 | for rowValue.Type() != t { 106 | tmp := reflect.New(rowValue.Type()) 107 | tmp.Elem().Set(rowValue) 108 | rowValue = tmp 109 | } 110 | v = reflect.Append(v, rowValue) 111 | } 112 | } else { 113 | for rows.Next() { 114 | ptrs := b.ptrsFor(v, cts) 115 | err = rows.Scan(ptrs...) 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | } 121 | // v is either struct or slice 122 | reflect.ValueOf(obj).Elem().Set(v) 123 | return nil 124 | } 125 | 126 | func bindToMap(rows *sql.Rows) ([]map[string]interface{}, error) { 127 | cts, err := rows.ColumnTypes() 128 | if err != nil { 129 | return nil, err 130 | } 131 | var ms []map[string]interface{} 132 | for rows.Next() { 133 | var ptrs []interface{} 134 | for _, ct := range cts { 135 | ptrs = append(ptrs, reflect.New(ct.ScanType()).Interface()) 136 | } 137 | 138 | err = rows.Scan(ptrs...) 139 | if err != nil { 140 | return nil, err 141 | } 142 | m := map[string]interface{}{} 143 | for i, ptr := range ptrs { 144 | m[cts[i].Name()] = reflect.ValueOf(ptr).Elem().Interface() 145 | } 146 | 147 | ms = append(ms, m) 148 | } 149 | return ms, nil 150 | } 151 | -------------------------------------------------------------------------------- /binder_test.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | "time" 7 | 8 | "github.com/DATA-DOG/go-sqlmock" 9 | 10 | _ "github.com/lib/pq" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type User struct { 15 | ID int64 16 | Name string 17 | Timestamps 18 | } 19 | 20 | func (u User) ConfigureEntity(e *EntityConfigurator) { 21 | e.Table("users") 22 | } 23 | 24 | type Address struct { 25 | ID int 26 | Path string 27 | } 28 | 29 | func TestBind(t *testing.T) { 30 | t.Run("single result", func(t *testing.T) { 31 | db, mock, err := sqlmock.New() 32 | assert.NoError(t, err) 33 | mock. 34 | ExpectQuery("SELECT .* FROM users"). 35 | WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at", "deleted_at"}). 36 | AddRow(1, "amirreza", sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{})) 37 | rows, err := db.Query(`SELECT * FROM users`) 38 | assert.NoError(t, err) 39 | 40 | u := &User{} 41 | md := schemaOfHeavyReflectionStuff(u) 42 | err = newBinder(md).bind(rows, u) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, "amirreza", u.Name) 46 | }) 47 | 48 | t.Run("multi result", func(t *testing.T) { 49 | db, mock, err := sqlmock.New() 50 | assert.NoError(t, err) 51 | mock. 52 | ExpectQuery("SELECT .* FROM users"). 53 | WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "amirreza").AddRow(2, "milad")) 54 | 55 | rows, err := db.Query(`SELECT * FROM users`) 56 | assert.NoError(t, err) 57 | 58 | md := schemaOfHeavyReflectionStuff(&User{}) 59 | var users []*User 60 | err = newBinder(md).bind(rows, &users) 61 | assert.NoError(t, err) 62 | 63 | assert.Equal(t, "amirreza", users[0].Name) 64 | assert.Equal(t, "milad", users[1].Name) 65 | }) 66 | } 67 | 68 | func TestBindMap(t *testing.T) { 69 | db, mock, err := sqlmock.New() 70 | assert.NoError(t, err) 71 | mock. 72 | ExpectQuery("SELECT .* FROM users"). 73 | WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at", "deleted_at"}). 74 | AddRow(1, "amirreza", sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{})) 75 | rows, err := db.Query(`SELECT * FROM users`) 76 | assert.NoError(t, err) 77 | 78 | ms, err := bindToMap(rows) 79 | 80 | assert.NoError(t, err) 81 | assert.NotEmpty(t, ms) 82 | 83 | assert.Len(t, ms, 1) 84 | } 85 | -------------------------------------------------------------------------------- /configurators.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/gertd/go-pluralize" 7 | ) 8 | 9 | type EntityConfigurator struct { 10 | connection string 11 | table string 12 | this Entity 13 | relations map[string]interface{} 14 | resolveRelations []func() 15 | columnConstraints []*FieldConfigurator 16 | } 17 | 18 | func newEntityConfigurator() *EntityConfigurator { 19 | return &EntityConfigurator{} 20 | } 21 | 22 | func (ec *EntityConfigurator) Table(name string) *EntityConfigurator { 23 | ec.table = name 24 | return ec 25 | } 26 | 27 | func (ec *EntityConfigurator) Connection(name string) *EntityConfigurator { 28 | ec.connection = name 29 | return ec 30 | } 31 | 32 | func (ec *EntityConfigurator) HasMany(property Entity, config HasManyConfig) *EntityConfigurator { 33 | if ec.relations == nil { 34 | ec.relations = map[string]interface{}{} 35 | } 36 | ec.resolveRelations = append(ec.resolveRelations, func() { 37 | if config.PropertyForeignKey != "" && config.PropertyTable != "" { 38 | ec.relations[config.PropertyTable] = config 39 | return 40 | } 41 | configurator := newEntityConfigurator() 42 | property.ConfigureEntity(configurator) 43 | 44 | if config.PropertyTable == "" { 45 | config.PropertyTable = configurator.table 46 | } 47 | 48 | if config.PropertyForeignKey == "" { 49 | config.PropertyForeignKey = pluralize.NewClient().Singular(ec.table) + "_id" 50 | } 51 | 52 | ec.relations[configurator.table] = config 53 | 54 | return 55 | }) 56 | return ec 57 | } 58 | 59 | func (ec *EntityConfigurator) HasOne(property Entity, config HasOneConfig) *EntityConfigurator { 60 | if ec.relations == nil { 61 | ec.relations = map[string]interface{}{} 62 | } 63 | ec.resolveRelations = append(ec.resolveRelations, func() { 64 | if config.PropertyForeignKey != "" && config.PropertyTable != "" { 65 | ec.relations[config.PropertyTable] = config 66 | return 67 | } 68 | 69 | configurator := newEntityConfigurator() 70 | property.ConfigureEntity(configurator) 71 | 72 | if config.PropertyTable == "" { 73 | config.PropertyTable = configurator.table 74 | } 75 | if config.PropertyForeignKey == "" { 76 | config.PropertyForeignKey = pluralize.NewClient().Singular(ec.table) + "_id" 77 | } 78 | 79 | ec.relations[configurator.table] = config 80 | return 81 | }) 82 | return ec 83 | } 84 | 85 | func (ec *EntityConfigurator) BelongsTo(owner Entity, config BelongsToConfig) *EntityConfigurator { 86 | if ec.relations == nil { 87 | ec.relations = map[string]interface{}{} 88 | } 89 | ec.resolveRelations = append(ec.resolveRelations, func() { 90 | if config.ForeignColumnName != "" && config.LocalForeignKey != "" && config.OwnerTable != "" { 91 | ec.relations[config.OwnerTable] = config 92 | return 93 | } 94 | ownerConfigurator := newEntityConfigurator() 95 | owner.ConfigureEntity(ownerConfigurator) 96 | if config.OwnerTable == "" { 97 | config.OwnerTable = ownerConfigurator.table 98 | } 99 | if config.LocalForeignKey == "" { 100 | config.LocalForeignKey = pluralize.NewClient().Singular(ownerConfigurator.table) + "_id" 101 | } 102 | if config.ForeignColumnName == "" { 103 | config.ForeignColumnName = "id" 104 | } 105 | ec.relations[ownerConfigurator.table] = config 106 | }) 107 | return ec 108 | } 109 | 110 | func (ec *EntityConfigurator) BelongsToMany(owner Entity, config BelongsToManyConfig) *EntityConfigurator { 111 | if ec.relations == nil { 112 | ec.relations = map[string]interface{}{} 113 | } 114 | ec.resolveRelations = append(ec.resolveRelations, func() { 115 | ownerConfigurator := newEntityConfigurator() 116 | owner.ConfigureEntity(ownerConfigurator) 117 | 118 | if config.OwnerLookupColumn == "" { 119 | var pkName string 120 | for _, field := range genericFieldsOf(owner) { 121 | if field.IsPK { 122 | pkName = field.Name 123 | } 124 | } 125 | config.OwnerLookupColumn = pkName 126 | 127 | } 128 | if config.OwnerTable == "" { 129 | config.OwnerTable = ownerConfigurator.table 130 | } 131 | if config.IntermediateTable == "" { 132 | panic("cannot infer intermediate table yet") 133 | } 134 | if config.IntermediatePropertyID == "" { 135 | config.IntermediatePropertyID = pluralize.NewClient().Singular(ownerConfigurator.table) + "_id" 136 | } 137 | if config.IntermediateOwnerID == "" { 138 | config.IntermediateOwnerID = pluralize.NewClient().Singular(ec.table) + "_id" 139 | } 140 | 141 | ec.relations[ownerConfigurator.table] = config 142 | }) 143 | return ec 144 | } 145 | 146 | type FieldConfigurator struct { 147 | fieldName string 148 | nullable sql.NullBool 149 | primaryKey bool 150 | column string 151 | isCreatedAt bool 152 | isUpdatedAt bool 153 | isDeletedAt bool 154 | } 155 | 156 | func (ec *EntityConfigurator) Field(name string) *FieldConfigurator { 157 | cc := &FieldConfigurator{fieldName: name} 158 | ec.columnConstraints = append(ec.columnConstraints, cc) 159 | return cc 160 | } 161 | 162 | func (fc *FieldConfigurator) IsPrimaryKey() *FieldConfigurator { 163 | fc.primaryKey = true 164 | return fc 165 | } 166 | 167 | func (fc *FieldConfigurator) IsCreatedAt() *FieldConfigurator { 168 | fc.isCreatedAt = true 169 | return fc 170 | } 171 | 172 | func (fc *FieldConfigurator) IsUpdatedAt() *FieldConfigurator { 173 | fc.isUpdatedAt = true 174 | return fc 175 | } 176 | 177 | func (fc *FieldConfigurator) IsDeletedAt() *FieldConfigurator { 178 | fc.isDeletedAt = true 179 | return fc 180 | } 181 | 182 | func (fc *FieldConfigurator) ColumnName(name string) *FieldConfigurator { 183 | fc.column = name 184 | return fc 185 | } 186 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/jedib0t/go-pretty/table" 8 | ) 9 | 10 | type connection struct { 11 | Name string 12 | Dialect *Dialect 13 | DB *sql.DB 14 | Schemas map[string]*schema 15 | DBSchema map[string][]columnSpec 16 | DatabaseValidations bool 17 | } 18 | 19 | func (c *connection) inferedTables() []string { 20 | var tables []string 21 | for t, s := range c.Schemas { 22 | tables = append(tables, t) 23 | for _, relC := range s.relations { 24 | if belongsToManyConfig, is := relC.(BelongsToManyConfig); is { 25 | tables = append(tables, belongsToManyConfig.IntermediateTable) 26 | } 27 | } 28 | } 29 | return tables 30 | } 31 | 32 | func (c *connection) validateAllTablesArePresent() error { 33 | for _, inferedTable := range c.inferedTables() { 34 | if _, exists := c.DBSchema[inferedTable]; !exists { 35 | return fmt.Errorf("orm infered %s but it's not found in your database, your database is out of sync", inferedTable) 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | func (c *connection) validateTablesSchemas() error { 42 | // check for entity tables: there should not be any struct field that does not have a coresponding column 43 | for table, sc := range c.Schemas { 44 | if columns, exists := c.DBSchema[table]; exists { 45 | for _, f := range sc.fields { 46 | found := false 47 | for _, c := range columns { 48 | if c.Name == f.Name { 49 | found = true 50 | } 51 | } 52 | if !found { 53 | return fmt.Errorf("column %s not found while it was inferred", f.Name) 54 | } 55 | } 56 | } else { 57 | return fmt.Errorf("tables are out of sync, %s was inferred but not present in database", table) 58 | } 59 | } 60 | 61 | // check for relation tables: for HasMany,HasOne relations check if OWNER pk column is in PROPERTY, 62 | // for BelongsToMany check intermediate table has 2 pk for two entities 63 | 64 | for table, sc := range c.Schemas { 65 | for _, rel := range sc.relations { 66 | switch rel.(type) { 67 | case BelongsToConfig: 68 | columns := c.DBSchema[table] 69 | var found bool 70 | for _, col := range columns { 71 | if col.Name == rel.(BelongsToConfig).LocalForeignKey { 72 | found = true 73 | } 74 | } 75 | if !found { 76 | return fmt.Errorf("cannot find local foreign key %s for relation", rel.(BelongsToConfig).LocalForeignKey) 77 | } 78 | case BelongsToManyConfig: 79 | columns := c.DBSchema[rel.(BelongsToManyConfig).IntermediateTable] 80 | var foundOwner bool 81 | var foundProperty bool 82 | 83 | for _, col := range columns { 84 | if col.Name == rel.(BelongsToManyConfig).IntermediateOwnerID { 85 | foundOwner = true 86 | } 87 | if col.Name == rel.(BelongsToManyConfig).IntermediatePropertyID { 88 | foundProperty = true 89 | } 90 | } 91 | if !foundOwner || !foundProperty { 92 | return fmt.Errorf("table schema for %s is not correct one of foreign keys is not present", rel.(BelongsToManyConfig).IntermediateTable) 93 | } 94 | } 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (c *connection) Schematic() { 102 | fmt.Printf("SQL Dialect: %s\n", c.Dialect.DriverName) 103 | for t, schema := range c.Schemas { 104 | fmt.Printf("t: %s\n", t) 105 | w := table.NewWriter() 106 | w.AppendHeader(table.Row{"SQL Name", "Type", "Is Primary Key", "Is Virtual"}) 107 | for _, field := range schema.fields { 108 | w.AppendRow(table.Row{field.Name, field.Type, field.IsPK, field.Virtual}) 109 | } 110 | fmt.Println(w.Render()) 111 | for _, rel := range schema.relations { 112 | switch rel.(type) { 113 | case HasOneConfig: 114 | fmt.Printf("%s 1-1 %s => %+v\n", t, rel.(HasOneConfig).PropertyTable, rel) 115 | case HasManyConfig: 116 | fmt.Printf("%s 1-N %s => %+v\n", t, rel.(HasManyConfig).PropertyTable, rel) 117 | 118 | case BelongsToConfig: 119 | fmt.Printf("%s N-1 %s => %+v\n", t, rel.(BelongsToConfig).OwnerTable, rel) 120 | 121 | case BelongsToManyConfig: 122 | fmt.Printf("%s N-N %s => %+v\n", t, rel.(BelongsToManyConfig).IntermediateTable, rel) 123 | } 124 | } 125 | fmt.Println("") 126 | } 127 | } 128 | 129 | func (c *connection) getSchema(t string) *schema { 130 | return c.Schemas[t] 131 | } 132 | 133 | func (c *connection) setSchema(e Entity, s *schema) { 134 | var configurator EntityConfigurator 135 | e.ConfigureEntity(&configurator) 136 | c.Schemas[configurator.table] = s 137 | } 138 | 139 | func GetConnection(name string) *connection { 140 | return globalConnections[name] 141 | } 142 | 143 | func (c *connection) exec(q string, args ...any) (sql.Result, error) { 144 | return c.DB.Exec(q, args...) 145 | } 146 | 147 | func (c *connection) query(q string, args ...any) (*sql.Rows, error) { 148 | return c.DB.Query(q, args...) 149 | } 150 | 151 | func (c *connection) queryRow(q string, args ...any) *sql.Row { 152 | return c.DB.QueryRow(q, args...) 153 | } 154 | -------------------------------------------------------------------------------- /dialect.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | type Dialect struct { 9 | DriverName string 10 | PlaceholderChar string 11 | IncludeIndexInPlaceholder bool 12 | AddTableNameInSelectColumns bool 13 | PlaceHolderGenerator func(n int) []string 14 | QueryListTables string 15 | QueryTableSchema string 16 | } 17 | 18 | func getListOfTables(query string) func(db *sql.DB) ([]string, error) { 19 | return func(db *sql.DB) ([]string, error) { 20 | rows, err := db.Query(query) 21 | if err != nil { 22 | return nil, err 23 | } 24 | var tables []string 25 | for rows.Next() { 26 | var table string 27 | err = rows.Scan(&table) 28 | if err != nil { 29 | return nil, err 30 | } 31 | tables = append(tables, table) 32 | } 33 | return tables, nil 34 | } 35 | } 36 | 37 | type columnSpec struct { 38 | //0|id|INTEGER|0||1 39 | Name string 40 | Type string 41 | Nullable bool 42 | DefaultValue sql.NullString 43 | IsPrimaryKey bool 44 | } 45 | 46 | func getTableSchema(query string) func(db *sql.DB, query string) ([]columnSpec, error) { 47 | return func(db *sql.DB, table string) ([]columnSpec, error) { 48 | rows, err := db.Query(fmt.Sprintf(query, table)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | var output []columnSpec 53 | for rows.Next() { 54 | var cs columnSpec 55 | var nullable string 56 | var pk int 57 | err = rows.Scan(&cs.Name, &cs.Type, &nullable, &cs.DefaultValue, &pk) 58 | if err != nil { 59 | return nil, err 60 | } 61 | cs.Nullable = nullable == "notnull" 62 | cs.IsPrimaryKey = pk == 1 63 | output = append(output, cs) 64 | } 65 | return output, nil 66 | 67 | } 68 | } 69 | 70 | var Dialects = &struct { 71 | MySQL *Dialect 72 | PostgreSQL *Dialect 73 | SQLite3 *Dialect 74 | }{ 75 | MySQL: &Dialect{ 76 | DriverName: "mysql", 77 | PlaceholderChar: "?", 78 | IncludeIndexInPlaceholder: false, 79 | AddTableNameInSelectColumns: true, 80 | PlaceHolderGenerator: questionMarks, 81 | QueryListTables: "SHOW TABLES", 82 | QueryTableSchema: "DESCRIBE %s", 83 | }, 84 | PostgreSQL: &Dialect{ 85 | DriverName: "postgres", 86 | PlaceholderChar: "$", 87 | IncludeIndexInPlaceholder: true, 88 | AddTableNameInSelectColumns: true, 89 | PlaceHolderGenerator: postgresPlaceholder, 90 | QueryListTables: `\dt`, 91 | QueryTableSchema: `\d %s`, 92 | }, 93 | SQLite3: &Dialect{ 94 | DriverName: "sqlite3", 95 | PlaceholderChar: "?", 96 | IncludeIndexInPlaceholder: false, 97 | AddTableNameInSelectColumns: false, 98 | PlaceHolderGenerator: questionMarks, 99 | QueryListTables: "SELECT name FROM sqlite_schema WHERE type='table'", 100 | QueryTableSchema: `SELECT name,type,"notnull","dflt_value","pk" FROM PRAGMA_TABLE_INFO('%s')`, 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql/driver" 5 | "github.com/iancoleman/strcase" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type field struct { 11 | Name string 12 | IsPK bool 13 | Virtual bool 14 | IsCreatedAt bool 15 | IsUpdatedAt bool 16 | IsDeletedAt bool 17 | Nullable bool 18 | Default any 19 | Type reflect.Type 20 | } 21 | 22 | func getFieldConfiguratorFor(fieldConfigurators []*FieldConfigurator, name string) *FieldConfigurator { 23 | for _, fc := range fieldConfigurators { 24 | if fc.fieldName == name { 25 | return fc 26 | } 27 | } 28 | return &FieldConfigurator{} 29 | } 30 | 31 | func fieldMetadata(ft reflect.StructField, fieldConfigurators []*FieldConfigurator) []*field { 32 | var fms []*field 33 | fc := getFieldConfiguratorFor(fieldConfigurators, ft.Name) 34 | baseFm := &field{} 35 | baseFm.Type = ft.Type 36 | fms = append(fms, baseFm) 37 | if fc.column != "" { 38 | baseFm.Name = fc.column 39 | } else { 40 | baseFm.Name = strcase.ToSnake(ft.Name) 41 | } 42 | if strings.ToLower(ft.Name) == "id" || fc.primaryKey { 43 | baseFm.IsPK = true 44 | } 45 | if strings.ToLower(ft.Name) == "createdat" || fc.isCreatedAt { 46 | baseFm.IsCreatedAt = true 47 | } 48 | if strings.ToLower(ft.Name) == "updatedat" || fc.isUpdatedAt { 49 | baseFm.IsUpdatedAt = true 50 | } 51 | if strings.ToLower(ft.Name) == "deletedat" || fc.isDeletedAt { 52 | baseFm.IsDeletedAt = true 53 | } 54 | if ft.Type.Kind() == reflect.Struct || ft.Type.Kind() == reflect.Ptr { 55 | t := ft.Type 56 | if ft.Type.Kind() == reflect.Ptr { 57 | t = ft.Type.Elem() 58 | } 59 | if !t.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) { 60 | for i := 0; i < t.NumField(); i++ { 61 | fms = append(fms, fieldMetadata(t.Field(i), fieldConfigurators)...) 62 | } 63 | fms = fms[1:] 64 | } 65 | } 66 | return fms 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/golobby/orm 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/gertd/go-pluralize v0.1.7 8 | github.com/go-sql-driver/mysql v1.6.0 9 | github.com/iancoleman/strcase v0.2.0 10 | github.com/jedib0t/go-pretty v4.3.0+incompatible 11 | github.com/lib/pq v1.10.4 12 | github.com/mattn/go-sqlite3 v1.14.11 13 | github.com/stretchr/testify v1.7.0 14 | ) 15 | 16 | require ( 17 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/go-openapi/errors v0.19.8 // indirect 20 | github.com/go-openapi/strfmt v0.21.2 // indirect 21 | github.com/go-stack/stack v1.8.0 // indirect 22 | github.com/mattn/go-runewidth v0.0.13 // indirect 23 | github.com/mitchellh/mapstructure v1.3.3 // indirect 24 | github.com/oklog/ulid v1.3.1 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/rivo/uniseg v0.2.0 // indirect 27 | go.mongodb.org/mongo-driver v1.7.5 // indirect 28 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect 29 | gopkg.in/yaml.v3 v3.0.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 2 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 3 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= 4 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gertd/go-pluralize v0.1.7 h1:RgvJTJ5W7olOoAks97BOwOlekBFsLEyh00W48Z6ZEZY= 10 | github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvjYgAahcqSDQ= 11 | github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc= 12 | github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 13 | github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os= 14 | github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= 15 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 16 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 17 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 18 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 19 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 20 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 21 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 23 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 25 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 26 | github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= 27 | github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= 28 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 35 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 36 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 37 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 38 | github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ= 39 | github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 40 | github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= 41 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 42 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 43 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 44 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 45 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 46 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 47 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 48 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 49 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 50 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 52 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 56 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 58 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 59 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 60 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 61 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 62 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 63 | go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= 64 | go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= 65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 66 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 67 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 68 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 74 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 79 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 83 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 87 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | -------------------------------------------------------------------------------- /orm.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | "time" 8 | 9 | // Drivers 10 | _ "github.com/go-sql-driver/mysql" 11 | _ "github.com/lib/pq" 12 | _ "github.com/mattn/go-sqlite3" 13 | ) 14 | 15 | var globalConnections = map[string]*connection{} 16 | 17 | // Schematic prints all information ORM inferred from your entities in startup, remember to pass 18 | // your entities in Entities when you call SetupConnections if you want their data inferred 19 | // otherwise Schematic does not print correct data since GoLobby ORM also 20 | // incrementally cache your entities metadata and schema. 21 | func Schematic() { 22 | for conn, connObj := range globalConnections { 23 | fmt.Printf("----------------%s---------------\n", conn) 24 | connObj.Schematic() 25 | fmt.Println("-----------------------------------") 26 | } 27 | } 28 | 29 | type ConnectionConfig struct { 30 | // Name of your database connection, it's up to you to name them anything 31 | // just remember that having a connection name is mandatory if 32 | // you have multiple connections 33 | Name string 34 | // If you already have an active database connection configured pass it in this value and 35 | // do not pass Driver and DSN fields. 36 | DB *sql.DB 37 | // Which dialect of sql to generate queries for, you don't need it most of the times when you are using 38 | // traditional databases such as mysql, sqlite3, postgres. 39 | Dialect *Dialect 40 | // List of entities that you want to use for this connection, remember that you can ignore this field 41 | // and GoLobby ORM will build our metadata cache incrementally but you will lose schematic 42 | // information that we can provide you and also potentialy validations that we 43 | // can do with the database 44 | Entities []Entity 45 | // Database validations, check if all tables exists and also table schemas contains all necessary columns. 46 | // Check if all infered tables exist in your database 47 | DatabaseValidations bool 48 | } 49 | 50 | // SetupConnections declares a new connections for ORM. 51 | func SetupConnections(configs ...ConnectionConfig) error { 52 | 53 | for _, c := range configs { 54 | if err := setupConnection(c); err != nil { 55 | return err 56 | } 57 | } 58 | for _, conn := range globalConnections { 59 | if !conn.DatabaseValidations { 60 | continue 61 | } 62 | 63 | tables, err := getListOfTables(conn.Dialect.QueryListTables)(conn.DB) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, table := range tables { 69 | if conn.DatabaseValidations { 70 | spec, err := getTableSchema(conn.Dialect.QueryTableSchema)(conn.DB, table) 71 | if err != nil { 72 | return err 73 | } 74 | conn.DBSchema[table] = spec 75 | } else { 76 | conn.DBSchema[table] = nil 77 | } 78 | } 79 | 80 | // check tables existence 81 | if conn.DatabaseValidations { 82 | err := conn.validateAllTablesArePresent() 83 | if err != nil { 84 | return err 85 | } 86 | } 87 | 88 | if conn.DatabaseValidations { 89 | err = conn.validateTablesSchemas() 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func setupConnection(config ConnectionConfig) error { 101 | schemas := map[string]*schema{} 102 | if config.Name == "" { 103 | config.Name = "default" 104 | } 105 | 106 | for _, entity := range config.Entities { 107 | s := schemaOfHeavyReflectionStuff(entity) 108 | var configurator EntityConfigurator 109 | entity.ConfigureEntity(&configurator) 110 | schemas[configurator.table] = s 111 | } 112 | 113 | s := &connection{ 114 | Name: config.Name, 115 | DB: config.DB, 116 | Dialect: config.Dialect, 117 | Schemas: schemas, 118 | DBSchema: make(map[string][]columnSpec), 119 | DatabaseValidations: config.DatabaseValidations, 120 | } 121 | 122 | globalConnections[fmt.Sprintf("%s", config.Name)] = s 123 | 124 | return nil 125 | } 126 | 127 | // Entity defines the interface that each of your structs that 128 | // you want to use as database entities should have, 129 | // it's a simple one and its ConfigureEntity. 130 | type Entity interface { 131 | // ConfigureEntity should be defined for all of your database entities 132 | // and it can define Table, DB and also relations of your Entity. 133 | ConfigureEntity(e *EntityConfigurator) 134 | } 135 | 136 | // InsertAll given entities into database based on their ConfigureEntity 137 | // we can find table and also DB name. 138 | func InsertAll(objs ...Entity) error { 139 | if len(objs) == 0 { 140 | return nil 141 | } 142 | s := getSchemaFor(objs[0]) 143 | cols := s.Columns(false) 144 | var values [][]interface{} 145 | for _, obj := range objs { 146 | createdAtF := s.createdAt() 147 | if createdAtF != nil { 148 | genericSet(obj, createdAtF.Name, sql.NullTime{Time: time.Now(), Valid: true}) 149 | } 150 | updatedAtF := s.updatedAt() 151 | if updatedAtF != nil { 152 | genericSet(obj, updatedAtF.Name, sql.NullTime{Time: time.Now(), Valid: true}) 153 | } 154 | values = append(values, genericValuesOf(obj, false)) 155 | } 156 | 157 | is := insertStmt{ 158 | PlaceHolderGenerator: s.getDialect().PlaceHolderGenerator, 159 | Table: s.getTable(), 160 | Columns: cols, 161 | Values: values, 162 | } 163 | 164 | q, args := is.ToSql() 165 | 166 | _, err := s.getConnection().exec(q, args...) 167 | if err != nil { 168 | return err 169 | } 170 | return nil 171 | } 172 | 173 | // Insert given entity into database based on their ConfigureEntity 174 | // we can find table and also DB name. 175 | func Insert(o Entity) error { 176 | s := getSchemaFor(o) 177 | cols := s.Columns(false) 178 | var values [][]interface{} 179 | createdAtF := s.createdAt() 180 | if createdAtF != nil { 181 | genericSet(o, createdAtF.Name, sql.NullTime{Time: time.Now(), Valid: true}) 182 | } 183 | updatedAtF := s.updatedAt() 184 | if updatedAtF != nil { 185 | genericSet(o, updatedAtF.Name, sql.NullTime{Time: time.Now(), Valid: true}) 186 | } 187 | values = append(values, genericValuesOf(o, false)) 188 | 189 | is := insertStmt{ 190 | PlaceHolderGenerator: s.getDialect().PlaceHolderGenerator, 191 | Table: s.getTable(), 192 | Columns: cols, 193 | Values: values, 194 | } 195 | 196 | if s.getDialect().DriverName == "postgres" { 197 | is.Returning = s.pkName() 198 | } 199 | q, args := is.ToSql() 200 | 201 | res, err := s.getConnection().exec(q, args...) 202 | if err != nil { 203 | return err 204 | } 205 | id, err := res.LastInsertId() 206 | if err != nil { 207 | return err 208 | } 209 | 210 | if s.pkName() != "" { 211 | // intermediate tables usually have no single pk column. 212 | s.setPK(o, id) 213 | } 214 | return nil 215 | } 216 | 217 | func isZero(val interface{}) bool { 218 | switch val.(type) { 219 | case int64: 220 | return val.(int64) == 0 221 | case int: 222 | return val.(int) == 0 223 | case string: 224 | return val.(string) == "" 225 | default: 226 | return reflect.ValueOf(val).Elem().IsZero() 227 | } 228 | } 229 | 230 | // Save saves given entity, if primary key is set 231 | // we will make an update query and if 232 | // primary key is zero value we will 233 | // insert it. 234 | func Save(obj Entity) error { 235 | if isZero(getSchemaFor(obj).getPK(obj)) { 236 | return Insert(obj) 237 | } else { 238 | return Update(obj) 239 | } 240 | } 241 | 242 | // Find finds the Entity you want based on generic type and primary key you passed. 243 | func Find[T Entity](id interface{}) (T, error) { 244 | var q string 245 | out := new(T) 246 | md := getSchemaFor(*out) 247 | q, args, err := NewQueryBuilder[T](md). 248 | SetDialect(md.getDialect()). 249 | Table(md.Table). 250 | Select(md.Columns(true)...). 251 | Where(md.pkName(), id). 252 | ToSql() 253 | if err != nil { 254 | return *out, err 255 | } 256 | err = bind[T](out, q, args) 257 | 258 | if err != nil { 259 | return *out, err 260 | } 261 | 262 | return *out, nil 263 | } 264 | 265 | func toKeyValues(obj Entity, withPK bool) []any { 266 | var tuples []any 267 | vs := genericValuesOf(obj, withPK) 268 | cols := getSchemaFor(obj).Columns(withPK) 269 | for i, col := range cols { 270 | tuples = append(tuples, col, vs[i]) 271 | } 272 | return tuples 273 | } 274 | 275 | // Update given Entity in database. 276 | func Update(obj Entity) error { 277 | s := getSchemaFor(obj) 278 | q, args, err := NewQueryBuilder[Entity](s). 279 | SetDialect(s.getDialect()). 280 | Set(toKeyValues(obj, false)...). 281 | Where(s.pkName(), genericGetPKValue(obj)).Table(s.Table).ToSql() 282 | 283 | if err != nil { 284 | return err 285 | } 286 | _, err = s.getConnection().exec(q, args...) 287 | return err 288 | } 289 | 290 | // Delete given Entity from database 291 | func Delete(obj Entity) error { 292 | s := getSchemaFor(obj) 293 | genericSet(obj, "deleted_at", sql.NullTime{Time: time.Now(), Valid: true}) 294 | query, args, err := NewQueryBuilder[Entity](s).SetDialect(s.getDialect()).Table(s.Table).Where(s.pkName(), genericGetPKValue(obj)).SetDelete().ToSql() 295 | if err != nil { 296 | return err 297 | } 298 | _, err = s.getConnection().exec(query, args...) 299 | return err 300 | } 301 | 302 | func bind[T Entity](output interface{}, q string, args []interface{}) error { 303 | outputMD := getSchemaFor(*new(T)) 304 | rows, err := outputMD.getConnection().query(q, args...) 305 | if err != nil { 306 | return err 307 | } 308 | return newBinder(outputMD).bind(rows, output) 309 | } 310 | 311 | // HasManyConfig contains all information we need for querying HasMany relationships. 312 | // We can infer both fields if you have them in standard way but you 313 | // can specify them if you want custom ones. 314 | type HasManyConfig struct { 315 | // PropertyTable is table of the property of HasMany relationship, 316 | // consider `Comment` in Post and Comment relationship, 317 | // each Post HasMany Comment, so PropertyTable is 318 | // `comments`. 319 | PropertyTable string 320 | // PropertyForeignKey is the foreign key field name in the property table, 321 | // for example in Post HasMany Comment, if comment has `post_id` field, 322 | // it's the PropertyForeignKey field. 323 | PropertyForeignKey string 324 | } 325 | 326 | // HasMany configures a QueryBuilder for a HasMany relationship 327 | // this relationship will be defined for owner argument 328 | // that has many of PROPERTY generic type for example 329 | // HasMany[Comment](&Post{}) 330 | // is for Post HasMany Comment relationship. 331 | func HasMany[PROPERTY Entity](owner Entity) *QueryBuilder[PROPERTY] { 332 | outSchema := getSchemaFor(*new(PROPERTY)) 333 | 334 | q := NewQueryBuilder[PROPERTY](outSchema) 335 | // getting config from our cache 336 | c, ok := getSchemaFor(owner).relations[outSchema.Table].(HasManyConfig) 337 | if !ok { 338 | q.err = fmt.Errorf("wrong config passed for HasMany") 339 | } 340 | 341 | s := getSchemaFor(owner) 342 | return q. 343 | SetDialect(s.getDialect()). 344 | Table(c.PropertyTable). 345 | Select(outSchema.Columns(true)...). 346 | Where(c.PropertyForeignKey, genericGetPKValue(owner)) 347 | } 348 | 349 | // HasOneConfig contains all information we need for a HasOne relationship, 350 | // it's similar to HasManyConfig. 351 | type HasOneConfig struct { 352 | // PropertyTable is table of the property of HasOne relationship, 353 | // consider `HeaderPicture` in Post and HeaderPicture relationship, 354 | // each Post HasOne HeaderPicture, so PropertyTable is 355 | // `header_pictures`. 356 | PropertyTable string 357 | // PropertyForeignKey is the foreign key field name in the property table, 358 | // forexample in Post HasOne HeaderPicture, if header_picture has `post_id` field, 359 | // it's the PropertyForeignKey field. 360 | PropertyForeignKey string 361 | } 362 | 363 | // HasOne configures a QueryBuilder for a HasOne relationship 364 | // this relationship will be defined for owner argument 365 | // that has one of PROPERTY generic type for example 366 | // HasOne[HeaderPicture](&Post{}) 367 | // is for Post HasOne HeaderPicture relationship. 368 | func HasOne[PROPERTY Entity](owner Entity) *QueryBuilder[PROPERTY] { 369 | property := getSchemaFor(*new(PROPERTY)) 370 | q := NewQueryBuilder[PROPERTY](property) 371 | c, ok := getSchemaFor(owner).relations[property.Table].(HasOneConfig) 372 | if !ok { 373 | q.err = fmt.Errorf("wrong config passed for HasOne") 374 | } 375 | 376 | // settings default config Values 377 | return q. 378 | SetDialect(property.getDialect()). 379 | Table(c.PropertyTable). 380 | Select(property.Columns(true)...). 381 | Where(c.PropertyForeignKey, genericGetPKValue(owner)) 382 | } 383 | 384 | // BelongsToConfig contains all information we need for a BelongsTo relationship 385 | // BelongsTo is a relationship between a Comment and it's Post, 386 | // A Comment BelongsTo Post. 387 | type BelongsToConfig struct { 388 | // OwnerTable is the table that contains owner of a BelongsTo 389 | // relationship. 390 | OwnerTable string 391 | // LocalForeignKey is name of the field that links property 392 | // to its owner in BelongsTo relation. for example when 393 | // a Comment BelongsTo Post, LocalForeignKey is 394 | // post_id of Comment. 395 | LocalForeignKey string 396 | // ForeignColumnName is name of the field that LocalForeignKey 397 | // field value will point to it, for example when 398 | // a Comment BelongsTo Post, ForeignColumnName is 399 | // id of Post. 400 | ForeignColumnName string 401 | } 402 | 403 | // BelongsTo configures a QueryBuilder for a BelongsTo relationship between 404 | // OWNER type parameter and property argument, so 405 | // property BelongsTo OWNER. 406 | func BelongsTo[OWNER Entity](property Entity) *QueryBuilder[OWNER] { 407 | owner := getSchemaFor(*new(OWNER)) 408 | q := NewQueryBuilder[OWNER](owner) 409 | c, ok := getSchemaFor(property).relations[owner.Table].(BelongsToConfig) 410 | if !ok { 411 | q.err = fmt.Errorf("wrong config passed for BelongsTo") 412 | } 413 | 414 | ownerIDidx := 0 415 | for idx, field := range owner.fields { 416 | if field.Name == c.LocalForeignKey { 417 | ownerIDidx = idx 418 | } 419 | } 420 | 421 | ownerID := genericValuesOf(property, true)[ownerIDidx] 422 | 423 | return q. 424 | SetDialect(owner.getDialect()). 425 | Table(c.OwnerTable).Select(owner.Columns(true)...). 426 | Where(c.ForeignColumnName, ownerID) 427 | 428 | } 429 | 430 | // BelongsToManyConfig contains information that we 431 | // need for creating many to many queries. 432 | type BelongsToManyConfig struct { 433 | // IntermediateTable is the name of the middle table 434 | // in a BelongsToMany (Many to Many) relationship. 435 | // for example when we have Post BelongsToMany 436 | // Category, this table will be post_categories 437 | // table, remember that this field cannot be 438 | // inferred. 439 | IntermediateTable string 440 | // IntermediatePropertyID is the name of the field name 441 | // of property foreign key in intermediate table, 442 | // for example when we have Post BelongsToMany 443 | // Category, in post_categories table, it would 444 | // be post_id. 445 | IntermediatePropertyID string 446 | // IntermediateOwnerID is the name of the field name 447 | // of property foreign key in intermediate table, 448 | // for example when we have Post BelongsToMany 449 | // Category, in post_categories table, it would 450 | // be category_id. 451 | IntermediateOwnerID string 452 | // Table name of the owner in BelongsToMany relation, 453 | // for example in Post BelongsToMany Category 454 | // Owner table is name of Category table 455 | // for example `categories`. 456 | OwnerTable string 457 | // OwnerLookupColumn is name of the field in the owner 458 | // table that is used in query, for example in Post BelongsToMany Category 459 | // Owner lookup field would be Category primary key which is id. 460 | OwnerLookupColumn string 461 | } 462 | 463 | // BelongsToMany configures a QueryBuilder for a BelongsToMany relationship 464 | func BelongsToMany[OWNER Entity](property Entity) *QueryBuilder[OWNER] { 465 | out := *new(OWNER) 466 | outSchema := getSchemaFor(out) 467 | q := NewQueryBuilder[OWNER](outSchema) 468 | c, ok := getSchemaFor(property).relations[outSchema.Table].(BelongsToManyConfig) 469 | if !ok { 470 | q.err = fmt.Errorf("wrong config passed for HasMany") 471 | } 472 | return q. 473 | Select(outSchema.Columns(true)...). 474 | Table(outSchema.Table). 475 | WhereIn(c.OwnerLookupColumn, Raw(fmt.Sprintf(`SELECT %s FROM %s WHERE %s = ?`, 476 | c.IntermediatePropertyID, 477 | c.IntermediateTable, c.IntermediateOwnerID), genericGetPKValue(property))) 478 | } 479 | 480 | // Add adds `items` to `to` using relations defined between items and to in ConfigureEntity method of `to`. 481 | func Add(to Entity, items ...Entity) error { 482 | if len(items) == 0 { 483 | return nil 484 | } 485 | rels := getSchemaFor(to).relations 486 | tname := getSchemaFor(items[0]).Table 487 | c, ok := rels[tname] 488 | if !ok { 489 | return fmt.Errorf("no config found for given to and item...") 490 | } 491 | switch c.(type) { 492 | case HasManyConfig: 493 | return addProperty(to, items...) 494 | case HasOneConfig: 495 | return addProperty(to, items[0]) 496 | case BelongsToManyConfig: 497 | return addM2M(to, items...) 498 | default: 499 | return fmt.Errorf("cannot add for relation: %T", rels[getSchemaFor(items[0]).Table]) 500 | } 501 | } 502 | 503 | func addM2M(to Entity, items ...Entity) error { 504 | //TODO: Optimize this 505 | rels := getSchemaFor(to).relations 506 | tname := getSchemaFor(items[0]).Table 507 | c := rels[tname].(BelongsToManyConfig) 508 | var values [][]interface{} 509 | ownerPk := genericGetPKValue(to) 510 | for _, item := range items { 511 | pk := genericGetPKValue(item) 512 | if isZero(pk) { 513 | err := Insert(item) 514 | if err != nil { 515 | return err 516 | } 517 | pk = genericGetPKValue(item) 518 | } 519 | values = append(values, []interface{}{ownerPk, pk}) 520 | } 521 | i := insertStmt{ 522 | PlaceHolderGenerator: getSchemaFor(to).getDialect().PlaceHolderGenerator, 523 | Table: c.IntermediateTable, 524 | Columns: []string{c.IntermediateOwnerID, c.IntermediatePropertyID}, 525 | Values: values, 526 | } 527 | 528 | q, args := i.ToSql() 529 | 530 | _, err := getConnectionFor(items[0]).DB.Exec(q, args...) 531 | if err != nil { 532 | return err 533 | } 534 | 535 | return err 536 | } 537 | 538 | // addHasMany(Post, comments) 539 | func addProperty(to Entity, items ...Entity) error { 540 | var lastTable string 541 | for _, obj := range items { 542 | s := getSchemaFor(obj) 543 | if lastTable == "" { 544 | lastTable = s.Table 545 | } else { 546 | if lastTable != s.Table { 547 | return fmt.Errorf("cannot batch insert for two different tables: %s and %s", s.Table, lastTable) 548 | } 549 | } 550 | } 551 | i := insertStmt{ 552 | PlaceHolderGenerator: getSchemaFor(to).getDialect().PlaceHolderGenerator, 553 | Table: getSchemaFor(items[0]).getTable(), 554 | } 555 | ownerPKIdx := -1 556 | ownerPKName := getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey 557 | for idx, col := range getSchemaFor(items[0]).Columns(false) { 558 | if col == ownerPKName { 559 | ownerPKIdx = idx 560 | } 561 | } 562 | 563 | ownerPK := genericGetPKValue(to) 564 | if ownerPKIdx != -1 { 565 | cols := getSchemaFor(items[0]).Columns(false) 566 | i.Columns = append(i.Columns, cols...) 567 | // Owner PK is present in the items struct 568 | for _, item := range items { 569 | vals := genericValuesOf(item, false) 570 | if cols[ownerPKIdx] != getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey { 571 | return fmt.Errorf("owner pk idx is not correct") 572 | } 573 | vals[ownerPKIdx] = ownerPK 574 | i.Values = append(i.Values, vals) 575 | } 576 | } else { 577 | ownerPKIdx = 0 578 | cols := getSchemaFor(items[0]).Columns(false) 579 | cols = append(cols[:ownerPKIdx+1], cols[ownerPKIdx:]...) 580 | cols[ownerPKIdx] = getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey 581 | i.Columns = append(i.Columns, cols...) 582 | for _, item := range items { 583 | vals := genericValuesOf(item, false) 584 | if cols[ownerPKIdx] != getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey { 585 | return fmt.Errorf("owner pk idx is not correct") 586 | } 587 | vals = append(vals[:ownerPKIdx+1], vals[ownerPKIdx:]...) 588 | vals[ownerPKIdx] = ownerPK 589 | i.Values = append(i.Values, vals) 590 | } 591 | } 592 | 593 | q, args := i.ToSql() 594 | 595 | _, err := getConnectionFor(items[0]).DB.Exec(q, args...) 596 | if err != nil { 597 | return err 598 | } 599 | 600 | return err 601 | 602 | } 603 | 604 | // Query creates a new QueryBuilder for given type parameter, sets dialect and table as well. 605 | func Query[E Entity]() *QueryBuilder[E] { 606 | s := getSchemaFor(*new(E)) 607 | q := NewQueryBuilder[E](s) 608 | q.SetDialect(s.getDialect()).Table(s.Table) 609 | return q 610 | } 611 | 612 | // ExecRaw executes given query string and arguments on given type parameter database connection. 613 | func ExecRaw[E Entity](q string, args ...interface{}) (int64, int64, error) { 614 | e := new(E) 615 | 616 | res, err := getSchemaFor(*e).getSQLDB().Exec(q, args...) 617 | if err != nil { 618 | return 0, 0, err 619 | } 620 | 621 | id, err := res.LastInsertId() 622 | if err != nil { 623 | return 0, 0, err 624 | } 625 | 626 | affected, err := res.RowsAffected() 627 | if err != nil { 628 | return 0, 0, err 629 | } 630 | 631 | return id, affected, nil 632 | } 633 | 634 | // QueryRaw queries given query string and arguments on given type parameter database connection. 635 | func QueryRaw[OUTPUT Entity](q string, args ...interface{}) ([]OUTPUT, error) { 636 | o := new(OUTPUT) 637 | rows, err := getSchemaFor(*o).getSQLDB().Query(q, args...) 638 | if err != nil { 639 | return nil, err 640 | } 641 | var output []OUTPUT 642 | err = newBinder(getSchemaFor(*o)).bind(rows, &output) 643 | if err != nil { 644 | return nil, err 645 | } 646 | return output, nil 647 | } 648 | -------------------------------------------------------------------------------- /orm_test.go: -------------------------------------------------------------------------------- 1 | package orm_test 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/golobby/orm" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type AuthorEmail struct { 12 | ID int64 13 | Email string 14 | } 15 | 16 | func (a AuthorEmail) ConfigureEntity(e *orm.EntityConfigurator) { 17 | e. 18 | Table("emails"). 19 | Connection("default"). 20 | BelongsTo(&Post{}, orm.BelongsToConfig{}) 21 | } 22 | 23 | type HeaderPicture struct { 24 | ID int64 25 | PostID int64 26 | Link string 27 | } 28 | 29 | func (h HeaderPicture) ConfigureEntity(e *orm.EntityConfigurator) { 30 | e.Table("header_pictures").BelongsTo(&Post{}, orm.BelongsToConfig{}) 31 | } 32 | 33 | type Post struct { 34 | ID int64 35 | BodyText string 36 | CreatedAt sql.NullTime 37 | UpdatedAt sql.NullTime 38 | DeletedAt sql.NullTime 39 | } 40 | 41 | func (p Post) ConfigureEntity(e *orm.EntityConfigurator) { 42 | e.Field("BodyText").ColumnName("body") 43 | e.Field("ID").ColumnName("id") 44 | e. 45 | Table("posts"). 46 | HasMany(Comment{}, orm.HasManyConfig{}). 47 | HasOne(HeaderPicture{}, orm.HasOneConfig{}). 48 | HasOne(AuthorEmail{}, orm.HasOneConfig{}). 49 | BelongsToMany(Category{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"}) 50 | 51 | } 52 | 53 | func (p *Post) Categories() ([]Category, error) { 54 | return orm.BelongsToMany[Category](p).All() 55 | } 56 | 57 | func (p *Post) Comments() *orm.QueryBuilder[Comment] { 58 | return orm.HasMany[Comment](p) 59 | } 60 | 61 | type Comment struct { 62 | ID int64 63 | PostID int64 64 | Body string 65 | } 66 | 67 | func (c Comment) ConfigureEntity(e *orm.EntityConfigurator) { 68 | e.Table("comments").BelongsTo(&Post{}, orm.BelongsToConfig{}) 69 | } 70 | 71 | func (c *Comment) Post() (Post, error) { 72 | return orm.BelongsTo[Post](c).Get() 73 | } 74 | 75 | type Category struct { 76 | ID int64 77 | Title string 78 | } 79 | 80 | func (c Category) ConfigureEntity(e *orm.EntityConfigurator) { 81 | e.Table("categories").BelongsToMany(Post{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"}) 82 | } 83 | 84 | func (c Category) Posts() ([]Post, error) { 85 | return orm.BelongsToMany[Post](c).All() 86 | } 87 | 88 | // enough models let's test 89 | // Entities is mandatory 90 | // Errors should be carried 91 | 92 | func setup() error { 93 | db, err := sql.Open("sqlite3", ":memory:") 94 | if err != nil { 95 | return err 96 | } 97 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`) 98 | if err != nil { 99 | return err 100 | } 101 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`) 102 | if err != nil { 103 | return err 104 | } 105 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`) 106 | if err != nil { 107 | return err 108 | } 109 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`) 110 | if err != nil { 111 | return err 112 | } 113 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`) 114 | if err != nil { 115 | return err 116 | } 117 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`) 118 | if err != nil { 119 | return err 120 | } 121 | return orm.SetupConnections(orm.ConnectionConfig{ 122 | Name: "default", 123 | DB: db, 124 | Dialect: orm.Dialects.SQLite3, 125 | Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}}, 126 | DatabaseValidations: true, 127 | }) 128 | } 129 | 130 | func TestFind(t *testing.T) { 131 | err := setup() 132 | assert.NoError(t, err) 133 | err = orm.InsertAll(&Post{ 134 | BodyText: "my body for insert", 135 | }) 136 | 137 | assert.NoError(t, err) 138 | 139 | post, err := orm.Find[Post](1) 140 | assert.NoError(t, err) 141 | assert.Equal(t, "my body for insert", post.BodyText) 142 | assert.Equal(t, int64(1), post.ID) 143 | } 144 | 145 | func TestInsert(t *testing.T) { 146 | err := setup() 147 | assert.NoError(t, err) 148 | post := &Post{ 149 | BodyText: "my body for insert", 150 | } 151 | err = orm.Insert(post) 152 | assert.NoError(t, err) 153 | assert.Equal(t, int64(1), post.ID) 154 | var p Post 155 | assert.NoError(t, 156 | orm.GetConnection("default").DB.QueryRow(`SELECT id, body FROM posts where id = ?`, 1).Scan(&p.ID, &p.BodyText)) 157 | 158 | assert.Equal(t, "my body for insert", p.BodyText) 159 | } 160 | func TestInsertAll(t *testing.T) { 161 | err := setup() 162 | assert.NoError(t, err) 163 | post1 := &Post{ 164 | BodyText: "Body1", 165 | } 166 | post2 := &Post{ 167 | BodyText: "Body2", 168 | } 169 | 170 | post3 := &Post{ 171 | BodyText: "Body3", 172 | } 173 | 174 | err = orm.InsertAll(post1, post2, post3) 175 | assert.NoError(t, err) 176 | var counter int 177 | assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT count(id) FROM posts`).Scan(&counter)) 178 | assert.Equal(t, 3, counter) 179 | 180 | } 181 | func TestUpdateORM(t *testing.T) { 182 | err := setup() 183 | assert.NoError(t, err) 184 | post := &Post{ 185 | BodyText: "my body for insert", 186 | } 187 | err = orm.Insert(post) 188 | assert.NoError(t, err) 189 | assert.Equal(t, int64(1), post.ID) 190 | 191 | post.BodyText += " update text" 192 | assert.NoError(t, orm.Update(post)) 193 | 194 | var body string 195 | assert.NoError(t, 196 | orm.GetConnection("default").DB.QueryRow(`SELECT body FROM posts where id = ?`, post.ID).Scan(&body)) 197 | 198 | assert.Equal(t, "my body for insert update text", body) 199 | } 200 | 201 | func TestDeleteORM(t *testing.T) { 202 | err := setup() 203 | assert.NoError(t, err) 204 | post := &Post{ 205 | BodyText: "my body for insert", 206 | } 207 | err = orm.Insert(post) 208 | assert.NoError(t, err) 209 | assert.Equal(t, int64(1), post.ID) 210 | 211 | assert.NoError(t, orm.Delete(post)) 212 | 213 | var count int 214 | assert.NoError(t, 215 | orm.GetConnection("default").DB.QueryRow(`SELECT count(id) FROM posts where id = ?`, post.ID).Scan(&count)) 216 | 217 | assert.Equal(t, 0, count) 218 | } 219 | func TestAdd_HasMany(t *testing.T) { 220 | err := setup() 221 | assert.NoError(t, err) 222 | post := &Post{ 223 | BodyText: "my body for insert", 224 | } 225 | err = orm.Insert(post) 226 | assert.NoError(t, err) 227 | assert.Equal(t, int64(1), post.ID) 228 | 229 | err = orm.Add(post, []orm.Entity{ 230 | Comment{ 231 | Body: "comment 1", 232 | }, 233 | Comment{ 234 | Body: "comment 2", 235 | }, 236 | }...) 237 | // orm.Query(qm.WhereBetween()) 238 | assert.NoError(t, err) 239 | var count int 240 | assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(id) FROM comments`).Scan(&count)) 241 | assert.Equal(t, 2, count) 242 | 243 | comment, err := orm.Find[Comment](1) 244 | assert.NoError(t, err) 245 | 246 | assert.Equal(t, int64(1), comment.PostID) 247 | 248 | } 249 | 250 | func TestAdd_ManyToMany(t *testing.T) { 251 | err := setup() 252 | assert.NoError(t, err) 253 | post := &Post{ 254 | BodyText: "my body for insert", 255 | } 256 | err = orm.Insert(post) 257 | assert.NoError(t, err) 258 | assert.Equal(t, int64(1), post.ID) 259 | 260 | err = orm.Add(post, []orm.Entity{ 261 | &Category{ 262 | Title: "cat 1", 263 | }, 264 | &Category{ 265 | Title: "cat 2", 266 | }, 267 | }...) 268 | assert.NoError(t, err) 269 | var count int 270 | assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(post_id) FROM post_categories`).Scan(&count)) 271 | assert.Equal(t, 2, count) 272 | assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(id) FROM categories`).Scan(&count)) 273 | assert.Equal(t, 2, count) 274 | 275 | categories, err := post.Categories() 276 | assert.NoError(t, err) 277 | 278 | assert.Equal(t, 2, len(categories)) 279 | assert.Equal(t, int64(1), categories[0].ID) 280 | assert.Equal(t, int64(2), categories[1].ID) 281 | 282 | } 283 | 284 | func TestSave(t *testing.T) { 285 | t.Run("save should insert", func(t *testing.T) { 286 | err := setup() 287 | assert.NoError(t, err) 288 | post := &Post{ 289 | BodyText: "1", 290 | } 291 | assert.NoError(t, orm.Save(post)) 292 | assert.Equal(t, int64(1), post.ID) 293 | }) 294 | 295 | t.Run("save should update", func(t *testing.T) { 296 | err := setup() 297 | assert.NoError(t, err) 298 | 299 | post := &Post{ 300 | BodyText: "1", 301 | } 302 | assert.NoError(t, orm.Save(post)) 303 | assert.Equal(t, int64(1), post.ID) 304 | 305 | post.BodyText += "2" 306 | assert.NoError(t, orm.Save(post)) 307 | 308 | myPost, err := orm.Find[Post](1) 309 | assert.NoError(t, err) 310 | 311 | assert.EqualValues(t, post.BodyText, myPost.BodyText) 312 | }) 313 | 314 | } 315 | 316 | func TestHasMany(t *testing.T) { 317 | err := setup() 318 | assert.NoError(t, err) 319 | 320 | post := &Post{ 321 | BodyText: "first post", 322 | } 323 | assert.NoError(t, orm.Save(post)) 324 | assert.Equal(t, int64(1), post.ID) 325 | 326 | assert.NoError(t, orm.Save(&Comment{ 327 | PostID: post.ID, 328 | Body: "comment 1", 329 | })) 330 | assert.NoError(t, orm.Save(&Comment{ 331 | PostID: post.ID, 332 | Body: "comment 2", 333 | })) 334 | 335 | comments, err := orm.HasMany[Comment](post).All() 336 | assert.NoError(t, err) 337 | 338 | assert.Len(t, comments, 2) 339 | 340 | assert.Equal(t, post.ID, comments[0].PostID) 341 | assert.Equal(t, post.ID, comments[1].PostID) 342 | } 343 | 344 | func TestBelongsTo(t *testing.T) { 345 | err := setup() 346 | assert.NoError(t, err) 347 | 348 | post := &Post{ 349 | BodyText: "first post", 350 | } 351 | assert.NoError(t, orm.Save(post)) 352 | assert.Equal(t, int64(1), post.ID) 353 | 354 | comment := &Comment{ 355 | PostID: post.ID, 356 | Body: "comment 1", 357 | } 358 | assert.NoError(t, orm.Save(comment)) 359 | 360 | post2, err := orm.BelongsTo[Post](comment).Get() 361 | assert.NoError(t, err) 362 | 363 | assert.Equal(t, post.BodyText, post2.BodyText) 364 | } 365 | 366 | func TestHasOne(t *testing.T) { 367 | err := setup() 368 | assert.NoError(t, err) 369 | 370 | post := &Post{ 371 | BodyText: "first post", 372 | } 373 | assert.NoError(t, orm.Save(post)) 374 | assert.Equal(t, int64(1), post.ID) 375 | 376 | headerPicture := &HeaderPicture{ 377 | PostID: post.ID, 378 | Link: "google", 379 | } 380 | assert.NoError(t, orm.Save(headerPicture)) 381 | 382 | c1, err := orm.HasOne[HeaderPicture](post).Get() 383 | assert.NoError(t, err) 384 | 385 | assert.Equal(t, headerPicture.PostID, c1.PostID) 386 | } 387 | 388 | func TestBelongsToMany(t *testing.T) { 389 | err := setup() 390 | assert.NoError(t, err) 391 | 392 | post := &Post{ 393 | BodyText: "first Post", 394 | } 395 | 396 | assert.NoError(t, orm.Save(post)) 397 | assert.Equal(t, int64(1), post.ID) 398 | 399 | category := &Category{ 400 | Title: "first category", 401 | } 402 | assert.NoError(t, orm.Save(category)) 403 | assert.Equal(t, int64(1), category.ID) 404 | 405 | _, _, err = orm.ExecRaw[Category](`INSERT INTO post_categories (post_id, category_id) VALUES (?,?)`, post.ID, category.ID) 406 | assert.NoError(t, err) 407 | 408 | categories, err := orm.BelongsToMany[Category](post).All() 409 | assert.NoError(t, err) 410 | 411 | assert.Len(t, categories, 1) 412 | } 413 | 414 | func TestSchematic(t *testing.T) { 415 | err := setup() 416 | assert.NoError(t, err) 417 | 418 | orm.Schematic() 419 | } 420 | 421 | func TestAddProperty(t *testing.T) { 422 | t.Run("having pk value", func(t *testing.T) { 423 | err := setup() 424 | assert.NoError(t, err) 425 | 426 | post := &Post{ 427 | BodyText: "first post", 428 | } 429 | 430 | assert.NoError(t, orm.Save(post)) 431 | assert.EqualValues(t, 1, post.ID) 432 | 433 | err = orm.Add(post, &Comment{PostID: post.ID, Body: "firstComment"}) 434 | assert.NoError(t, err) 435 | 436 | var comment Comment 437 | assert.NoError(t, orm.GetConnection("default"). 438 | DB. 439 | QueryRow(`SELECT id, post_id, body FROM comments WHERE post_id=?`, post.ID). 440 | Scan(&comment.ID, &comment.PostID, &comment.Body)) 441 | 442 | assert.EqualValues(t, post.ID, comment.PostID) 443 | }) 444 | t.Run("not having PK value", func(t *testing.T) { 445 | err := setup() 446 | assert.NoError(t, err) 447 | 448 | post := &Post{ 449 | BodyText: "first post", 450 | } 451 | assert.NoError(t, orm.Save(post)) 452 | assert.EqualValues(t, 1, post.ID) 453 | 454 | err = orm.Add(post, &AuthorEmail{Email: "myemail"}) 455 | assert.NoError(t, err) 456 | 457 | emails, err := orm.QueryRaw[AuthorEmail](`SELECT id, email FROM emails WHERE post_id=?`, post.ID) 458 | 459 | assert.NoError(t, err) 460 | assert.Equal(t, []AuthorEmail{{ID: 1, Email: "myemail"}}, emails) 461 | }) 462 | } 463 | 464 | func TestQuery(t *testing.T) { 465 | t.Run("querying single row", func(t *testing.T) { 466 | err := setup() 467 | assert.NoError(t, err) 468 | 469 | assert.NoError(t, orm.Save(&Post{BodyText: "body 1"})) 470 | // post, err := orm.Query[Post]().Where("id", 1).First() 471 | post, err := orm.Query[Post]().WherePK(1).First().Get() 472 | assert.NoError(t, err) 473 | assert.EqualValues(t, "body 1", post.BodyText) 474 | assert.EqualValues(t, 1, post.ID) 475 | 476 | }) 477 | t.Run("querying multiple rows", func(t *testing.T) { 478 | err := setup() 479 | assert.NoError(t, err) 480 | 481 | assert.NoError(t, orm.Save(&Post{BodyText: "body 1"})) 482 | assert.NoError(t, orm.Save(&Post{BodyText: "body 2"})) 483 | assert.NoError(t, orm.Save(&Post{BodyText: "body 3"})) 484 | posts, err := orm.Query[Post]().All() 485 | assert.NoError(t, err) 486 | assert.Len(t, posts, 3) 487 | assert.Equal(t, "body 1", posts[0].BodyText) 488 | }) 489 | 490 | t.Run("updating a row using query interface", func(t *testing.T) { 491 | err := setup() 492 | assert.NoError(t, err) 493 | 494 | assert.NoError(t, orm.Save(&Post{BodyText: "body 1"})) 495 | 496 | affected, err := orm.Query[Post]().Where("id", 1).Set("body", "body jadid").Update() 497 | assert.NoError(t, err) 498 | assert.EqualValues(t, 1, affected) 499 | 500 | post, err := orm.Find[Post](1) 501 | assert.NoError(t, err) 502 | assert.Equal(t, "body jadid", post.BodyText) 503 | }) 504 | 505 | t.Run("deleting a row using query interface", func(t *testing.T) { 506 | err := setup() 507 | assert.NoError(t, err) 508 | 509 | assert.NoError(t, orm.Save(&Post{BodyText: "body 1"})) 510 | 511 | affected, err := orm.Query[Post]().WherePK(1).Delete() 512 | assert.NoError(t, err) 513 | assert.EqualValues(t, 1, affected) 514 | count, err := orm.Query[Post]().WherePK(1).Count().Get() 515 | assert.NoError(t, err) 516 | assert.EqualValues(t, 0, count) 517 | }) 518 | 519 | t.Run("count", func(t *testing.T) { 520 | err := setup() 521 | assert.NoError(t, err) 522 | 523 | count, err := orm.Query[Post]().WherePK(1).Count().Get() 524 | assert.NoError(t, err) 525 | assert.EqualValues(t, 0, count) 526 | }) 527 | 528 | t.Run("latest", func(t *testing.T) { 529 | err := setup() 530 | assert.NoError(t, err) 531 | 532 | assert.NoError(t, orm.Save(&Post{BodyText: "body 1"})) 533 | assert.NoError(t, orm.Save(&Post{BodyText: "body 2"})) 534 | 535 | post, err := orm.Query[Post]().Latest().Get() 536 | assert.NoError(t, err) 537 | 538 | assert.EqualValues(t, "body 2", post.BodyText) 539 | }) 540 | } 541 | 542 | func TestSetup(t *testing.T) { 543 | t.Run("tables are out of sync", func(t *testing.T) { 544 | db, err := sql.Open("sqlite3", ":memory:") 545 | // _, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`) 546 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`) 547 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`) 548 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`) 549 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`) 550 | // _, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`) 551 | 552 | err = orm.SetupConnections(orm.ConnectionConfig{ 553 | Name: "default", 554 | DB: db, 555 | Dialect: orm.Dialects.SQLite3, 556 | Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}}, 557 | DatabaseValidations: true, 558 | }) 559 | assert.Error(t, err) 560 | 561 | }) 562 | t.Run("schemas are wrong", func(t *testing.T) { 563 | db, err := sql.Open("sqlite3", ":memory:") 564 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`) 565 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`) 566 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`) 567 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, body text)`) // missing post_id 568 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`) 569 | _, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`) 570 | 571 | err = orm.SetupConnections(orm.ConnectionConfig{ 572 | Name: "default", 573 | DB: db, 574 | Dialect: orm.Dialects.SQLite3, 575 | Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}}, 576 | DatabaseValidations: true, 577 | }) 578 | assert.Error(t, err) 579 | 580 | }) 581 | } 582 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | queryTypeSELECT = iota + 1 11 | queryTypeUPDATE 12 | queryTypeDelete 13 | ) 14 | 15 | // QueryBuilder is our query builder, almost all methods and functions in GoLobby ORM 16 | // create or configure instance of QueryBuilder. 17 | type QueryBuilder[OUTPUT any] struct { 18 | typ int 19 | schema *schema 20 | // general parts 21 | where *whereClause 22 | table string 23 | placeholderGenerator func(n int) []string 24 | 25 | // select parts 26 | orderBy *orderByClause 27 | groupBy *GroupBy 28 | selected *selected 29 | subQuery *struct { 30 | q string 31 | args []interface{} 32 | placeholderGenerator func(n int) []string 33 | } 34 | joins []*Join 35 | limit *Limit 36 | offset *Offset 37 | 38 | // update parts 39 | sets [][2]interface{} 40 | 41 | // execution parts 42 | db *sql.DB 43 | err error 44 | } 45 | 46 | // Finisher APIs 47 | 48 | // execute is a finisher executes QueryBuilder query, remember to use this when you have an Update 49 | // or Delete Query. 50 | func (q *QueryBuilder[OUTPUT]) execute() (sql.Result, error) { 51 | if q.err != nil { 52 | return nil, q.err 53 | } 54 | if q.typ == queryTypeSELECT { 55 | return nil, fmt.Errorf("query type is SELECT") 56 | } 57 | query, args, err := q.ToSql() 58 | if err != nil { 59 | return nil, err 60 | } 61 | return q.schema.getConnection().exec(query, args...) 62 | } 63 | 64 | // Get limit results to 1, runs query generated by query builder, scans result into OUTPUT. 65 | func (q *QueryBuilder[OUTPUT]) Get() (OUTPUT, error) { 66 | if q.err != nil { 67 | return *new(OUTPUT), q.err 68 | } 69 | queryString, args, err := q.ToSql() 70 | if err != nil { 71 | return *new(OUTPUT), err 72 | } 73 | rows, err := q.schema.getConnection().query(queryString, args...) 74 | if err != nil { 75 | return *new(OUTPUT), err 76 | } 77 | var output OUTPUT 78 | err = newBinder(q.schema).bind(rows, &output) 79 | if err != nil { 80 | return *new(OUTPUT), err 81 | } 82 | return output, nil 83 | } 84 | 85 | // All is a finisher, create the Select query based on QueryBuilder and scan results into 86 | // slice of type parameter E. 87 | func (q *QueryBuilder[OUTPUT]) All() ([]OUTPUT, error) { 88 | if q.err != nil { 89 | return nil, q.err 90 | } 91 | q.SetSelect() 92 | queryString, args, err := q.ToSql() 93 | if err != nil { 94 | return nil, err 95 | } 96 | rows, err := q.schema.getConnection().query(queryString, args...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | var output []OUTPUT 101 | err = newBinder(q.schema).bind(rows, &output) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return output, nil 106 | } 107 | 108 | // Delete is a finisher, creates a delete query from query builder and executes it. 109 | func (q *QueryBuilder[OUTPUT]) Delete() (rowsAffected int64, err error) { 110 | if q.err != nil { 111 | return 0, q.err 112 | } 113 | q.SetDelete() 114 | res, err := q.execute() 115 | if err != nil { 116 | return 0, q.err 117 | } 118 | return res.RowsAffected() 119 | } 120 | 121 | // Update is a finisher, creates an Update query from QueryBuilder and executes in into database, returns 122 | func (q *QueryBuilder[OUTPUT]) Update() (rowsAffected int64, err error) { 123 | if q.err != nil { 124 | return 0, q.err 125 | } 126 | q.SetUpdate() 127 | res, err := q.execute() 128 | if err != nil { 129 | return 0, err 130 | } 131 | return res.RowsAffected() 132 | } 133 | 134 | func copyQueryBuilder[T1 any, T2 any](q *QueryBuilder[T1], q2 *QueryBuilder[T2]) { 135 | q2.db = q.db 136 | q2.err = q.err 137 | q2.groupBy = q.groupBy 138 | q2.joins = q.joins 139 | q2.limit = q.limit 140 | q2.offset = q.offset 141 | q2.orderBy = q.orderBy 142 | q2.placeholderGenerator = q.placeholderGenerator 143 | q2.schema = q.schema 144 | q2.selected = q.selected 145 | q2.sets = q.sets 146 | 147 | q2.subQuery = q.subQuery 148 | q2.table = q.table 149 | q2.typ = q.typ 150 | q2.where = q.where 151 | } 152 | 153 | // Count creates and execute a select query from QueryBuilder and set it's field list of selection 154 | // to COUNT(id). 155 | func (q *QueryBuilder[OUTPUT]) Count() *QueryBuilder[int] { 156 | q.selected = &selected{Columns: []string{"COUNT(id)"}} 157 | q.SetSelect() 158 | qCount := NewQueryBuilder[int](q.schema) 159 | 160 | copyQueryBuilder(q, qCount) 161 | 162 | return qCount 163 | } 164 | 165 | // First returns first record of database using OrderBy primary key 166 | // ascending order. 167 | func (q *QueryBuilder[OUTPUT]) First() *QueryBuilder[OUTPUT] { 168 | q.OrderBy(q.schema.pkName(), ASC).Limit(1) 169 | return q 170 | } 171 | 172 | // Latest is like Get but it also do a OrderBy(primary key, DESC) 173 | func (q *QueryBuilder[OUTPUT]) Latest() *QueryBuilder[OUTPUT] { 174 | q.OrderBy(q.schema.pkName(), DESC).Limit(1) 175 | return q 176 | } 177 | 178 | // WherePK adds a where clause to QueryBuilder and also gets primary key name 179 | // from type parameter schema. 180 | func (q *QueryBuilder[OUTPUT]) WherePK(value interface{}) *QueryBuilder[OUTPUT] { 181 | return q.Where(q.schema.pkName(), value) 182 | } 183 | 184 | func (d *QueryBuilder[OUTPUT]) toSqlDelete() (string, []interface{}, error) { 185 | base := fmt.Sprintf("DELETE FROM %s", d.table) 186 | var args []interface{} 187 | if d.where != nil { 188 | d.where.PlaceHolderGenerator = d.placeholderGenerator 189 | where, whereArgs, err := d.where.ToSql() 190 | if err != nil { 191 | return "", nil, err 192 | } 193 | base += " WHERE " + where 194 | args = append(args, whereArgs...) 195 | } 196 | return base, args, nil 197 | } 198 | func pop(phs *[]string) string { 199 | top := (*phs)[len(*phs)-1] 200 | *phs = (*phs)[:len(*phs)-1] 201 | return top 202 | } 203 | 204 | func (u *QueryBuilder[OUTPUT]) kvString() string { 205 | phs := u.placeholderGenerator(len(u.sets)) 206 | var sets []string 207 | for _, pair := range u.sets { 208 | sets = append(sets, fmt.Sprintf("%s=%s", pair[0], pop(&phs))) 209 | } 210 | return strings.Join(sets, ",") 211 | } 212 | 213 | func (u *QueryBuilder[OUTPUT]) args() []interface{} { 214 | var values []interface{} 215 | for _, pair := range u.sets { 216 | values = append(values, pair[1]) 217 | } 218 | return values 219 | } 220 | 221 | func (u *QueryBuilder[OUTPUT]) toSqlUpdate() (string, []interface{}, error) { 222 | if u.table == "" { 223 | return "", nil, fmt.Errorf("table cannot be empty") 224 | } 225 | base := fmt.Sprintf("UPDATE %s SET %s", u.table, u.kvString()) 226 | args := u.args() 227 | if u.where != nil { 228 | u.where.PlaceHolderGenerator = u.placeholderGenerator 229 | where, whereArgs, err := u.where.ToSql() 230 | if err != nil { 231 | return "", nil, err 232 | } 233 | args = append(args, whereArgs...) 234 | base += " WHERE " + where 235 | } 236 | return base, args, nil 237 | } 238 | func (s *QueryBuilder[OUTPUT]) toSqlSelect() (string, []interface{}, error) { 239 | if s.err != nil { 240 | return "", nil, s.err 241 | } 242 | base := "SELECT" 243 | var args []interface{} 244 | // select 245 | if s.selected == nil { 246 | s.selected = &selected{ 247 | Columns: []string{"*"}, 248 | } 249 | } 250 | base += " " + s.selected.String() 251 | // from 252 | if s.table == "" && s.subQuery == nil { 253 | return "", nil, fmt.Errorf("Table name cannot be empty") 254 | } else if s.table != "" && s.subQuery != nil { 255 | return "", nil, fmt.Errorf("cannot have both Table and subquery") 256 | } 257 | if s.table != "" { 258 | base += " " + "FROM " + s.table 259 | } 260 | if s.subQuery != nil { 261 | s.subQuery.placeholderGenerator = s.placeholderGenerator 262 | base += " " + "FROM (" + s.subQuery.q + " )" 263 | args = append(args, s.subQuery.args...) 264 | } 265 | // Joins 266 | if s.joins != nil { 267 | for _, join := range s.joins { 268 | base += " " + join.String() 269 | } 270 | } 271 | // whereClause 272 | if s.where != nil { 273 | s.where.PlaceHolderGenerator = s.placeholderGenerator 274 | where, whereArgs, err := s.where.ToSql() 275 | if err != nil { 276 | return "", nil, err 277 | } 278 | base += " WHERE " + where 279 | args = append(args, whereArgs...) 280 | } 281 | 282 | // orderByClause 283 | if s.orderBy != nil { 284 | base += " " + s.orderBy.String() 285 | } 286 | 287 | // GroupBy 288 | if s.groupBy != nil { 289 | base += " " + s.groupBy.String() 290 | } 291 | 292 | // Limit 293 | if s.limit != nil { 294 | base += " " + s.limit.String() 295 | } 296 | 297 | // Offset 298 | if s.offset != nil { 299 | base += " " + s.offset.String() 300 | } 301 | 302 | return base, args, nil 303 | } 304 | 305 | // ToSql creates sql query from QueryBuilder based on internal fields it would decide what kind 306 | // of query to build. 307 | func (q *QueryBuilder[OUTPUT]) ToSql() (string, []interface{}, error) { 308 | if q.err != nil { 309 | return "", nil, q.err 310 | } 311 | if q.typ == queryTypeSELECT { 312 | return q.toSqlSelect() 313 | } else if q.typ == queryTypeDelete { 314 | return q.toSqlDelete() 315 | } else if q.typ == queryTypeUPDATE { 316 | return q.toSqlUpdate() 317 | } else { 318 | return "", nil, fmt.Errorf("no sql type matched") 319 | } 320 | } 321 | 322 | type orderByOrder string 323 | 324 | const ( 325 | ASC = "ASC" 326 | DESC = "DESC" 327 | ) 328 | 329 | type orderByClause struct { 330 | Columns [][2]string 331 | } 332 | 333 | func (o orderByClause) String() string { 334 | var tuples []string 335 | for _, pair := range o.Columns { 336 | tuples = append(tuples, fmt.Sprintf("%s %s", pair[0], pair[1])) 337 | } 338 | return fmt.Sprintf("ORDER BY %s", strings.Join(tuples, ",")) 339 | } 340 | 341 | type GroupBy struct { 342 | Columns []string 343 | } 344 | 345 | func (g GroupBy) String() string { 346 | return fmt.Sprintf("GROUP BY %s", strings.Join(g.Columns, ",")) 347 | } 348 | 349 | type joinType string 350 | 351 | const ( 352 | JoinTypeInner = "INNER" 353 | JoinTypeLeft = "LEFT" 354 | JoinTypeRight = "RIGHT" 355 | JoinTypeFull = "FULL OUTER" 356 | JoinTypeSelf = "SELF" 357 | ) 358 | 359 | type JoinOn struct { 360 | Lhs string 361 | Rhs string 362 | } 363 | 364 | func (j JoinOn) String() string { 365 | return fmt.Sprintf("%s = %s", j.Lhs, j.Rhs) 366 | } 367 | 368 | type Join struct { 369 | Type joinType 370 | Table string 371 | On JoinOn 372 | } 373 | 374 | func (j Join) String() string { 375 | return fmt.Sprintf("%s JOIN %s ON %s", j.Type, j.Table, j.On.String()) 376 | } 377 | 378 | type Limit struct { 379 | N int 380 | } 381 | 382 | func (l Limit) String() string { 383 | return fmt.Sprintf("LIMIT %d", l.N) 384 | } 385 | 386 | type Offset struct { 387 | N int 388 | } 389 | 390 | func (o Offset) String() string { 391 | return fmt.Sprintf("OFFSET %d", o.N) 392 | } 393 | 394 | type selected struct { 395 | Columns []string 396 | } 397 | 398 | func (s selected) String() string { 399 | return fmt.Sprintf("%s", strings.Join(s.Columns, ",")) 400 | } 401 | 402 | // OrderBy adds an OrderBy section to QueryBuilder. 403 | func (q *QueryBuilder[OUTPUT]) OrderBy(column string, how string) *QueryBuilder[OUTPUT] { 404 | q.SetSelect() 405 | if q.orderBy == nil { 406 | q.orderBy = &orderByClause{} 407 | } 408 | q.orderBy.Columns = append(q.orderBy.Columns, [2]string{column, how}) 409 | return q 410 | } 411 | 412 | // LeftJoin adds a left join section to QueryBuilder. 413 | func (q *QueryBuilder[OUTPUT]) LeftJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] { 414 | q.SetSelect() 415 | q.joins = append(q.joins, &Join{ 416 | Type: JoinTypeLeft, 417 | Table: table, 418 | On: JoinOn{ 419 | Lhs: onLhs, 420 | Rhs: onRhs, 421 | }, 422 | }) 423 | return q 424 | } 425 | 426 | // RightJoin adds a right join section to QueryBuilder. 427 | func (q *QueryBuilder[OUTPUT]) RightJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] { 428 | q.SetSelect() 429 | q.joins = append(q.joins, &Join{ 430 | Type: JoinTypeRight, 431 | Table: table, 432 | On: JoinOn{ 433 | Lhs: onLhs, 434 | Rhs: onRhs, 435 | }, 436 | }) 437 | return q 438 | } 439 | 440 | // InnerJoin adds a inner join section to QueryBuilder. 441 | func (q *QueryBuilder[OUTPUT]) InnerJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] { 442 | q.SetSelect() 443 | q.joins = append(q.joins, &Join{ 444 | Type: JoinTypeInner, 445 | Table: table, 446 | On: JoinOn{ 447 | Lhs: onLhs, 448 | Rhs: onRhs, 449 | }, 450 | }) 451 | return q 452 | } 453 | 454 | // Join adds a inner join section to QueryBuilder. 455 | func (q *QueryBuilder[OUTPUT]) Join(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] { 456 | return q.InnerJoin(table, onLhs, onRhs) 457 | } 458 | 459 | // FullOuterJoin adds a full outer join section to QueryBuilder. 460 | func (q *QueryBuilder[OUTPUT]) FullOuterJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] { 461 | q.SetSelect() 462 | q.joins = append(q.joins, &Join{ 463 | Type: JoinTypeFull, 464 | Table: table, 465 | On: JoinOn{ 466 | Lhs: onLhs, 467 | Rhs: onRhs, 468 | }, 469 | }) 470 | return q 471 | } 472 | 473 | // Where Adds a where clause to query, if already have where clause append to it 474 | // as AndWhere. 475 | func (q *QueryBuilder[OUTPUT]) Where(parts ...interface{}) *QueryBuilder[OUTPUT] { 476 | if q.where != nil { 477 | return q.addWhere("AND", parts...) 478 | } 479 | if len(parts) == 1 { 480 | if r, isRaw := parts[0].(*raw); isRaw { 481 | q.where = &whereClause{raw: r.sql, args: r.args, PlaceHolderGenerator: q.placeholderGenerator} 482 | return q 483 | } else { 484 | q.err = fmt.Errorf("when you have one argument passed to where, it should be *raw") 485 | return q 486 | } 487 | 488 | } else if len(parts) == 2 { 489 | if strings.Index(parts[0].(string), " ") == -1 { 490 | // Equal mode 491 | q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: Eq, Rhs: parts[1]}, PlaceHolderGenerator: q.placeholderGenerator} 492 | } 493 | return q 494 | } else if len(parts) == 3 { 495 | // operator mode 496 | q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2]}, PlaceHolderGenerator: q.placeholderGenerator} 497 | return q 498 | } else if len(parts) > 3 && parts[1].(string) == "IN" { 499 | q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2:]}, PlaceHolderGenerator: q.placeholderGenerator} 500 | return q 501 | } else { 502 | q.err = fmt.Errorf("wrong number of arguments passed to Where") 503 | return q 504 | } 505 | } 506 | 507 | type binaryOp string 508 | 509 | const ( 510 | Eq = "=" 511 | GT = ">" 512 | LT = "<" 513 | GE = ">=" 514 | LE = "<=" 515 | NE = "!=" 516 | Between = "BETWEEN" 517 | Like = "LIKE" 518 | In = "IN" 519 | ) 520 | 521 | type cond struct { 522 | PlaceHolderGenerator func(n int) []string 523 | 524 | Lhs string 525 | Op binaryOp 526 | Rhs interface{} 527 | } 528 | 529 | func (b cond) ToSql() (string, []interface{}, error) { 530 | var phs []string 531 | if b.Op == In { 532 | rhs, isInterfaceSlice := b.Rhs.([]interface{}) 533 | if isInterfaceSlice { 534 | phs = b.PlaceHolderGenerator(len(rhs)) 535 | return fmt.Sprintf("%s IN (%s)", b.Lhs, strings.Join(phs, ",")), rhs, nil 536 | } else if rawThing, isRaw := b.Rhs.(*raw); isRaw { 537 | return fmt.Sprintf("%s IN (%s)", b.Lhs, rawThing.sql), rawThing.args, nil 538 | } else { 539 | return "", nil, fmt.Errorf("Right hand side of Cond when operator is IN should be either a interface{} slice or *raw") 540 | } 541 | 542 | } else { 543 | phs = b.PlaceHolderGenerator(1) 544 | return fmt.Sprintf("%s %s %s", b.Lhs, b.Op, pop(&phs)), []interface{}{b.Rhs}, nil 545 | } 546 | } 547 | 548 | const ( 549 | nextType_AND = "AND" 550 | nextType_OR = "OR" 551 | ) 552 | 553 | type whereClause struct { 554 | PlaceHolderGenerator func(n int) []string 555 | nextTyp string 556 | next *whereClause 557 | cond 558 | raw string 559 | args []interface{} 560 | } 561 | 562 | func (w whereClause) ToSql() (string, []interface{}, error) { 563 | var base string 564 | var args []interface{} 565 | var err error 566 | if w.raw != "" { 567 | base = w.raw 568 | args = w.args 569 | } else { 570 | w.cond.PlaceHolderGenerator = w.PlaceHolderGenerator 571 | base, args, err = w.cond.ToSql() 572 | if err != nil { 573 | return "", nil, err 574 | } 575 | } 576 | if w.next == nil { 577 | return base, args, nil 578 | } 579 | if w.next != nil { 580 | next, nextArgs, err := w.next.ToSql() 581 | if err != nil { 582 | return "", nil, err 583 | } 584 | base += " " + w.nextTyp + " " + next 585 | args = append(args, nextArgs...) 586 | return base, args, nil 587 | } 588 | 589 | return base, args, nil 590 | } 591 | 592 | //func (q *QueryBuilder[OUTPUT]) WhereKeyValue(m map) {} 593 | 594 | // WhereIn adds a where clause to QueryBuilder using In operator. 595 | func (q *QueryBuilder[OUTPUT]) WhereIn(column string, values ...interface{}) *QueryBuilder[OUTPUT] { 596 | return q.Where(append([]interface{}{column, In}, values...)...) 597 | } 598 | 599 | // AndWhere appends a where clause to query builder as And where clause. 600 | func (q *QueryBuilder[OUTPUT]) AndWhere(parts ...interface{}) *QueryBuilder[OUTPUT] { 601 | return q.addWhere(nextType_AND, parts...) 602 | } 603 | 604 | // OrWhere appends a where clause to query builder as Or where clause. 605 | func (q *QueryBuilder[OUTPUT]) OrWhere(parts ...interface{}) *QueryBuilder[OUTPUT] { 606 | return q.addWhere(nextType_OR, parts...) 607 | } 608 | 609 | func (q *QueryBuilder[OUTPUT]) addWhere(typ string, parts ...interface{}) *QueryBuilder[OUTPUT] { 610 | w := q.where 611 | for { 612 | if w == nil { 613 | break 614 | } else if w.next == nil { 615 | w.next = &whereClause{PlaceHolderGenerator: q.placeholderGenerator} 616 | w.nextTyp = typ 617 | w = w.next 618 | break 619 | } else { 620 | w = w.next 621 | } 622 | } 623 | if w == nil { 624 | w = &whereClause{PlaceHolderGenerator: q.placeholderGenerator} 625 | } 626 | if len(parts) == 1 { 627 | w.raw = parts[0].(*raw).sql 628 | w.args = parts[0].(*raw).args 629 | return q 630 | } else if len(parts) == 2 { 631 | // Equal mode 632 | w.cond = cond{Lhs: parts[0].(string), Op: Eq, Rhs: parts[1]} 633 | return q 634 | } else if len(parts) == 3 { 635 | // operator mode 636 | w.cond = cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2]} 637 | return q 638 | } else { 639 | panic("wrong number of arguments passed to Where") 640 | } 641 | } 642 | 643 | // Offset adds offset section to query builder. 644 | func (q *QueryBuilder[OUTPUT]) Offset(n int) *QueryBuilder[OUTPUT] { 645 | q.SetSelect() 646 | q.offset = &Offset{N: n} 647 | return q 648 | } 649 | 650 | // Limit adds limit section to query builder. 651 | func (q *QueryBuilder[OUTPUT]) Limit(n int) *QueryBuilder[OUTPUT] { 652 | q.SetSelect() 653 | q.limit = &Limit{N: n} 654 | return q 655 | } 656 | 657 | // Table sets table of QueryBuilder. 658 | func (q *QueryBuilder[OUTPUT]) Table(t string) *QueryBuilder[OUTPUT] { 659 | q.table = t 660 | return q 661 | } 662 | 663 | // SetSelect sets query type of QueryBuilder to Select. 664 | func (q *QueryBuilder[OUTPUT]) SetSelect() *QueryBuilder[OUTPUT] { 665 | q.typ = queryTypeSELECT 666 | return q 667 | } 668 | 669 | // GroupBy adds a group by section to QueryBuilder. 670 | func (q *QueryBuilder[OUTPUT]) GroupBy(columns ...string) *QueryBuilder[OUTPUT] { 671 | q.SetSelect() 672 | if q.groupBy == nil { 673 | q.groupBy = &GroupBy{} 674 | } 675 | q.groupBy.Columns = append(q.groupBy.Columns, columns...) 676 | return q 677 | } 678 | 679 | // Select adds columns to QueryBuilder select field list. 680 | func (q *QueryBuilder[OUTPUT]) Select(columns ...string) *QueryBuilder[OUTPUT] { 681 | q.SetSelect() 682 | if q.selected == nil { 683 | q.selected = &selected{} 684 | } 685 | q.selected.Columns = append(q.selected.Columns, columns...) 686 | return q 687 | } 688 | 689 | // FromQuery sets subquery of QueryBuilder to be given subquery so 690 | // when doing select instead of from table we do from(subquery). 691 | func (q *QueryBuilder[OUTPUT]) FromQuery(subQuery *QueryBuilder[OUTPUT]) *QueryBuilder[OUTPUT] { 692 | q.SetSelect() 693 | subQuery.SetSelect() 694 | subQuery.placeholderGenerator = q.placeholderGenerator 695 | subQueryString, args, err := subQuery.ToSql() 696 | q.err = err 697 | q.subQuery = &struct { 698 | q string 699 | args []interface{} 700 | placeholderGenerator func(n int) []string 701 | }{ 702 | subQueryString, args, q.placeholderGenerator, 703 | } 704 | return q 705 | } 706 | 707 | func (q *QueryBuilder[OUTPUT]) SetUpdate() *QueryBuilder[OUTPUT] { 708 | q.typ = queryTypeUPDATE 709 | return q 710 | } 711 | 712 | func (q *QueryBuilder[OUTPUT]) Set(keyValues ...any) *QueryBuilder[OUTPUT] { 713 | if len(keyValues)%2 != 0 { 714 | q.err = fmt.Errorf("when using Set, passed argument count should be even: %w", q.err) 715 | return q 716 | } 717 | q.SetUpdate() 718 | for i := 0; i < len(keyValues); i++ { 719 | if i != 0 && i%2 == 1 { 720 | q.sets = append(q.sets, [2]any{keyValues[i-1], keyValues[i]}) 721 | } 722 | } 723 | return q 724 | } 725 | 726 | func (q *QueryBuilder[OUTPUT]) SetDialect(dialect *Dialect) *QueryBuilder[OUTPUT] { 727 | q.placeholderGenerator = dialect.PlaceHolderGenerator 728 | return q 729 | } 730 | func (q *QueryBuilder[OUTPUT]) SetDelete() *QueryBuilder[OUTPUT] { 731 | q.typ = queryTypeDelete 732 | return q 733 | } 734 | 735 | type raw struct { 736 | sql string 737 | args []interface{} 738 | } 739 | 740 | // Raw creates a Raw sql query chunk that you can add to several components of QueryBuilder like 741 | // Wheres. 742 | func Raw(sql string, args ...interface{}) *raw { 743 | return &raw{sql: sql, args: args} 744 | } 745 | 746 | func NewQueryBuilder[OUTPUT any](s *schema) *QueryBuilder[OUTPUT] { 747 | return &QueryBuilder[OUTPUT]{schema: s} 748 | } 749 | 750 | type insertStmt struct { 751 | PlaceHolderGenerator func(n int) []string 752 | Table string 753 | Columns []string 754 | Values [][]interface{} 755 | Returning string 756 | } 757 | 758 | func (i insertStmt) flatValues() []interface{} { 759 | var values []interface{} 760 | for _, row := range i.Values { 761 | values = append(values, row...) 762 | } 763 | return values 764 | } 765 | 766 | func (i insertStmt) getValuesStr() string { 767 | phs := i.PlaceHolderGenerator(len(i.Values) * len(i.Values[0])) 768 | 769 | var output []string 770 | for _, valueRow := range i.Values { 771 | output = append(output, fmt.Sprintf("(%s)", strings.Join(phs[:len(valueRow)], ","))) 772 | phs = phs[len(valueRow):] 773 | } 774 | return strings.Join(output, ",") 775 | } 776 | 777 | func (i insertStmt) ToSql() (string, []interface{}) { 778 | base := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", 779 | i.Table, 780 | strings.Join(i.Columns, ","), 781 | i.getValuesStr(), 782 | ) 783 | if i.Returning != "" { 784 | base += "RETURNING " + i.Returning 785 | } 786 | return base, i.flatValues() 787 | } 788 | 789 | func postgresPlaceholder(n int) []string { 790 | output := []string{} 791 | for i := 1; i < n+1; i++ { 792 | output = append(output, fmt.Sprintf("$%d", i)) 793 | } 794 | return output 795 | } 796 | 797 | func questionMarks(n int) []string { 798 | output := []string{} 799 | for i := 0; i < n; i++ { 800 | output = append(output, "?") 801 | } 802 | 803 | return output 804 | } 805 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type Dummy struct{} 10 | 11 | func (d Dummy) ConfigureEntity(e *EntityConfigurator) { 12 | // TODO implement me 13 | panic("implement me") 14 | } 15 | 16 | func TestSelect(t *testing.T) { 17 | t.Run("only select * from Table", func(t *testing.T) { 18 | s := NewQueryBuilder[Dummy](nil) 19 | s.Table("users").SetSelect() 20 | str, args, err := s.ToSql() 21 | assert.NoError(t, err) 22 | assert.Empty(t, args) 23 | assert.Equal(t, "SELECT * FROM users", str) 24 | }) 25 | t.Run("select with whereClause", func(t *testing.T) { 26 | s := NewQueryBuilder[Dummy](nil) 27 | 28 | s.Table("users").SetDialect(Dialects.MySQL). 29 | Where("age", 10). 30 | AndWhere("age", "<", 10). 31 | Where("name", "Amirreza"). 32 | OrWhere("age", GT, 11). 33 | SetSelect() 34 | 35 | str, args, err := s.ToSql() 36 | assert.NoError(t, err) 37 | assert.EqualValues(t, []interface{}{10, 10, "Amirreza", 11}, args) 38 | assert.Equal(t, "SELECT * FROM users WHERE age = ? AND age < ? AND name = ? OR age > ?", str) 39 | }) 40 | t.Run("select with order by", func(t *testing.T) { 41 | s := NewQueryBuilder[Dummy](nil).Table("users").OrderBy("created_at", ASC).OrderBy("updated_at", DESC) 42 | str, args, err := s.ToSql() 43 | assert.NoError(t, err) 44 | assert.Empty(t, args) 45 | assert.Equal(t, "SELECT * FROM users ORDER BY created_at ASC,updated_at DESC", str) 46 | }) 47 | 48 | t.Run("select with group by", func(t *testing.T) { 49 | s := NewQueryBuilder[Dummy](nil).Table("users").GroupBy("created_at", "updated_at") 50 | str, args, err := s.ToSql() 51 | assert.NoError(t, err) 52 | assert.Empty(t, args) 53 | assert.Equal(t, "SELECT * FROM users GROUP BY created_at,updated_at", str) 54 | }) 55 | 56 | t.Run("Select with limit", func(t *testing.T) { 57 | s := NewQueryBuilder[Dummy](nil).Table("users").Limit(10) 58 | str, args, err := s.ToSql() 59 | assert.NoError(t, err) 60 | assert.Empty(t, args) 61 | assert.Equal(t, "SELECT * FROM users LIMIT 10", str) 62 | }) 63 | 64 | t.Run("Select with offset", func(t *testing.T) { 65 | s := NewQueryBuilder[Dummy](nil).Table("users").Offset(10) 66 | str, args, err := s.ToSql() 67 | assert.NoError(t, err) 68 | assert.Empty(t, args) 69 | assert.Equal(t, "SELECT * FROM users OFFSET 10", str) 70 | }) 71 | 72 | t.Run("select with join", func(t *testing.T) { 73 | s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").RightJoin("addresses", "users.id", "addresses.user_id") 74 | str, args, err := s.ToSql() 75 | assert.NoError(t, err) 76 | assert.Empty(t, args) 77 | assert.Equal(t, `SELECT id,name FROM users RIGHT JOIN addresses ON users.id = addresses.user_id`, str) 78 | }) 79 | 80 | t.Run("select with multiple joins", func(t *testing.T) { 81 | s := NewQueryBuilder[Dummy](nil).Table("users"). 82 | Select("id", "name"). 83 | RightJoin("addresses", "users.id", "addresses.user_id"). 84 | LeftJoin("user_credits", "users.id", "user_credits.user_id") 85 | sql, args, err := s.ToSql() 86 | assert.NoError(t, err) 87 | assert.Empty(t, args) 88 | assert.Equal(t, `SELECT id,name FROM users RIGHT JOIN addresses ON users.id = addresses.user_id LEFT JOIN user_credits ON users.id = user_credits.user_id`, sql) 89 | }) 90 | 91 | t.Run("select with subquery", func(t *testing.T) { 92 | s := NewQueryBuilder[Dummy](nil).SetDialect(Dialects.MySQL) 93 | s.FromQuery(NewQueryBuilder[Dummy](nil).Table("users").Where("age", "<", 10)) 94 | sql, args, err := s.ToSql() 95 | assert.NoError(t, err) 96 | assert.EqualValues(t, []interface{}{10}, args) 97 | assert.Equal(t, `SELECT * FROM (SELECT * FROM users WHERE age < ? )`, sql) 98 | 99 | }) 100 | 101 | t.Run("select with inner join", func(t *testing.T) { 102 | s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").InnerJoin("addresses", "users.id", "addresses.user_id") 103 | str, args, err := s.ToSql() 104 | assert.NoError(t, err) 105 | assert.Empty(t, args) 106 | assert.Equal(t, `SELECT id,name FROM users INNER JOIN addresses ON users.id = addresses.user_id`, str) 107 | }) 108 | 109 | t.Run("select with join", func(t *testing.T) { 110 | s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").Join("addresses", "users.id", "addresses.user_id") 111 | str, args, err := s.ToSql() 112 | assert.NoError(t, err) 113 | assert.Empty(t, args) 114 | assert.Equal(t, `SELECT id,name FROM users INNER JOIN addresses ON users.id = addresses.user_id`, str) 115 | }) 116 | 117 | t.Run("select with full outer join", func(t *testing.T) { 118 | s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").FullOuterJoin("addresses", "users.id", "addresses.user_id") 119 | str, args, err := s.ToSql() 120 | assert.NoError(t, err) 121 | assert.Empty(t, args) 122 | assert.Equal(t, `SELECT id,name FROM users FULL OUTER JOIN addresses ON users.id = addresses.user_id`, str) 123 | }) 124 | t.Run("raw where", func(t *testing.T) { 125 | sql, args, err := 126 | NewQueryBuilder[Dummy](nil). 127 | SetDialect(Dialects.MySQL). 128 | Table("users"). 129 | Where(Raw("id = ?", 1)). 130 | AndWhere(Raw("age < ?", 10)). 131 | SetSelect(). 132 | ToSql() 133 | 134 | assert.NoError(t, err) 135 | assert.EqualValues(t, []interface{}{1, 10}, args) 136 | assert.Equal(t, `SELECT * FROM users WHERE id = ? AND age < ?`, sql) 137 | }) 138 | t.Run("no sql type matched", func(t *testing.T) { 139 | sql, args, err := NewQueryBuilder[Dummy](nil).ToSql() 140 | assert.Error(t, err) 141 | assert.Empty(t, args) 142 | assert.Empty(t, sql) 143 | }) 144 | 145 | t.Run("raw where in", func(t *testing.T) { 146 | sql, args, err := 147 | NewQueryBuilder[Dummy](nil). 148 | SetDialect(Dialects.MySQL). 149 | Table("users"). 150 | WhereIn("id", Raw("SELECT user_id FROM user_books WHERE book_id = ?", 10)). 151 | SetSelect(). 152 | ToSql() 153 | 154 | assert.NoError(t, err) 155 | assert.EqualValues(t, []interface{}{10}, args) 156 | assert.Equal(t, `SELECT * FROM users WHERE id IN (SELECT user_id FROM user_books WHERE book_id = ?)`, sql) 157 | }) 158 | t.Run("where in", func(t *testing.T) { 159 | sql, args, err := 160 | NewQueryBuilder[Dummy](nil). 161 | SetDialect(Dialects.MySQL). 162 | Table("users"). 163 | WhereIn("id", 1, 2, 3, 4, 5, 6). 164 | SetSelect(). 165 | ToSql() 166 | 167 | assert.NoError(t, err) 168 | assert.EqualValues(t, []interface{}{1, 2, 3, 4, 5, 6}, args) 169 | assert.Equal(t, `SELECT * FROM users WHERE id IN (?,?,?,?,?,?)`, sql) 170 | 171 | }) 172 | } 173 | func TestUpdate(t *testing.T) { 174 | t.Run("update no whereClause", func(t *testing.T) { 175 | u := NewQueryBuilder[Dummy](nil).Table("users").Set("name", "amirreza").SetDialect(Dialects.MySQL) 176 | sql, args, err := u.ToSql() 177 | assert.NoError(t, err) 178 | assert.Equal(t, `UPDATE users SET name=?`, sql) 179 | assert.Equal(t, []interface{}{"amirreza"}, args) 180 | }) 181 | t.Run("update with whereClause", func(t *testing.T) { 182 | u := NewQueryBuilder[Dummy](nil).Table("users").Set("name", "amirreza").Where("age", "<", 18).SetDialect(Dialects.MySQL) 183 | sql, args, err := u.ToSql() 184 | assert.NoError(t, err) 185 | assert.Equal(t, `UPDATE users SET name=? WHERE age < ?`, sql) 186 | assert.Equal(t, []interface{}{"amirreza", 18}, args) 187 | 188 | }) 189 | } 190 | func TestDelete(t *testing.T) { 191 | t.Run("delete without whereClause", func(t *testing.T) { 192 | d := NewQueryBuilder[Dummy](nil).Table("users").SetDelete() 193 | sql, args, err := d.ToSql() 194 | assert.NoError(t, err) 195 | assert.Equal(t, `DELETE FROM users`, sql) 196 | assert.Empty(t, args) 197 | }) 198 | t.Run("delete with whereClause", func(t *testing.T) { 199 | d := NewQueryBuilder[Dummy](nil).Table("users").SetDialect(Dialects.MySQL).Where("created_at", ">", "2012-01-10").SetDelete() 200 | sql, args, err := d.ToSql() 201 | assert.NoError(t, err) 202 | assert.Equal(t, `DELETE FROM users WHERE created_at > ?`, sql) 203 | assert.EqualValues(t, []interface{}{"2012-01-10"}, args) 204 | }) 205 | } 206 | 207 | func TestInsert(t *testing.T) { 208 | t.Run("insert into multiple rows", func(t *testing.T) { 209 | i := insertStmt{} 210 | i.Table = "users" 211 | i.PlaceHolderGenerator = Dialects.MySQL.PlaceHolderGenerator 212 | i.Columns = []string{"name", "age"} 213 | i.Values = append(i.Values, []interface{}{"amirreza", 11}, []interface{}{"parsa", 10}) 214 | s, args := i.ToSql() 215 | assert.Equal(t, `INSERT INTO users (name,age) VALUES (?,?),(?,?)`, s) 216 | assert.EqualValues(t, []interface{}{"amirreza", 11, "parsa", 10}, args) 217 | }) 218 | 219 | t.Run("insert into single row", func(t *testing.T) { 220 | i := insertStmt{} 221 | i.Table = "users" 222 | i.PlaceHolderGenerator = Dialects.MySQL.PlaceHolderGenerator 223 | i.Columns = []string{"name", "age"} 224 | i.Values = append(i.Values, []interface{}{"amirreza", 11}) 225 | s, args := i.ToSql() 226 | assert.Equal(t, `INSERT INTO users (name,age) VALUES (?,?)`, s) 227 | assert.Equal(t, []interface{}{"amirreza", 11}, args) 228 | }) 229 | } 230 | 231 | func TestPostgresPlaceholder(t *testing.T) { 232 | t.Run("for 5 it should have 5", func(t *testing.T) { 233 | phs := postgresPlaceholder(5) 234 | assert.EqualValues(t, []string{"$1", "$2", "$3", "$4", "$5"}, phs) 235 | }) 236 | } 237 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | func getConnectionFor(e Entity) *connection { 11 | configurator := newEntityConfigurator() 12 | e.ConfigureEntity(configurator) 13 | 14 | if len(globalConnections) > 1 && (configurator.connection == "" || configurator.table == "") { 15 | panic("need table and DB name when having more than 1 DB registered") 16 | } 17 | if len(globalConnections) == 1 { 18 | for _, db := range globalConnections { 19 | return db 20 | } 21 | } 22 | if db, exists := globalConnections[fmt.Sprintf("%s", configurator.connection)]; exists { 23 | return db 24 | } 25 | panic("no db found") 26 | } 27 | 28 | func getSchemaFor(e Entity) *schema { 29 | configurator := newEntityConfigurator() 30 | c := getConnectionFor(e) 31 | e.ConfigureEntity(configurator) 32 | s := c.getSchema(configurator.table) 33 | if s == nil { 34 | s = schemaOfHeavyReflectionStuff(e) 35 | c.setSchema(e, s) 36 | } 37 | return s 38 | } 39 | 40 | type schema struct { 41 | Connection string 42 | Table string 43 | fields []*field 44 | relations map[string]interface{} 45 | setPK func(o Entity, value interface{}) 46 | getPK func(o Entity) interface{} 47 | columnConstraints []*FieldConfigurator 48 | } 49 | 50 | func (s *schema) getField(sf reflect.StructField) *field { 51 | for _, f := range s.fields { 52 | if sf.Name == f.Name { 53 | return f 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (s *schema) getDialect() *Dialect { 60 | return GetConnection(s.Connection).Dialect 61 | } 62 | func (s *schema) Columns(withPK bool) []string { 63 | var cols []string 64 | for _, field := range s.fields { 65 | if field.Virtual { 66 | continue 67 | } 68 | if !withPK && field.IsPK { 69 | continue 70 | } 71 | if s.getDialect().AddTableNameInSelectColumns { 72 | cols = append(cols, s.Table+"."+field.Name) 73 | } else { 74 | cols = append(cols, field.Name) 75 | } 76 | } 77 | return cols 78 | } 79 | 80 | func (s *schema) pkName() string { 81 | for _, field := range s.fields { 82 | if field.IsPK { 83 | return field.Name 84 | } 85 | } 86 | return "" 87 | } 88 | 89 | func genericFieldsOf(obj Entity) []*field { 90 | t := reflect.TypeOf(obj) 91 | for t.Kind() == reflect.Ptr { 92 | t = t.Elem() 93 | 94 | } 95 | if t.Kind() == reflect.Slice { 96 | t = t.Elem() 97 | for t.Kind() == reflect.Ptr { 98 | t = t.Elem() 99 | } 100 | } 101 | var ec EntityConfigurator 102 | obj.ConfigureEntity(&ec) 103 | 104 | var fms []*field 105 | for i := 0; i < t.NumField(); i++ { 106 | ft := t.Field(i) 107 | fm := fieldMetadata(ft, ec.columnConstraints) 108 | fms = append(fms, fm...) 109 | } 110 | return fms 111 | } 112 | 113 | func valuesOfField(vf reflect.Value) []interface{} { 114 | var values []interface{} 115 | if vf.Type().Kind() == reflect.Struct || vf.Type().Kind() == reflect.Ptr { 116 | t := vf.Type() 117 | if vf.Type().Kind() == reflect.Ptr { 118 | t = vf.Type().Elem() 119 | } 120 | if !t.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) { 121 | // go into 122 | // it does not implement driver.Valuer interface 123 | for i := 0; i < vf.NumField(); i++ { 124 | vif := vf.Field(i) 125 | values = append(values, valuesOfField(vif)...) 126 | } 127 | } else { 128 | values = append(values, vf.Interface()) 129 | } 130 | } else { 131 | values = append(values, vf.Interface()) 132 | } 133 | return values 134 | } 135 | func genericValuesOf(o Entity, withPK bool) []interface{} { 136 | t := reflect.TypeOf(o) 137 | v := reflect.ValueOf(o) 138 | if t.Kind() == reflect.Ptr { 139 | t = t.Elem() 140 | v = v.Elem() 141 | } 142 | fields := getSchemaFor(o).fields 143 | pkIdx := -1 144 | for i, field := range fields { 145 | if field.IsPK { 146 | pkIdx = i 147 | } 148 | 149 | } 150 | 151 | var values []interface{} 152 | 153 | for i := 0; i < t.NumField(); i++ { 154 | if !withPK && i == pkIdx { 155 | continue 156 | } 157 | if fields[i].Virtual { 158 | continue 159 | } 160 | vf := v.Field(i) 161 | values = append(values, valuesOfField(vf)...) 162 | } 163 | return values 164 | } 165 | 166 | func genericSetPkValue(obj Entity, value interface{}) { 167 | genericSet(obj, getSchemaFor(obj).pkName(), value) 168 | } 169 | 170 | func genericGetPKValue(obj Entity) interface{} { 171 | t := reflect.TypeOf(obj) 172 | val := reflect.ValueOf(obj) 173 | if t.Kind() == reflect.Ptr { 174 | val = val.Elem() 175 | } 176 | 177 | fields := getSchemaFor(obj).fields 178 | for i, field := range fields { 179 | if field.IsPK { 180 | return val.Field(i).Interface() 181 | } 182 | } 183 | return "" 184 | } 185 | 186 | func (s *schema) createdAt() *field { 187 | for _, f := range s.fields { 188 | if f.IsCreatedAt { 189 | return f 190 | } 191 | } 192 | return nil 193 | } 194 | func (s *schema) updatedAt() *field { 195 | for _, f := range s.fields { 196 | if f.IsUpdatedAt { 197 | return f 198 | } 199 | } 200 | return nil 201 | } 202 | 203 | func (s *schema) deletedAt() *field { 204 | for _, f := range s.fields { 205 | if f.IsDeletedAt { 206 | return f 207 | } 208 | } 209 | return nil 210 | } 211 | func pointersOf(v reflect.Value) map[string]interface{} { 212 | m := map[string]interface{}{} 213 | actualV := v 214 | for actualV.Type().Kind() == reflect.Ptr { 215 | actualV = actualV.Elem() 216 | } 217 | for i := 0; i < actualV.NumField(); i++ { 218 | f := actualV.Field(i) 219 | if (f.Type().Kind() == reflect.Struct || f.Type().Kind() == reflect.Ptr) && !f.Type().Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) { 220 | fm := pointersOf(f) 221 | for k, p := range fm { 222 | m[k] = p 223 | } 224 | } else { 225 | fm := fieldMetadata(actualV.Type().Field(i), nil)[0] 226 | m[fm.Name] = actualV.Field(i) 227 | } 228 | } 229 | 230 | return m 231 | } 232 | func genericSet(obj Entity, name string, value interface{}) { 233 | n2p := pointersOf(reflect.ValueOf(obj)) 234 | var val interface{} 235 | for k, v := range n2p { 236 | if k == name { 237 | val = v 238 | } 239 | } 240 | val.(reflect.Value).Set(reflect.ValueOf(value)) 241 | } 242 | func schemaOfHeavyReflectionStuff(v Entity) *schema { 243 | userEntityConfigurator := newEntityConfigurator() 244 | v.ConfigureEntity(userEntityConfigurator) 245 | for _, relation := range userEntityConfigurator.resolveRelations { 246 | relation() 247 | } 248 | schema := &schema{} 249 | if userEntityConfigurator.connection != "" { 250 | schema.Connection = userEntityConfigurator.connection 251 | } 252 | if userEntityConfigurator.table != "" { 253 | schema.Table = userEntityConfigurator.table 254 | } else { 255 | panic("you need to have table name for getting schema.") 256 | } 257 | 258 | schema.columnConstraints = userEntityConfigurator.columnConstraints 259 | if schema.Connection == "" { 260 | schema.Connection = "default" 261 | } 262 | if schema.fields == nil { 263 | schema.fields = genericFieldsOf(v) 264 | } 265 | if schema.getPK == nil { 266 | schema.getPK = genericGetPKValue 267 | } 268 | 269 | if schema.setPK == nil { 270 | schema.setPK = genericSetPkValue 271 | } 272 | 273 | schema.relations = userEntityConfigurator.relations 274 | 275 | return schema 276 | } 277 | 278 | func (s *schema) getTable() string { 279 | return s.Table 280 | } 281 | 282 | func (s *schema) getSQLDB() *sql.DB { 283 | return s.getConnection().DB 284 | } 285 | 286 | func (s *schema) getConnection() *connection { 287 | if len(globalConnections) > 1 && (s.Connection == "" || s.Table == "") { 288 | panic("need table and DB name when having more than 1 DB registered") 289 | } 290 | if len(globalConnections) == 1 { 291 | for _, db := range globalConnections { 292 | return db 293 | } 294 | } 295 | if db, exists := globalConnections[fmt.Sprintf("%s", s.Connection)]; exists { 296 | return db 297 | } 298 | panic("no db found") 299 | } 300 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func setup(t *testing.T) { 11 | db, err := sql.Open("sqlite3", ":memory:") 12 | err = SetupConnections(ConnectionConfig{ 13 | Name: "default", 14 | DB: db, 15 | Dialect: Dialects.SQLite3, 16 | }) 17 | // orm.Schematic() 18 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`) 19 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`) 20 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`) 21 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`) 22 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`) 23 | _, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`) 24 | assert.NoError(t, err) 25 | } 26 | 27 | type Object struct { 28 | ID int64 29 | Name string 30 | Timestamps 31 | } 32 | 33 | func (o Object) ConfigureEntity(e *EntityConfigurator) { 34 | e.Table("objects").Connection("default") 35 | } 36 | 37 | func TestGenericFieldsOf(t *testing.T) { 38 | t.Run("fields of with id and timestamps embedded", func(t *testing.T) { 39 | fs := genericFieldsOf(&Object{}) 40 | assert.Len(t, fs, 5) 41 | assert.Equal(t, "id", fs[0].Name) 42 | assert.True(t, fs[0].IsPK) 43 | assert.Equal(t, "name", fs[1].Name) 44 | assert.Equal(t, "created_at", fs[2].Name) 45 | assert.Equal(t, "updated_at", fs[3].Name) 46 | assert.Equal(t, "deleted_at", fs[4].Name) 47 | }) 48 | } 49 | 50 | func TestGenericValuesOf(t *testing.T) { 51 | t.Run("values of", func(t *testing.T) { 52 | 53 | setup(t) 54 | vs := genericValuesOf(Object{}, true) 55 | assert.Len(t, vs, 5) 56 | }) 57 | } 58 | 59 | func TestEntityConfigurator(t *testing.T) { 60 | t.Run("test has many with user provided values", func(t *testing.T) { 61 | setup(t) 62 | var ec EntityConfigurator 63 | ec.Table("users").Connection("default").HasMany(Object{}, HasManyConfig{ 64 | "objects", "user_id", 65 | }) 66 | 67 | }) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /timestamps.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | type Timestamps struct { 8 | CreatedAt sql.NullTime 9 | UpdatedAt sql.NullTime 10 | DeletedAt sql.NullTime 11 | } 12 | --------------------------------------------------------------------------------