├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── License ├── README.md ├── binuuid.go ├── binuuid_test.go ├── date.go ├── date_test.go ├── go.mod ├── go.sum ├── json.go ├── json_map.go ├── json_map_test.go ├── json_test.go ├── json_type.go ├── json_type_test.go ├── main_test.go ├── null.go ├── null_test.go ├── test_all.sh ├── time.go ├── time_test.go ├── url.go ├── url_test.go ├── uuid.go └── uuid_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "gomod" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'gh-pages' 7 | pull_request: 8 | branches-ignore: 9 | - 'gh-pages' 10 | 11 | jobs: 12 | # Label of the container job 13 | sqlite: 14 | strategy: 15 | matrix: 16 | go: ['1.22', '1.21', '1.20'] 17 | platform: [ubuntu-latest, macos-latest] # can not run in windows OS 18 | runs-on: ${{ matrix.platform }} 19 | 20 | steps: 21 | - name: Set up Go 1.x 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v4 28 | 29 | - name: go mod pakcage cache 30 | uses: actions/cache@v3 31 | with: 32 | path: ~/go/pkg/mod 33 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }} 34 | 35 | - name: Tests 36 | run: GORM_DIALECT=sqlite ./test_all.sh 37 | 38 | mysql: 39 | strategy: 40 | matrix: 41 | dbversion: ['mysql:latest', 'mysql:5.7'] 42 | go: ['1.22', '1.21', '1.20'] 43 | platform: [ubuntu-latest] 44 | runs-on: ${{ matrix.platform }} 45 | 46 | services: 47 | mysql: 48 | image: ${{ matrix.dbversion }} 49 | env: 50 | MYSQL_DATABASE: gorm 51 | MYSQL_USER: gorm 52 | MYSQL_PASSWORD: gorm 53 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 54 | ports: 55 | - 9910:3306 56 | options: >- 57 | --health-cmd "mysqladmin ping -ugorm -pgorm" 58 | --health-interval 10s 59 | --health-start-period 10s 60 | --health-timeout 5s 61 | --health-retries 10 62 | 63 | steps: 64 | - name: Set up Go 1.x 65 | uses: actions/setup-go@v4 66 | with: 67 | go-version: ${{ matrix.go }} 68 | 69 | - name: Check out code into the Go module directory 70 | uses: actions/checkout@v4 71 | 72 | - name: go mod pakcage cache 73 | uses: actions/cache@v3 74 | with: 75 | path: ~/go/pkg/mod 76 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }} 77 | 78 | - name: Tests 79 | run: GORM_DIALECT=mysql GORM_DSN="gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True" ./test_all.sh 80 | 81 | mariadb: 82 | strategy: 83 | matrix: 84 | dbversion: [ 'mariadb:latest' ] 85 | go: ['1.22', '1.21', '1.20'] 86 | platform: [ ubuntu-latest ] 87 | runs-on: ${{ matrix.platform }} 88 | 89 | services: 90 | mysql: 91 | image: ${{ matrix.dbversion }} 92 | env: 93 | MYSQL_DATABASE: gorm 94 | MYSQL_USER: gorm 95 | MYSQL_PASSWORD: gorm 96 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 97 | ports: 98 | - 9910:3306 99 | options: >- 100 | --health-cmd "mariadb-admin ping -ugorm -pgorm" 101 | --health-interval 10s 102 | --health-start-period 10s 103 | --health-timeout 5s 104 | --health-retries 10 105 | 106 | steps: 107 | - name: Set up Go 1.x 108 | uses: actions/setup-go@v4 109 | with: 110 | go-version: ${{ matrix.go }} 111 | 112 | - name: Check out code into the Go module directory 113 | uses: actions/checkout@v4 114 | 115 | 116 | - name: go mod pakcage cache 117 | uses: actions/cache@v3 118 | with: 119 | path: ~/go/pkg/mod 120 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }} 121 | 122 | - name: Tests 123 | run: GORM_DIALECT=mysql GORM_DSN="gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True" ./test_all.sh 124 | 125 | postgres: 126 | strategy: 127 | matrix: 128 | dbversion: ['postgres:latest', 'postgres:11', 'postgres:10'] 129 | go: ['1.22', '1.21', '1.20'] 130 | platform: [ubuntu-latest] # can not run in macOS and widnowsOS 131 | runs-on: ${{ matrix.platform }} 132 | 133 | services: 134 | postgres: 135 | image: ${{ matrix.dbversion }} 136 | env: 137 | POSTGRES_PASSWORD: gorm 138 | POSTGRES_USER: gorm 139 | POSTGRES_DB: gorm 140 | TZ: Asia/Shanghai 141 | ports: 142 | - 9920:5432 143 | # Set health checks to wait until postgres has started 144 | options: >- 145 | --health-cmd pg_isready 146 | --health-interval 10s 147 | --health-timeout 5s 148 | --health-retries 5 149 | 150 | steps: 151 | - name: Set up Go 1.x 152 | uses: actions/setup-go@v4 153 | with: 154 | go-version: ${{ matrix.go }} 155 | 156 | - name: Check out code into the Go module directory 157 | uses: actions/checkout@v4 158 | 159 | - name: go mod pakcage cache 160 | uses: actions/cache@v3 161 | with: 162 | path: ~/go/pkg/mod 163 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }} 164 | 165 | - name: Tests 166 | run: GORM_DIALECT=postgres GORM_DSN="user=gorm password=gorm dbname=gorm host=localhost port=9920 sslmode=disable TimeZone=Asia/Shanghai" ./test_all.sh 167 | 168 | sqlserver: 169 | strategy: 170 | matrix: 171 | go: ['1.22', '1.21', '1.20'] 172 | platform: [ubuntu-latest] # can not run test in macOS and windows 173 | runs-on: ${{ matrix.platform }} 174 | 175 | services: 176 | mssql: 177 | image: mcr.microsoft.com/mssql/server:2022-latest 178 | env: 179 | TZ: Asia/Shanghai 180 | ACCEPT_EULA: Y 181 | MSSQL_SA_PASSWORD: LoremIpsum86 182 | ports: 183 | - 9930:1433 184 | options: >- 185 | --health-cmd="/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P ${MSSQL_SA_PASSWORD} -N -C -l 30 -Q \"SELECT 1\" || exit 1" 186 | --health-start-period 10s 187 | --health-interval 10s 188 | --health-timeout 5s 189 | --health-retries 10 190 | 191 | steps: 192 | - name: Set up Go 1.x 193 | uses: actions/setup-go@v4 194 | with: 195 | go-version: ${{ matrix.go }} 196 | 197 | - name: Check out code into the Go module directory 198 | uses: actions/checkout@v4 199 | 200 | - name: go mod pakcage cache 201 | uses: actions/cache@v3 202 | with: 203 | path: ~/go/pkg/mod 204 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('tests/go.mod') }} 205 | 206 | - name: Tests 207 | run: GORM_DIALECT=sqlserver GORM_DSN="sqlserver://sa:LoremIpsum86@localhost:9930?database=master" ./test_all.sh 208 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-NOW Jinzhu 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GORM Data Types 2 | 3 | ## JSON 4 | 5 | sqlite, mysql, postgres supported 6 | 7 | ```go 8 | import "gorm.io/datatypes" 9 | 10 | type UserWithJSON struct { 11 | gorm.Model 12 | Name string 13 | Attributes datatypes.JSON 14 | } 15 | 16 | DB.Create(&UserWithJSON{ 17 | Name: "json-1", 18 | Attributes: datatypes.JSON([]byte(`{"name": "jinzhu", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)), 19 | } 20 | 21 | // Check JSON has keys 22 | datatypes.JSONQuery("attributes").HasKey(value, keys...) 23 | 24 | db.Find(&user, datatypes.JSONQuery("attributes").HasKey("role")) 25 | db.Find(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga")) 26 | // MySQL 27 | // SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.role') IS NOT NULL 28 | // SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.orgs.orga') IS NOT NULL 29 | 30 | // PostgreSQL 31 | // SELECT * FROM "user" WHERE "attributes"::jsonb ? 'role' 32 | // SELECT * FROM "user" WHERE "attributes"::jsonb -> 'orgs' ? 'orga' 33 | 34 | 35 | // Check JSON extract value from keys equal to value 36 | datatypes.JSONQuery("attributes").Equals(value, keys...) 37 | 38 | DB.First(&user, datatypes.JSONQuery("attributes").Equals("jinzhu", "name")) 39 | DB.First(&user, datatypes.JSONQuery("attributes").Equals("orgb", "orgs", "orgb")) 40 | // MySQL 41 | // SELECT * FROM `user` WHERE JSON_EXTRACT(`attributes`, '$.name') = "jinzhu" 42 | // SELECT * FROM `user` WHERE JSON_EXTRACT(`attributes`, '$.orgs.orgb') = "orgb" 43 | 44 | // PostgreSQL 45 | // SELECT * FROM "user" WHERE json_extract_path_text("attributes"::json,'name') = 'jinzhu' 46 | // SELECT * FROM "user" WHERE json_extract_path_text("attributes"::json,'orgs','orgb') = 'orgb' 47 | ``` 48 | 49 | NOTE: SQlite need to build with `json1` tag, e.g: `go build --tags json1`, refer https://github.com/mattn/go-sqlite3#usage 50 | 51 | ## Date 52 | 53 | ```go 54 | import "gorm.io/datatypes" 55 | 56 | type UserWithDate struct { 57 | gorm.Model 58 | Name string 59 | Date datatypes.Date 60 | } 61 | 62 | user := UserWithDate{Name: "jinzhu", Date: datatypes.Date(time.Now())} 63 | DB.Create(&user) 64 | // INSERT INTO `user_with_dates` (`name`,`date`) VALUES ("jinzhu","2020-07-17 00:00:00") 65 | 66 | DB.First(&result, "name = ? AND date = ?", "jinzhu", datatypes.Date(curTime)) 67 | // SELECT * FROM user_with_dates WHERE name = "jinzhu" AND date = "2020-07-17 00:00:00" ORDER BY `user_with_dates`.`id` LIMIT 1 68 | ``` 69 | 70 | ## Time 71 | 72 | MySQL, PostgreSQL, SQLite, SQLServer are supported. 73 | 74 | Time with nanoseconds is supported for some databases which support for time with fractional second scale. 75 | 76 | ```go 77 | import "gorm.io/datatypes" 78 | 79 | type UserWithTime struct { 80 | gorm.Model 81 | Name string 82 | Time datatypes.Time 83 | } 84 | 85 | user := UserWithTime{Name: "jinzhu", Time: datatypes.NewTime(1, 2, 3, 0)} 86 | DB.Create(&user) 87 | // INSERT INTO `user_with_times` (`name`,`time`) VALUES ("jinzhu","01:02:03") 88 | 89 | DB.First(&result, "name = ? AND time = ?", "jinzhu", datatypes.NewTime(1, 2, 3, 0)) 90 | // SELECT * FROM user_with_times WHERE name = "jinzhu" AND time = "01:02:03" ORDER BY `user_with_times`.`id` LIMIT 1 91 | ``` 92 | 93 | NOTE: If the current using database is SQLite, the field column type is defined as `TEXT` type 94 | when GORM AutoMigrate because SQLite doesn't have time type. 95 | 96 | ## JSON_SET 97 | 98 | sqlite, mysql, postgres supported 99 | 100 | ```go 101 | import ( 102 | "gorm.io/datatypes" 103 | "gorm.io/gorm" 104 | ) 105 | 106 | type UserWithJSON struct { 107 | gorm.Model 108 | Name string 109 | Attributes datatypes.JSON 110 | } 111 | 112 | DB.Create(&UserWithJSON{ 113 | Name: "json-1", 114 | Attributes: datatypes.JSON([]byte(`{"name": "json-1", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)), 115 | }) 116 | 117 | type User struct { 118 | Name string 119 | Age int 120 | } 121 | 122 | friend := User{ 123 | Name: "Bob", 124 | Age: 21, 125 | } 126 | 127 | // Set fields of JSON column 128 | datatypes.JSONSet("attributes").Set("age", 20).Set("tags[0]", "tag2").Set("orgs.orga", "orgb") 129 | 130 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("age", 20).Set("tags[0]", "tag3").Set("orgs.orga", "orgb")) 131 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", []string{"10085", "10086"})) 132 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", gorm.Expr("CAST(? AS JSON)", `["10085", "10086"]`))) 133 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("friend", friend)) 134 | // MySQL 135 | // UPDATE `user_with_jsons` SET `attributes` = JSON_SET(`attributes`, '$.tags[0]', 'tag3', '$.orgs.orga', 'orgb', '$.age', 20) WHERE name = 'json-1' 136 | // UPDATE `user_with_jsons` SET `attributes` = JSON_SET(`attributes`, '$.phones', CAST('["10085", "10086"]' AS JSON)) WHERE name = 'json-1' 137 | // UPDATE `user_with_jsons` SET `attributes` = JSON_SET(`attributes`, '$.phones', CAST('["10085", "10086"]' AS JSON)) WHERE name = 'json-1' 138 | // UPDATE `user_with_jsons` SET `attributes` = JSON_SET(`attributes`, '$.friend', CAST('{"Name": "Bob", "Age": 21}' AS JSON)) WHERE name = 'json-1' 139 | ``` 140 | NOTE: MariaDB does not support CAST(? AS JSON). 141 | 142 | NOTE: Path in PostgreSQL is different. 143 | 144 | ```go 145 | // Set fields of JSON column 146 | datatypes.JSONSet("attributes").Set("{age}", 20).Set("{tags, 0}", "tag2").Set("{orgs, orga}", "orgb") 147 | 148 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{age}", 20).Set("{tags, 0}", "tag2").Set("{orgs, orga}", "orgb")) 149 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{phones}", []string{"10085", "10086"})) 150 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{phones}", gorm.Expr("?::jsonb", `["10085", "10086"]`))) 151 | DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{friend}", friend)) 152 | // PostgreSQL 153 | // UPDATE "user_with_jsons" SET "attributes" = JSONB_SET(JSONB_SET(JSONB_SET("attributes", '{age}', '20'), '{tags, 0}', '"tag2"'), '{orgs, orga}', '"orgb"') WHERE name = 'json-1' 154 | // UPDATE "user_with_jsons" SET "attributes" = JSONB_SET("attributes", '{phones}', '["10085","10086"]') WHERE name = 'json-1' 155 | // UPDATE "user_with_jsons" SET "attributes" = JSONB_SET("attributes", '{phones}', '["10085","10086"]'::jsonb) WHERE name = 'json-1' 156 | // UPDATE "user_with_jsons" SET "attributes" = JSONB_SET("attributes", '{friend}', '{"Name": "Bob", "Age": 21}') WHERE name = 'json-1' 157 | ``` 158 | 159 | ## JSONType[T] 160 | 161 | sqlite, mysql, postgres supported 162 | 163 | ```go 164 | import "gorm.io/datatypes" 165 | 166 | type Attribute struct { 167 | Sex int 168 | Age int 169 | Orgs map[string]string 170 | Tags []string 171 | Admin bool 172 | Role string 173 | } 174 | 175 | type UserWithJSON struct { 176 | gorm.Model 177 | Name string 178 | Attributes datatypes.JSONType[Attribute] 179 | } 180 | 181 | var user = UserWithJSON{ 182 | Name: "hello", 183 | Attributes: datatypes.NewJSONType(Attribute{ 184 | Age: 18, 185 | Sex: 1, 186 | Orgs: map[string]string{"orga": "orga"}, 187 | Tags: []string{"tag1", "tag2", "tag3"}, 188 | }), 189 | } 190 | 191 | // Create 192 | DB.Create(&user) 193 | 194 | // First 195 | var result UserWithJSON 196 | DB.First(&result, user.ID) 197 | 198 | // Update 199 | jsonMap = UserWithJSON{ 200 | Attributes: datatypes.NewJSONType(Attribute{ 201 | Age: 18, 202 | Sex: 1, 203 | Orgs: map[string]string{"orga": "orga"}, 204 | Tags: []string{"tag1", "tag2", "tag3"}, 205 | }), 206 | } 207 | 208 | DB.Model(&user).Updates(jsonMap) 209 | ``` 210 | 211 | NOTE: it's not support json query 212 | 213 | ## JSONSlice[T] 214 | 215 | sqlite, mysql, postgres supported 216 | 217 | ```go 218 | import "gorm.io/datatypes" 219 | 220 | type Tag struct { 221 | Name string 222 | Score float64 223 | } 224 | 225 | type UserWithJSON struct { 226 | gorm.Model 227 | Name string 228 | Tags datatypes.JSONSlice[Tag] 229 | } 230 | 231 | var tags = []Tag{{Name: "tag1", Score: 0.1}, {Name: "tag2", Score: 0.2}} 232 | var user = UserWithJSON{ 233 | Name: "hello", 234 | Tags: datatypes.NewJSONSlice(tags), 235 | } 236 | 237 | // Create 238 | DB.Create(&user) 239 | 240 | // First 241 | var result UserWithJSON 242 | DB.First(&result, user.ID) 243 | 244 | // Update 245 | var tags2 = []Tag{{Name: "tag3", Score: 10.1}, {Name: "tag4", Score: 10.2}} 246 | jsonMap = UserWithJSON{ 247 | Tags: datatypes.NewJSONSlice(tags2), 248 | } 249 | 250 | DB.Model(&user).Updates(jsonMap) 251 | ``` 252 | 253 | NOTE: it's not support json query and `db.Pluck` method 254 | 255 | ## JSONArray 256 | 257 | mysql supported 258 | 259 | ```go 260 | import "gorm.io/datatypes" 261 | 262 | type Param struct { 263 | ID int 264 | Letters string 265 | Config datatypes.JSON 266 | } 267 | 268 | //Create 269 | DB.Create(&Param{ 270 | Letters: "JSONArray-1", 271 | Config: datatypes.JSON("[\"a\", \"b\"]"), 272 | }) 273 | 274 | DB.Create(&Param{ 275 | Letters: "JSONArray-2", 276 | Config: datatypes.JSON("[\"a\", \"c\"]"), 277 | }) 278 | 279 | //Query 280 | var retMultiple []Param 281 | DB.Where(datatypes.JSONArrayQuery("config").Contains("c")).Find(&retMultiple) 282 | } 283 | ``` 284 | 285 | ## UUID 286 | 287 | MySQL, PostgreSQL, SQLServer and SQLite are supported. 288 | 289 | ```go 290 | import "gorm.io/datatypes" 291 | 292 | type UserWithUUID struct { 293 | gorm.Model 294 | Name string 295 | UserUUID datatypes.UUID 296 | } 297 | 298 | // Generate a new random UUID (version 4). 299 | userUUID := datatypes.NewUUIDv4() 300 | 301 | user := UserWithUUID{Name: "jinzhu", UserUUID: userUUID} 302 | DB.Create(&user) 303 | // INSERT INTO `user_with_uuids` (`name`,`user_uuid`) VALUES ("jinzhu","ca95a578-816c-4812-babd-a7602b042460") 304 | 305 | var result UserWithUUID 306 | DB.First(&result, "name = ? AND user_uuid = ?", "jinzhu", userUUID) 307 | // SELECT * FROM user_with_uuids WHERE name = "jinzhu" AND user_uuid = "ca95a578-816c-4812-babd-a7602b042460" ORDER BY `user_with_uuids`.`id` LIMIT 1 308 | 309 | // Use the datatype's Equals() to compare the UUIDs. 310 | if userCreate.UserUUID.Equals(userFound.UserUUID) { 311 | fmt.Println("User UUIDs match as expected.") 312 | } else { 313 | fmt.Println("User UUIDs do not match. Something is wrong.") 314 | } 315 | 316 | // Use the datatype's String() function to get the UUID as a string type. 317 | fmt.Printf("User UUID is %s", userFound.UserUUID.String()) 318 | 319 | // Check the UUID value with datatype's IsNil() and IsEmpty() functions. 320 | if userFound.UserUUID.IsNil() { 321 | fmt.Println("User UUID is a nil UUID (i.e. all bits are zero)") 322 | } 323 | if userFound.UserUUID.IsEmpty() { 324 | fmt.Println( 325 | "User UUID is empty (i.e. either a nil UUID or a zero length string)", 326 | ) 327 | } 328 | ``` 329 | -------------------------------------------------------------------------------- /binuuid.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "errors" 7 | 8 | "github.com/google/uuid" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | // This datatype is similar to datatypes.UUID, major difference being that 14 | // this datatype stores the uuid in the database as a binary (byte) array 15 | // instead of a string. Developers may use either as per their preference. 16 | type BinUUID uuid.UUID 17 | 18 | // NewBinUUIDv1 generates a uuid version 1, panics on generation failure. 19 | func NewBinUUIDv1() BinUUID { 20 | return BinUUID(uuid.Must(uuid.NewUUID())) 21 | } 22 | 23 | // NewBinUUIDv4 generates a uuid version 4, panics on generation failure. 24 | func NewBinUUIDv4() BinUUID { 25 | return BinUUID(uuid.Must(uuid.NewRandom())) 26 | } 27 | 28 | // NewNilBinUUID generates a nil uuid. 29 | func NewNilBinUUID() BinUUID { 30 | return BinUUID(uuid.Nil) 31 | } 32 | 33 | // BinUUIDFromString returns the BinUUID representation of the specified uuidStr. 34 | func BinUUIDFromString(uuidStr string) BinUUID { 35 | return BinUUID(uuid.MustParse(uuidStr)) 36 | } 37 | 38 | // GormDataType gorm common data type. 39 | func (BinUUID) GormDataType() string { 40 | return "BINARY(16)" 41 | } 42 | 43 | // GormDBDataType gorm db data type. 44 | func (BinUUID) GormDBDataType(db *gorm.DB, field *schema.Field) string { 45 | switch db.Dialector.Name() { 46 | case "mysql": 47 | return "BINARY(16)" 48 | case "postgres": 49 | return "BYTEA" 50 | case "sqlserver": 51 | return "BINARY(16)" 52 | case "sqlite": 53 | return "BLOB" 54 | default: 55 | return "" 56 | } 57 | } 58 | 59 | // Scan is the scanner function for this datatype. 60 | func (u *BinUUID) Scan(value interface{}) error { 61 | valueBytes, ok := value.([]byte) 62 | if !ok { 63 | return errors.New("unable to convert value to bytes") 64 | } 65 | valueUUID, err := uuid.FromBytes(valueBytes) 66 | if err != nil { 67 | return err 68 | } 69 | *u = BinUUID(valueUUID) 70 | return nil 71 | } 72 | 73 | // Value is the valuer function for this datatype. 74 | func (u BinUUID) Value() (driver.Value, error) { 75 | return uuid.UUID(u).MarshalBinary() 76 | } 77 | 78 | // String returns the string form of the UUID. 79 | func (u BinUUID) Bytes() []byte { 80 | bytes, err := uuid.UUID(u).MarshalBinary() 81 | if err != nil { 82 | return nil 83 | } 84 | return bytes 85 | } 86 | 87 | // String returns the string form of the UUID. 88 | func (u BinUUID) String() string { 89 | return uuid.UUID(u).String() 90 | } 91 | 92 | // Equals returns true if bytes form of BinUUID matches other, false otherwise. 93 | func (u BinUUID) Equals(other BinUUID) bool { 94 | return bytes.Equal(u.Bytes(), other.Bytes()) 95 | } 96 | 97 | // Length returns the number of characters in string form of UUID. 98 | func (u BinUUID) LengthBytes() int { 99 | return len(u.Bytes()) 100 | } 101 | 102 | // Length returns the number of characters in string form of UUID. 103 | func (u BinUUID) Length() int { 104 | return len(u.String()) 105 | } 106 | 107 | // IsNil returns true if the BinUUID is nil uuid (all zeroes), false otherwise. 108 | func (u BinUUID) IsNil() bool { 109 | return uuid.UUID(u) == uuid.Nil 110 | } 111 | 112 | // IsEmpty returns true if BinUUID is nil uuid or of zero length, false otherwise. 113 | func (u BinUUID) IsEmpty() bool { 114 | return u.IsNil() || u.Length() == 0 115 | } 116 | 117 | // IsNilPtr returns true if caller BinUUID ptr is nil, false otherwise. 118 | func (u *BinUUID) IsNilPtr() bool { 119 | return u == nil 120 | } 121 | 122 | // IsEmptyPtr returns true if caller BinUUID ptr is nil or it's value is empty. 123 | func (u *BinUUID) IsEmptyPtr() bool { 124 | return u.IsNilPtr() || u.IsEmpty() 125 | } 126 | -------------------------------------------------------------------------------- /binuuid_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/datatypes" 9 | "gorm.io/gorm" 10 | . "gorm.io/gorm/utils/tests" 11 | ) 12 | 13 | var _ driver.Valuer = &datatypes.BinUUID{} 14 | 15 | func TestBinUUID(t *testing.T) { 16 | if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { 17 | type UserWithBinUUID struct { 18 | gorm.Model 19 | Name string 20 | UserUUID datatypes.BinUUID 21 | } 22 | 23 | DB.Migrator().DropTable(&UserWithBinUUID{}) 24 | if err := DB.Migrator().AutoMigrate(&UserWithBinUUID{}); err != nil { 25 | t.Errorf("failed to migrate, got error: %v", err) 26 | } 27 | 28 | users := []UserWithBinUUID{{ 29 | Name: "uuid-1", 30 | UserUUID: datatypes.NewBinUUIDv1(), 31 | }, { 32 | Name: "uuid-2", 33 | UserUUID: datatypes.NewBinUUIDv1(), 34 | }, { 35 | Name: "uuid-3", 36 | UserUUID: datatypes.NewBinUUIDv4(), 37 | }, { 38 | Name: "uuid-4", 39 | UserUUID: datatypes.NewBinUUIDv4(), 40 | }} 41 | 42 | if err := DB.Create(&users).Error; err != nil { 43 | t.Errorf("Failed to create users %v", err) 44 | } 45 | 46 | for _, user := range users { 47 | result := UserWithBinUUID{} 48 | if err := DB.First( 49 | &result, "name = ? AND user_uuid = ?", 50 | user.Name, 51 | user.UserUUID, 52 | ).Error; err != nil { 53 | t.Fatalf("failed to find user with uuid, got error: %v", err) 54 | } 55 | AssertEqual(t, !result.UserUUID.IsEmpty(), true) 56 | AssertEqual(t, user.UserUUID.Equals(result.UserUUID), true) 57 | valueUser, err := user.UserUUID.Value() 58 | if err != nil { 59 | t.Fatalf("failed to get user value, got error: %v", err) 60 | } 61 | valueResult, err := result.UserUUID.Value() 62 | if err != nil { 63 | t.Fatalf("failed to get result value, got error: %v", err) 64 | } 65 | AssertEqual(t, valueUser, valueResult) 66 | AssertEqual(t, user.UserUUID.LengthBytes(), 16) 67 | AssertEqual(t, user.UserUUID.Length(), 36) 68 | } 69 | 70 | var tx *gorm.DB 71 | user1 := users[0] 72 | AssertEqual(t, user1.UserUUID.IsNil(), false) 73 | AssertEqual(t, user1.UserUUID.IsEmpty(), false) 74 | tx = DB.Model(&user1).Updates( 75 | map[string]interface{}{ 76 | "user_uuid": datatypes.BinUUIDFromString(uuid.Nil.String()), 77 | }, 78 | ) 79 | AssertEqual(t, tx.Error, nil) 80 | AssertEqual(t, user1.UserUUID.IsNil(), true) 81 | AssertEqual(t, user1.UserUUID.IsEmpty(), true) 82 | user1NewUUID := datatypes.NewBinUUIDv4() 83 | tx = DB.Model(&user1).Updates( 84 | map[string]interface{}{ 85 | "user_uuid": user1NewUUID, 86 | }, 87 | ) 88 | AssertEqual(t, tx.Error, nil) 89 | AssertEqual(t, user1.UserUUID, user1NewUUID) 90 | 91 | user2 := users[1] 92 | AssertEqual(t, user2.UserUUID.IsNil(), false) 93 | AssertEqual(t, user2.UserUUID.IsEmpty(), false) 94 | tx = DB.Model(&user2).Updates( 95 | map[string]interface{}{"user_uuid": datatypes.NewNilBinUUID()}, 96 | ) 97 | AssertEqual(t, tx.Error, nil) 98 | AssertEqual(t, user2.UserUUID.IsNil(), true) 99 | AssertEqual(t, user2.UserUUID.IsEmpty(), true) 100 | user2NewUUID := datatypes.NewBinUUIDv4() 101 | tx = DB.Model(&user2).Updates( 102 | map[string]interface{}{ 103 | "user_uuid": user2NewUUID, 104 | }, 105 | ) 106 | AssertEqual(t, tx.Error, nil) 107 | AssertEqual(t, user2.UserUUID, user2NewUUID) 108 | } 109 | } 110 | 111 | func TestBinUUIDPtr(t *testing.T) { 112 | if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { 113 | type UserWithBinUUIDPtr struct { 114 | gorm.Model 115 | Name string 116 | UserUUID *datatypes.BinUUID 117 | } 118 | 119 | DB.Migrator().DropTable(&UserWithBinUUIDPtr{}) 120 | if err := DB.Migrator().AutoMigrate(&UserWithBinUUIDPtr{}); err != nil { 121 | t.Errorf("failed to migrate, got error: %v", err) 122 | } 123 | 124 | uuid1 := datatypes.NewBinUUIDv1() 125 | uuid2 := datatypes.NewBinUUIDv1() 126 | uuid3 := datatypes.NewBinUUIDv4() 127 | uuid4 := datatypes.NewBinUUIDv4() 128 | 129 | users := []UserWithBinUUIDPtr{{ 130 | Name: "uuid-1", 131 | UserUUID: &uuid1, 132 | }, { 133 | Name: "uuid-2", 134 | UserUUID: &uuid2, 135 | }, { 136 | Name: "uuid-3", 137 | UserUUID: &uuid3, 138 | }, { 139 | Name: "uuid-4", 140 | UserUUID: &uuid4, 141 | }} 142 | 143 | if err := DB.Create(&users).Error; err != nil { 144 | t.Errorf("Failed to create users %v", err) 145 | } 146 | 147 | for _, user := range users { 148 | result := UserWithBinUUIDPtr{} 149 | if err := DB.First( 150 | &result, "name = ? AND user_uuid = ?", 151 | user.Name, 152 | *user.UserUUID, 153 | ).Error; err != nil { 154 | t.Fatalf("failed to find user with uuid, got error: %v", err) 155 | } 156 | AssertEqual(t, !result.UserUUID.IsEmpty(), true) 157 | AssertEqual(t, user.UserUUID, result.UserUUID) 158 | valueUser, err := user.UserUUID.Value() 159 | if err != nil { 160 | t.Fatalf("failed to get user value, got error: %v", err) 161 | } 162 | valueResult, err := result.UserUUID.Value() 163 | if err != nil { 164 | t.Fatalf("failed to get result value, got error: %v", err) 165 | } 166 | AssertEqual(t, valueUser, valueResult) 167 | AssertEqual(t, user.UserUUID.LengthBytes(), 16) 168 | AssertEqual(t, user.UserUUID.Length(), 36) 169 | } 170 | 171 | user1 := users[0] 172 | AssertEqual(t, user1.UserUUID.IsNilPtr(), false) 173 | AssertEqual(t, user1.UserUUID.IsEmptyPtr(), false) 174 | tx := DB.Model(&user1).Updates(map[string]interface{}{ 175 | "user_uuid": datatypes.NewNilBinUUID(), 176 | }) 177 | AssertEqual(t, tx.Error, nil) 178 | AssertEqual(t, user1.UserUUID.IsNil(), true) 179 | AssertEqual(t, user1.UserUUID.IsEmptyPtr(), true) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /date.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | type Date time.Time 10 | 11 | func (date *Date) Scan(value interface{}) (err error) { 12 | nullTime := &sql.NullTime{} 13 | err = nullTime.Scan(value) 14 | *date = Date(nullTime.Time) 15 | return 16 | } 17 | 18 | func (date Date) Value() (driver.Value, error) { 19 | y, m, d := time.Time(date).Date() 20 | return time.Date(y, m, d, 0, 0, 0, 0, time.Time(date).Location()), nil 21 | } 22 | 23 | // GormDataType gorm common data type 24 | func (date Date) GormDataType() string { 25 | return "date" 26 | } 27 | 28 | func (date Date) GobEncode() ([]byte, error) { 29 | return time.Time(date).GobEncode() 30 | } 31 | 32 | func (date *Date) GobDecode(b []byte) error { 33 | return (*time.Time)(date).GobDecode(b) 34 | } 35 | 36 | func (date Date) MarshalJSON() ([]byte, error) { 37 | return time.Time(date).MarshalJSON() 38 | } 39 | 40 | func (date *Date) UnmarshalJSON(b []byte) error { 41 | return (*time.Time)(date).UnmarshalJSON(b) 42 | } 43 | -------------------------------------------------------------------------------- /date_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "encoding/json" 7 | "testing" 8 | "time" 9 | 10 | "github.com/jinzhu/now" 11 | "gorm.io/datatypes" 12 | . "gorm.io/gorm/utils/tests" 13 | ) 14 | 15 | func TestDate(t *testing.T) { 16 | type UserWithDate struct { 17 | ID uint 18 | Name string 19 | Date datatypes.Date 20 | } 21 | 22 | DB.Migrator().DropTable(&UserWithDate{}) 23 | if err := DB.Migrator().AutoMigrate(&UserWithDate{}); err != nil { 24 | t.Errorf("failed to migrate, got error: %v", err) 25 | } 26 | 27 | curTime := time.Now().UTC() 28 | beginningOfDay := now.New(curTime).BeginningOfDay() 29 | 30 | user := UserWithDate{Name: "jinzhu", Date: datatypes.Date(curTime)} 31 | DB.Create(&user) 32 | 33 | result := UserWithDate{} 34 | if err := DB.First(&result, "name = ? AND date = ?", "jinzhu", datatypes.Date(curTime)).Error; err != nil { 35 | t.Fatalf("Failed to find record with date") 36 | } 37 | 38 | AssertEqual(t, result.Date, beginningOfDay) 39 | } 40 | 41 | func TestGobEncoding(t *testing.T) { 42 | date := datatypes.Date(time.Now()) 43 | var buf bytes.Buffer 44 | enc := gob.NewEncoder(&buf) 45 | if err := enc.Encode(date); err != nil { 46 | t.Fatalf("failed to encode datatypes.Date: %v", err) 47 | } 48 | 49 | dec := gob.NewDecoder(&buf) 50 | var got datatypes.Date 51 | if err := dec.Decode(&got); err != nil { 52 | t.Fatalf("failed to decode to datatypes.Date: %v", err) 53 | } 54 | 55 | AssertEqual(t, date, got) 56 | } 57 | 58 | func TestJSONEncoding(t *testing.T) { 59 | date := datatypes.Date(time.Now()) 60 | b, err := json.Marshal(date) 61 | if err != nil { 62 | t.Fatalf("failed to encode datatypes.Date: %v", err) 63 | } 64 | 65 | var got datatypes.Date 66 | if err := json.Unmarshal(b, &got); err != nil { 67 | t.Fatalf("failed to decode to datatypes.Date: %v", err) 68 | } 69 | 70 | AssertEqual(t, date, got) 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gorm.io/datatypes 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/jinzhu/now v1.1.5 8 | gorm.io/driver/mysql v1.5.6 9 | gorm.io/driver/postgres v1.5.0 10 | gorm.io/driver/sqlite v1.4.3 11 | gorm.io/driver/sqlserver v1.6.0 12 | gorm.io/gorm v1.30.0 13 | ) 14 | 15 | require ( 16 | filippo.io/edwards25519 v1.1.0 // indirect 17 | github.com/go-sql-driver/mysql v1.8.1 // indirect 18 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 19 | github.com/golang-sql/sqlexp v0.1.0 // indirect 20 | github.com/jackc/pgpassfile v1.0.0 // indirect 21 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect 22 | github.com/jackc/pgx/v5 v5.5.5 // indirect 23 | github.com/jackc/puddle/v2 v2.2.1 // indirect 24 | github.com/jinzhu/inflection v1.0.0 // indirect 25 | github.com/mattn/go-sqlite3 v1.14.15 // indirect 26 | github.com/microsoft/go-mssqldb v1.7.2 // indirect 27 | golang.org/x/crypto v0.23.0 // indirect 28 | golang.org/x/sync v0.9.0 // indirect 29 | golang.org/x/text v0.20.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= 4 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= 5 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= 7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= 8 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= 9 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= 10 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= 11 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= 12 | github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= 13 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= 14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= 19 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 20 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 21 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 22 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 23 | github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 24 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 25 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 26 | github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 27 | github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= 28 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 29 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 30 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 31 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 32 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 33 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 37 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 38 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 39 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 40 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 41 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 42 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 43 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 44 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 45 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 46 | github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= 47 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 48 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 49 | github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 50 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 51 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 52 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 53 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 54 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 55 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 56 | github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 57 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 58 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 59 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 60 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 61 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 62 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 63 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 65 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 69 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 70 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 71 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 72 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 73 | github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook= 74 | github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= 75 | github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= 76 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 77 | github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 78 | github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= 79 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 80 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 86 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 89 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 91 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 92 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 93 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 94 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 95 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 96 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 98 | golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 100 | golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 101 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 102 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 103 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 104 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 105 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 106 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 107 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 108 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 109 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 110 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 111 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 112 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 113 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 114 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 115 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 116 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 117 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 118 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 119 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 120 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 121 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 122 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 123 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 124 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 125 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 126 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 130 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 131 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 132 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 133 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 134 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 150 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 151 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 152 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 153 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 154 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 155 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 156 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 157 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 158 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 159 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 160 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 161 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 162 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 163 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 164 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 165 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 166 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 167 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 168 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 169 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 170 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 171 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 173 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 174 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 175 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 176 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 177 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 181 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 182 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 183 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 185 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 187 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 189 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 190 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 191 | gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= 192 | gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 193 | gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= 194 | gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= 195 | gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= 196 | gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 197 | gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= 198 | gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= 199 | gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 200 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 201 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 202 | gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= 203 | gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 204 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | 14 | "gorm.io/driver/mysql" 15 | "gorm.io/gorm" 16 | "gorm.io/gorm/clause" 17 | "gorm.io/gorm/schema" 18 | ) 19 | 20 | // JSON defined JSON data type, need to implements driver.Valuer, sql.Scanner interface 21 | type JSON json.RawMessage 22 | 23 | // Value return json value, implement driver.Valuer interface 24 | func (j JSON) Value() (driver.Value, error) { 25 | if len(j) == 0 { 26 | return nil, nil 27 | } 28 | return string(j), nil 29 | } 30 | 31 | // Scan scan value into Jsonb, implements sql.Scanner interface 32 | func (j *JSON) Scan(value interface{}) error { 33 | if value == nil { 34 | *j = JSON("null") 35 | return nil 36 | } 37 | var bytes []byte 38 | switch v := value.(type) { 39 | case []byte: 40 | if len(v) > 0 { 41 | bytes = make([]byte, len(v)) 42 | copy(bytes, v) 43 | } 44 | case string: 45 | bytes = []byte(v) 46 | default: 47 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 48 | } 49 | 50 | result := json.RawMessage(bytes) 51 | *j = JSON(result) 52 | return nil 53 | } 54 | 55 | // MarshalJSON to output non base64 encoded []byte 56 | func (j JSON) MarshalJSON() ([]byte, error) { 57 | return json.RawMessage(j).MarshalJSON() 58 | } 59 | 60 | // UnmarshalJSON to deserialize []byte 61 | func (j *JSON) UnmarshalJSON(b []byte) error { 62 | result := json.RawMessage{} 63 | err := result.UnmarshalJSON(b) 64 | *j = JSON(result) 65 | return err 66 | } 67 | 68 | func (j JSON) String() string { 69 | return string(j) 70 | } 71 | 72 | // GormDataType gorm common data type 73 | func (JSON) GormDataType() string { 74 | return "json" 75 | } 76 | 77 | // GormDBDataType gorm db data type 78 | func (JSON) GormDBDataType(db *gorm.DB, field *schema.Field) string { 79 | switch db.Dialector.Name() { 80 | case "sqlite": 81 | return "JSON" 82 | case "mysql": 83 | return "JSON" 84 | case "postgres": 85 | return "JSONB" 86 | } 87 | return "" 88 | } 89 | 90 | func (js JSON) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { 91 | if len(js) == 0 { 92 | return gorm.Expr("NULL") 93 | } 94 | 95 | data, _ := js.MarshalJSON() 96 | 97 | switch db.Dialector.Name() { 98 | case "mysql": 99 | if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") { 100 | return gorm.Expr("CAST(? AS JSON)", string(data)) 101 | } 102 | } 103 | 104 | return gorm.Expr("?", string(data)) 105 | } 106 | 107 | // JSONQueryExpression json query expression, implements clause.Expression interface to use as querier 108 | type JSONQueryExpression struct { 109 | column string 110 | keys []string 111 | hasKeys bool 112 | equals bool 113 | likes bool 114 | equalsValue interface{} 115 | extract bool 116 | path string 117 | } 118 | 119 | // JSONQuery query column as json 120 | func JSONQuery(column string) *JSONQueryExpression { 121 | return &JSONQueryExpression{column: column} 122 | } 123 | 124 | // Extract extract json with path 125 | func (jsonQuery *JSONQueryExpression) Extract(path string) *JSONQueryExpression { 126 | jsonQuery.extract = true 127 | jsonQuery.path = path 128 | return jsonQuery 129 | } 130 | 131 | // HasKey returns clause.Expression 132 | func (jsonQuery *JSONQueryExpression) HasKey(keys ...string) *JSONQueryExpression { 133 | jsonQuery.keys = keys 134 | jsonQuery.hasKeys = true 135 | return jsonQuery 136 | } 137 | 138 | // Keys returns clause.Expression 139 | func (jsonQuery *JSONQueryExpression) Equals(value interface{}, keys ...string) *JSONQueryExpression { 140 | jsonQuery.keys = keys 141 | jsonQuery.equals = true 142 | jsonQuery.equalsValue = value 143 | return jsonQuery 144 | } 145 | 146 | // Likes return clause.Expression 147 | func (jsonQuery *JSONQueryExpression) Likes(value interface{}, keys ...string) *JSONQueryExpression { 148 | jsonQuery.keys = keys 149 | jsonQuery.likes = true 150 | jsonQuery.equalsValue = value 151 | return jsonQuery 152 | } 153 | 154 | // Build implements clause.Expression 155 | func (jsonQuery *JSONQueryExpression) Build(builder clause.Builder) { 156 | if stmt, ok := builder.(*gorm.Statement); ok { 157 | switch stmt.Dialector.Name() { 158 | case "mysql", "sqlite": 159 | switch { 160 | case jsonQuery.extract: 161 | builder.WriteString("JSON_EXTRACT(") 162 | builder.WriteQuoted(jsonQuery.column) 163 | builder.WriteByte(',') 164 | builder.AddVar(stmt, prefix+jsonQuery.path) 165 | builder.WriteString(")") 166 | case jsonQuery.hasKeys: 167 | if len(jsonQuery.keys) > 0 { 168 | builder.WriteString("JSON_EXTRACT(") 169 | builder.WriteQuoted(jsonQuery.column) 170 | builder.WriteByte(',') 171 | builder.AddVar(stmt, jsonQueryJoin(jsonQuery.keys)) 172 | builder.WriteString(") IS NOT NULL") 173 | } 174 | case jsonQuery.equals: 175 | if len(jsonQuery.keys) > 0 { 176 | builder.WriteString("JSON_EXTRACT(") 177 | builder.WriteQuoted(jsonQuery.column) 178 | builder.WriteByte(',') 179 | builder.AddVar(stmt, jsonQueryJoin(jsonQuery.keys)) 180 | builder.WriteString(") = ") 181 | if value, ok := jsonQuery.equalsValue.(bool); ok { 182 | builder.WriteString(strconv.FormatBool(value)) 183 | } else { 184 | stmt.AddVar(builder, jsonQuery.equalsValue) 185 | } 186 | } 187 | case jsonQuery.likes: 188 | if len(jsonQuery.keys) > 0 { 189 | builder.WriteString("JSON_EXTRACT(") 190 | builder.WriteQuoted(jsonQuery.column) 191 | builder.WriteByte(',') 192 | builder.AddVar(stmt, jsonQueryJoin(jsonQuery.keys)) 193 | builder.WriteString(") LIKE ") 194 | if value, ok := jsonQuery.equalsValue.(bool); ok { 195 | builder.WriteString(strconv.FormatBool(value)) 196 | } else { 197 | stmt.AddVar(builder, jsonQuery.equalsValue) 198 | } 199 | } 200 | } 201 | case "postgres": 202 | switch { 203 | case jsonQuery.extract: 204 | builder.WriteString(fmt.Sprintf("json_extract_path_text(%v::json,", stmt.Quote(jsonQuery.column))) 205 | stmt.AddVar(builder, jsonQuery.path) 206 | builder.WriteByte(')') 207 | case jsonQuery.hasKeys: 208 | if len(jsonQuery.keys) > 0 { 209 | stmt.WriteQuoted(jsonQuery.column) 210 | stmt.WriteString("::jsonb") 211 | for _, key := range jsonQuery.keys[0 : len(jsonQuery.keys)-1] { 212 | stmt.WriteString(" -> ") 213 | stmt.AddVar(builder, key) 214 | } 215 | 216 | stmt.WriteString(" ? ") 217 | stmt.AddVar(builder, jsonQuery.keys[len(jsonQuery.keys)-1]) 218 | } 219 | case jsonQuery.equals: 220 | if len(jsonQuery.keys) > 0 { 221 | builder.WriteString(fmt.Sprintf("json_extract_path_text(%v::json,", stmt.Quote(jsonQuery.column))) 222 | 223 | for idx, key := range jsonQuery.keys { 224 | if idx > 0 { 225 | builder.WriteByte(',') 226 | } 227 | stmt.AddVar(builder, key) 228 | } 229 | builder.WriteString(") = ") 230 | 231 | if _, ok := jsonQuery.equalsValue.(string); ok { 232 | stmt.AddVar(builder, jsonQuery.equalsValue) 233 | } else { 234 | stmt.AddVar(builder, fmt.Sprint(jsonQuery.equalsValue)) 235 | } 236 | } 237 | case jsonQuery.likes: 238 | if len(jsonQuery.keys) > 0 { 239 | builder.WriteString(fmt.Sprintf("json_extract_path_text(%v::json,", stmt.Quote(jsonQuery.column))) 240 | 241 | for idx, key := range jsonQuery.keys { 242 | if idx > 0 { 243 | builder.WriteByte(',') 244 | } 245 | stmt.AddVar(builder, key) 246 | } 247 | builder.WriteString(") LIKE ") 248 | 249 | if _, ok := jsonQuery.equalsValue.(string); ok { 250 | stmt.AddVar(builder, jsonQuery.equalsValue) 251 | } else { 252 | stmt.AddVar(builder, fmt.Sprint(jsonQuery.equalsValue)) 253 | } 254 | } 255 | } 256 | } 257 | } 258 | } 259 | 260 | // JSONOverlapsExpression JSON_OVERLAPS expression, implements clause.Expression interface to use as querier 261 | type JSONOverlapsExpression struct { 262 | column clause.Expression 263 | val string 264 | } 265 | 266 | // JSONOverlaps query column as json 267 | func JSONOverlaps(column clause.Expression, value string) *JSONOverlapsExpression { 268 | return &JSONOverlapsExpression{ 269 | column: column, 270 | val: value, 271 | } 272 | } 273 | 274 | // Build implements clause.Expression 275 | // only mysql support JSON_OVERLAPS 276 | func (json *JSONOverlapsExpression) Build(builder clause.Builder) { 277 | if stmt, ok := builder.(*gorm.Statement); ok { 278 | switch stmt.Dialector.Name() { 279 | case "mysql": 280 | builder.WriteString("JSON_OVERLAPS(") 281 | json.column.Build(builder) 282 | builder.WriteString(",") 283 | builder.AddVar(stmt, json.val) 284 | builder.WriteString(")") 285 | } 286 | } 287 | } 288 | 289 | type columnExpression string 290 | 291 | func Column(col string) columnExpression { 292 | return columnExpression(col) 293 | } 294 | 295 | func (col columnExpression) Build(builder clause.Builder) { 296 | if stmt, ok := builder.(*gorm.Statement); ok { 297 | switch stmt.Dialector.Name() { 298 | case "mysql", "sqlite", "postgres": 299 | builder.WriteString(stmt.Quote(string(col))) 300 | } 301 | } 302 | } 303 | 304 | const prefix = "$." 305 | 306 | func jsonQueryJoin(keys []string) string { 307 | if len(keys) == 1 { 308 | return prefix + keys[0] 309 | } 310 | 311 | n := len(prefix) 312 | n += len(keys) - 1 313 | for i := 0; i < len(keys); i++ { 314 | n += len(keys[i]) 315 | } 316 | 317 | var b strings.Builder 318 | b.Grow(n) 319 | b.WriteString(prefix) 320 | b.WriteString(keys[0]) 321 | for _, key := range keys[1:] { 322 | b.WriteString(".") 323 | b.WriteString(key) 324 | } 325 | return b.String() 326 | } 327 | 328 | // JSONSetExpression json set expression, implements clause.Expression interface to use as updater 329 | type JSONSetExpression struct { 330 | column string 331 | path2value map[string]interface{} 332 | mutex sync.RWMutex 333 | } 334 | 335 | // JSONSet update fields of json column 336 | func JSONSet(column string) *JSONSetExpression { 337 | return &JSONSetExpression{column: column, path2value: make(map[string]interface{})} 338 | } 339 | 340 | // Set return clause.Expression. 341 | // 342 | // { 343 | // "age": 20, 344 | // "name": "json-1", 345 | // "orgs": {"orga": "orgv"}, 346 | // "tags": ["tag1", "tag2"] 347 | // } 348 | // 349 | // // In MySQL/SQLite, path is `age`, `name`, `orgs.orga`, `tags[0]`, `tags[1]`. 350 | // DB.UpdateColumn("attr", JSONSet("attr").Set("orgs.orga", 42)) 351 | // 352 | // // In PostgreSQL, path is `{age}`, `{name}`, `{orgs,orga}`, `{tags, 0}`, `{tags, 1}`. 353 | // DB.UpdateColumn("attr", JSONSet("attr").Set("{orgs, orga}", "bar")) 354 | func (jsonSet *JSONSetExpression) Set(path string, value interface{}) *JSONSetExpression { 355 | jsonSet.mutex.Lock() 356 | jsonSet.path2value[path] = value 357 | jsonSet.mutex.Unlock() 358 | return jsonSet 359 | } 360 | 361 | // Build implements clause.Expression 362 | // support mysql, sqlite and postgres 363 | func (jsonSet *JSONSetExpression) Build(builder clause.Builder) { 364 | if stmt, ok := builder.(*gorm.Statement); ok { 365 | switch stmt.Dialector.Name() { 366 | case "mysql": 367 | 368 | var isMariaDB bool 369 | if v, ok := stmt.Dialector.(*mysql.Dialector); ok { 370 | isMariaDB = strings.Contains(v.ServerVersion, "MariaDB") 371 | } 372 | 373 | builder.WriteString("JSON_SET(") 374 | builder.WriteQuoted(jsonSet.column) 375 | for path, value := range jsonSet.path2value { 376 | builder.WriteByte(',') 377 | builder.AddVar(stmt, prefix+path) 378 | builder.WriteByte(',') 379 | 380 | if _, ok := value.(clause.Expression); ok { 381 | stmt.AddVar(builder, value) 382 | continue 383 | } 384 | 385 | rv := reflect.ValueOf(value) 386 | if rv.Kind() == reflect.Ptr { 387 | rv = rv.Elem() 388 | } 389 | switch rv.Kind() { 390 | case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map: 391 | b, _ := json.Marshal(value) 392 | if isMariaDB { 393 | stmt.AddVar(builder, string(b)) 394 | break 395 | } 396 | stmt.AddVar(builder, gorm.Expr("CAST(? AS JSON)", string(b))) 397 | case reflect.Bool: 398 | builder.WriteString(strconv.FormatBool(rv.Bool())) 399 | default: 400 | stmt.AddVar(builder, value) 401 | } 402 | } 403 | builder.WriteString(")") 404 | 405 | case "sqlite": 406 | builder.WriteString("JSON_SET(") 407 | builder.WriteQuoted(jsonSet.column) 408 | for path, value := range jsonSet.path2value { 409 | builder.WriteByte(',') 410 | builder.AddVar(stmt, prefix+path) 411 | builder.WriteByte(',') 412 | 413 | if _, ok := value.(clause.Expression); ok { 414 | stmt.AddVar(builder, value) 415 | continue 416 | } 417 | 418 | rv := reflect.ValueOf(value) 419 | if rv.Kind() == reflect.Ptr { 420 | rv = rv.Elem() 421 | } 422 | switch rv.Kind() { 423 | case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map: 424 | b, _ := json.Marshal(value) 425 | stmt.AddVar(builder, gorm.Expr("JSON(?)", string(b))) 426 | default: 427 | stmt.AddVar(builder, value) 428 | } 429 | } 430 | builder.WriteString(")") 431 | 432 | case "postgres": 433 | var expr clause.Expression = columnExpression(jsonSet.column) 434 | for path, value := range jsonSet.path2value { 435 | if _, ok = value.(clause.Expression); ok { 436 | expr = gorm.Expr("JSONB_SET(?,?,?)", expr, path, value) 437 | continue 438 | } else { 439 | b, _ := json.Marshal(value) 440 | expr = gorm.Expr("JSONB_SET(?,?,?)", expr, path, string(b)) 441 | } 442 | } 443 | stmt.AddVar(builder, expr) 444 | } 445 | } 446 | } 447 | 448 | func JSONArrayQuery(column string) *JSONArrayExpression { 449 | return &JSONArrayExpression{ 450 | column: column, 451 | } 452 | } 453 | 454 | type JSONArrayExpression struct { 455 | contains bool 456 | in bool 457 | column string 458 | keys []string 459 | equalsValue interface{} 460 | } 461 | 462 | // Contains checks if column[keys] contains the value given. The keys parameter is only supported for MySQL and SQLite. 463 | func (json *JSONArrayExpression) Contains(value interface{}, keys ...string) *JSONArrayExpression { 464 | json.contains = true 465 | json.equalsValue = value 466 | json.keys = keys 467 | return json 468 | } 469 | 470 | // In checks if columns[keys] is in the array value given. This method is only supported for MySQL and SQLite. 471 | func (json *JSONArrayExpression) In(value interface{}, keys ...string) *JSONArrayExpression { 472 | json.in = true 473 | json.keys = keys 474 | json.equalsValue = value 475 | return json 476 | } 477 | 478 | // Build implements clause.Expression 479 | func (json *JSONArrayExpression) Build(builder clause.Builder) { 480 | if stmt, ok := builder.(*gorm.Statement); ok { 481 | switch stmt.Dialector.Name() { 482 | case "mysql": 483 | switch { 484 | case json.contains: 485 | builder.WriteString("JSON_CONTAINS(" + stmt.Quote(json.column) + ",JSON_ARRAY(") 486 | builder.AddVar(stmt, json.equalsValue) 487 | builder.WriteByte(')') 488 | if len(json.keys) > 0 { 489 | builder.WriteByte(',') 490 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 491 | } 492 | builder.WriteByte(')') 493 | case json.in: 494 | builder.WriteString("JSON_CONTAINS(JSON_ARRAY") 495 | builder.AddVar(stmt, json.equalsValue) 496 | builder.WriteByte(',') 497 | if len(json.keys) > 0 { 498 | builder.WriteString("JSON_EXTRACT(") 499 | } 500 | builder.WriteQuoted(json.column) 501 | if len(json.keys) > 0 { 502 | builder.WriteByte(',') 503 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 504 | builder.WriteByte(')') 505 | } 506 | builder.WriteByte(')') 507 | } 508 | case "sqlite": 509 | switch { 510 | case json.contains: 511 | builder.WriteString("EXISTS(SELECT 1 FROM json_each(") 512 | builder.WriteQuoted(json.column) 513 | if len(json.keys) > 0 { 514 | builder.WriteByte(',') 515 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 516 | } 517 | builder.WriteString(") WHERE value = ") 518 | builder.AddVar(stmt, json.equalsValue) 519 | builder.WriteString(") AND json_array_length(") 520 | builder.WriteQuoted(json.column) 521 | if len(json.keys) > 0 { 522 | builder.WriteByte(',') 523 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 524 | } 525 | builder.WriteString(") > 0") 526 | case json.in: 527 | builder.WriteString("CASE WHEN json_type(") 528 | builder.WriteQuoted(json.column) 529 | if len(json.keys) > 0 { 530 | builder.WriteByte(',') 531 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 532 | } 533 | builder.WriteString(") = 'array' THEN NOT EXISTS(SELECT 1 FROM json_each(") 534 | builder.WriteQuoted(json.column) 535 | if len(json.keys) > 0 { 536 | builder.WriteByte(',') 537 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 538 | } 539 | builder.WriteString(") WHERE value NOT IN ") 540 | builder.AddVar(stmt, json.equalsValue) 541 | builder.WriteString(") ELSE ") 542 | if len(json.keys) > 0 { 543 | builder.WriteString("json_extract(") 544 | } 545 | builder.WriteQuoted(json.column) 546 | if len(json.keys) > 0 { 547 | builder.WriteByte(',') 548 | builder.AddVar(stmt, jsonQueryJoin(json.keys)) 549 | builder.WriteByte(')') 550 | } 551 | builder.WriteString(" IN ") 552 | builder.AddVar(stmt, json.equalsValue) 553 | builder.WriteString(" END") 554 | } 555 | case "postgres": 556 | switch { 557 | case json.contains: 558 | builder.WriteString(stmt.Quote(json.column)) 559 | builder.WriteString(" ? ") 560 | builder.AddVar(stmt, json.equalsValue) 561 | } 562 | } 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /json_map.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql/driver" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "strings" 11 | 12 | "gorm.io/driver/mysql" 13 | "gorm.io/gorm" 14 | "gorm.io/gorm/clause" 15 | "gorm.io/gorm/schema" 16 | ) 17 | 18 | // JSONMap defined JSON data type, need to implements driver.Valuer, sql.Scanner interface 19 | type JSONMap map[string]interface{} 20 | 21 | // Value return json value, implement driver.Valuer interface 22 | func (m JSONMap) Value() (driver.Value, error) { 23 | if m == nil { 24 | return nil, nil 25 | } 26 | ba, err := m.MarshalJSON() 27 | return string(ba), err 28 | } 29 | 30 | // Scan scan value into Jsonb, implements sql.Scanner interface 31 | func (m *JSONMap) Scan(val interface{}) error { 32 | if val == nil { 33 | *m = make(JSONMap) 34 | return nil 35 | } 36 | var ba []byte 37 | switch v := val.(type) { 38 | case []byte: 39 | ba = v 40 | case string: 41 | ba = []byte(v) 42 | default: 43 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", val)) 44 | } 45 | t := map[string]interface{}{} 46 | rd := bytes.NewReader(ba) 47 | decoder := json.NewDecoder(rd) 48 | decoder.UseNumber() 49 | err := decoder.Decode(&t) 50 | *m = t 51 | return err 52 | } 53 | 54 | // MarshalJSON to output non base64 encoded []byte 55 | func (m JSONMap) MarshalJSON() ([]byte, error) { 56 | if m == nil { 57 | return []byte("null"), nil 58 | } 59 | t := (map[string]interface{})(m) 60 | return json.Marshal(t) 61 | } 62 | 63 | // UnmarshalJSON to deserialize []byte 64 | func (m *JSONMap) UnmarshalJSON(b []byte) error { 65 | t := map[string]interface{}{} 66 | err := json.Unmarshal(b, &t) 67 | *m = JSONMap(t) 68 | return err 69 | } 70 | 71 | // GormDataType gorm common data type 72 | func (m JSONMap) GormDataType() string { 73 | return "jsonmap" 74 | } 75 | 76 | // GormDBDataType gorm db data type 77 | func (JSONMap) GormDBDataType(db *gorm.DB, field *schema.Field) string { 78 | switch db.Dialector.Name() { 79 | case "sqlite": 80 | return "JSON" 81 | case "mysql": 82 | return "JSON" 83 | case "postgres": 84 | return "JSONB" 85 | case "sqlserver": 86 | return "NVARCHAR(MAX)" 87 | } 88 | return "" 89 | } 90 | 91 | func (jm JSONMap) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { 92 | data, _ := jm.MarshalJSON() 93 | switch db.Dialector.Name() { 94 | case "mysql": 95 | if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") { 96 | return gorm.Expr("CAST(? AS JSON)", string(data)) 97 | } 98 | } 99 | return gorm.Expr("?", string(data)) 100 | } 101 | -------------------------------------------------------------------------------- /json_map_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | 9 | "gorm.io/datatypes" 10 | "gorm.io/gorm" 11 | . "gorm.io/gorm/utils/tests" 12 | ) 13 | 14 | var _ driver.Valuer = &datatypes.JSON{} 15 | 16 | func TestJSONMap(t *testing.T) { 17 | if SupportedDriver("sqlite", "mysql", "postgres") { 18 | type UserWithJSONMap struct { 19 | gorm.Model 20 | Name string 21 | Attributes datatypes.JSONMap 22 | } 23 | 24 | DB.Migrator().DropTable(&UserWithJSONMap{}) 25 | if err := DB.Migrator().AutoMigrate(&UserWithJSONMap{}); err != nil { 26 | t.Errorf("failed to migrate, got error: %v", err) 27 | } 28 | 29 | // Go's json marshaler removes whitespace & orders keys alphabetically 30 | // use to compare against marshaled []byte of datatypes.JSON 31 | user1AttrsStr := `{"age":18,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"]}` 32 | user1Attrs := map[string]interface{}{ 33 | "age": 18, 34 | "name": "json-1", 35 | "orgs": map[string]interface{}{ 36 | "orga": "orga", 37 | }, 38 | "tags": []interface{}{"tag1", "tag2"}, 39 | } 40 | 41 | user2Attrs := map[string]interface{}{ 42 | "name": "json-2", 43 | "age": 28, 44 | "tags": []interface{}{"tag1", "tag3"}, 45 | "role": "admin", 46 | "orgs": map[string]interface{}{ 47 | "orgb": "orgb", 48 | }, 49 | } 50 | 51 | users := []UserWithJSONMap{{ 52 | Name: "json-1", 53 | Attributes: datatypes.JSONMap(user1Attrs), 54 | }, { 55 | Name: "json-2", 56 | Attributes: datatypes.JSONMap(user2Attrs), 57 | }, 58 | { 59 | Name: "json-3", 60 | Attributes: datatypes.JSONMap{}, 61 | }, 62 | } 63 | 64 | if err := DB.Create(&users).Error; err != nil { 65 | t.Errorf("Failed to create users %v", err) 66 | } 67 | 68 | var result UserWithJSONMap 69 | if err := DB.First(&result, datatypes.JSONQuery("attributes").HasKey("role")).Error; err != nil { 70 | t.Fatalf("failed to find user with json key, got error %v", err) 71 | } 72 | AssertEqual(t, result.Name, users[1].Name) 73 | 74 | var result2 UserWithJSONMap 75 | if err := DB.First(&result2, datatypes.JSONQuery("attributes").HasKey("orgs", "orga")).Error; err != nil { 76 | t.Fatalf("failed to find user with json key, got error %v", err) 77 | } 78 | AssertEqual(t, result2.Name, users[0].Name) 79 | 80 | AssertEqual(t, result2.Attributes, user1Attrs) 81 | 82 | // attributes should not marshal to base64 encoded []byte 83 | result2Attrs, err := json.Marshal(result2.Attributes) 84 | if err != nil { 85 | t.Fatalf("failed to marshal result2.Attributes, got error %v", err) 86 | } 87 | 88 | AssertEqual(t, string(result2Attrs), user1AttrsStr) 89 | 90 | // []byte should unmarshal into type datatypes.JSONMap 91 | var j datatypes.JSONMap 92 | if err := json.Unmarshal([]byte(user1AttrsStr), &j); err != nil { 93 | t.Fatalf("failed to unmarshal user1Attrs, got error %v", err) 94 | } 95 | 96 | AssertEqual(t, fmt.Sprint(j), fmt.Sprint(user1Attrs)) 97 | 98 | var result3 UserWithJSONMap 99 | if err := DB.First(&result3, datatypes.JSONQuery("attributes").Equals("json-1", "name")).Error; err != nil { 100 | t.Fatalf("failed to find user with json value, got error %v", err) 101 | } 102 | AssertEqual(t, result3.Name, users[0].Name) 103 | 104 | var result4 UserWithJSONMap 105 | if err := DB.First(&result4, datatypes.JSONQuery("attributes").Equals("orgb", "orgs", "orgb")).Error; err != nil { 106 | t.Fatalf("failed to find user with json value, got error %v", err) 107 | } 108 | AssertEqual(t, result4.Name, users[1].Name) 109 | 110 | // FirstOrCreate 111 | jsonMap := map[string]interface{}{"Attributes": datatypes.JSON(`{"age":19,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"]}`)} 112 | if err := DB.Where(&UserWithJSONMap{Name: "json-1"}).Assign(jsonMap).FirstOrCreate(&UserWithJSONMap{}).Error; err != nil { 113 | t.Errorf("failed to run FirstOrCreate") 114 | } 115 | 116 | var result5 UserWithJSONMap 117 | if err := DB.First(&result5, datatypes.JSONQuery("attributes").Equals(19, "age")).Error; err != nil { 118 | t.Fatalf("failed to find user with json value, got error %v", err) 119 | } 120 | 121 | var result6 UserWithJSONMap 122 | if err := DB.Where("name = ?", "json-3").First(&result6).Error; err != nil { 123 | t.Fatalf("failed to find user with json value, got error %v", err) 124 | } 125 | 126 | AssertEqual(t, result6.Attributes, datatypes.JSONMap{}) 127 | 128 | type UserWithJSONMapPtr struct { 129 | gorm.Model 130 | Name string 131 | Attributes *datatypes.JSONMap 132 | } 133 | 134 | DB.Migrator().DropTable(&UserWithJSONMapPtr{}) 135 | if err := DB.Migrator().AutoMigrate(&UserWithJSONMapPtr{}); err != nil { 136 | t.Errorf("failed to migrate, got error: %v", err) 137 | } 138 | 139 | jm1 := datatypes.JSONMap(user1Attrs) 140 | 141 | ujmps := []*UserWithJSONMapPtr{ 142 | { 143 | Name: "json-4", 144 | Attributes: &jm1, 145 | }, 146 | { 147 | Name: "json-5", 148 | }, 149 | } 150 | 151 | if err := DB.Create(&ujmps).Error; err != nil { 152 | t.Errorf("Failed to create users %v", err) 153 | } 154 | 155 | var result7 UserWithJSONMapPtr 156 | if err := DB.Where("name = ?", "json-4").First(&result7).Error; err != nil { 157 | t.Fatalf("failed to find user with json value, got error %v", err) 158 | } 159 | 160 | AssertEqual(t, *result7.Attributes, jm1) 161 | 162 | var result8 UserWithJSONMapPtr 163 | if err := DB.Where("name = ?", "json-5").First(&result8).Error; err != nil { 164 | t.Fatalf("failed to find user with json value, got error %v", err) 165 | } 166 | 167 | AssertEqual(t, result8.Attributes, nil) 168 | 169 | var result9 UserWithJSONMapPtr 170 | if err := DB.Where(result8, "Attributes").First(&result9).Error; err != nil { 171 | t.Fatalf("failed to find user with json value, got error %v", err) 172 | } 173 | } 174 | } 175 | 176 | func TestJSONMap_Scan(t *testing.T) { 177 | content := `{"user_id": 1085238870184050699, "name": "Name of user"}` 178 | obj := make(datatypes.JSONMap) 179 | err := obj.Scan([]byte(content)) 180 | if err != nil { 181 | t.Fatalf("decode error %v", err) 182 | } 183 | AssertEqual(t, obj["user_id"], 1085238870184050699) 184 | } 185 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "strings" 7 | "testing" 8 | 9 | "gorm.io/datatypes" 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | . "gorm.io/gorm/utils/tests" 13 | ) 14 | 15 | var _ driver.Valuer = &datatypes.JSON{} 16 | 17 | func TestJSON(t *testing.T) { 18 | if SupportedDriver("sqlite", "mysql", "postgres") { 19 | type UserWithJSON struct { 20 | gorm.Model 21 | Name string 22 | Attributes datatypes.JSON 23 | } 24 | 25 | DB.Migrator().DropTable(&UserWithJSON{}) 26 | if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil { 27 | t.Errorf("failed to migrate, got error: %v", err) 28 | } 29 | 30 | // Go's json marshaler removes whitespace & orders keys alphabetically 31 | // use to compare against marshaled []byte of datatypes.JSON 32 | user1Attrs := `{"age":18,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"],"admin":true}` 33 | 34 | users := []UserWithJSON{{ 35 | Name: "json-1", 36 | Attributes: datatypes.JSON([]byte(user1Attrs)), 37 | }, { 38 | Name: "json-2", 39 | Attributes: datatypes.JSON([]byte(`{"name": "json-2", "age": 28, "tags": ["tag1", "tag3"], "role": "admin", "orgs": {"orgb": "orgb"}}`)), 40 | }, { 41 | Name: "json-3", 42 | Attributes: datatypes.JSON([]byte(`["tag1","tag2","tag3"]`)), 43 | }} 44 | 45 | if err := DB.Create(&users).Error; err != nil { 46 | t.Errorf("Failed to create users %v", err) 47 | } 48 | 49 | var result UserWithJSON 50 | if err := DB.First(&result, datatypes.JSONQuery("attributes").HasKey("role")).Error; err != nil { 51 | t.Fatalf("failed to find user with json key, got error %v", err) 52 | } 53 | AssertEqual(t, result.Name, users[1].Name) 54 | 55 | var result2 UserWithJSON 56 | if err := DB.First(&result2, datatypes.JSONQuery("attributes").HasKey("orgs", "orga")).Error; err != nil { 57 | t.Fatalf("failed to find user with json key, got error %v", err) 58 | } 59 | AssertEqual(t, result2.Name, users[0].Name) 60 | 61 | // attributes should not marshal to base64 encoded []byte 62 | result2Attrs, err := json.Marshal(&result2.Attributes) 63 | if err != nil { 64 | t.Fatalf("failed to marshal result2.Attributes, got error %v", err) 65 | } 66 | AssertEqual(t, string(result2Attrs), user1Attrs) 67 | 68 | // []byte should unmarshal into type datatypes.JSON 69 | var j datatypes.JSON 70 | if err := json.Unmarshal([]byte(user1Attrs), &j); err != nil { 71 | t.Fatalf("failed to unmarshal user1Attrs, got error %v", err) 72 | } 73 | 74 | AssertEqual(t, string(j), user1Attrs) 75 | 76 | var result3 UserWithJSON 77 | if err := DB.First(&result3, datatypes.JSONQuery("attributes").Equals("json-1", "name")).Error; err != nil { 78 | t.Fatalf("failed to find user with json value, got error %v", err) 79 | } 80 | AssertEqual(t, result3.Name, users[0].Name) 81 | 82 | var result4 UserWithJSON 83 | if err := DB.First(&result4, datatypes.JSONQuery("attributes").Equals("orgb", "orgs", "orgb")).Error; err != nil { 84 | t.Fatalf("failed to find user with json value, got error %v", err) 85 | } 86 | AssertEqual(t, result4.Name, users[1].Name) 87 | 88 | var results5 []UserWithJSON 89 | if err := DB.Where(datatypes.JSONQuery("attributes").HasKey("age")).Where(datatypes.JSONQuery("attributes").Equals(true, "admin")).Find(&results5).Error; err != nil || len(results5) != 1 { 90 | t.Fatalf("failed to find user with json value, got error %v", err) 91 | } 92 | AssertEqual(t, results5[0].Name, users[0].Name) 93 | 94 | // FirstOrCreate 95 | jsonMap := map[string]interface{}{"Attributes": datatypes.JSON(`{"age":19,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"]}`)} 96 | if err := DB.Where(&UserWithJSON{Name: "json-1"}).Assign(jsonMap).FirstOrCreate(&UserWithJSON{}).Error; err != nil { 97 | t.Errorf("failed to run FirstOrCreate") 98 | } 99 | 100 | var result6 UserWithJSON 101 | if err := DB.First(&result6, datatypes.JSONQuery("attributes").Equals(19, "age")).Error; err != nil { 102 | t.Fatalf("failed to find user with json value, got error %v", err) 103 | } 104 | 105 | // Update 106 | jsonMap = map[string]interface{}{"Attributes": datatypes.JSON(`{"age":29,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"]}`)} 107 | if err := DB.Model(&result3).Updates(jsonMap).Error; err != nil { 108 | t.Errorf("failed to run FirstOrCreate") 109 | } 110 | 111 | var result7 UserWithJSON 112 | if err := DB.First(&result7, datatypes.JSONQuery("attributes").Equals(29, "age")).Error; err != nil { 113 | t.Fatalf("failed to find user with json value, got error %v", err) 114 | } 115 | 116 | var result8 UserWithJSON 117 | if err := DB.Where(result7, "Attributes").First(&result8).Error; err != nil { 118 | t.Fatalf("failed to find user with json value, got error %v", err) 119 | } 120 | 121 | var results9 []UserWithJSON 122 | if err := DB.Where("? = ?", datatypes.JSONQuery("attributes").Extract("name"), "json-2").Find(&results9).Error; err != nil || len(results9) != 1 { 123 | t.Fatalf("failed to find user with json value, got error %v", err) 124 | } 125 | AssertEqual(t, results9[0].Name, users[1].Name) 126 | 127 | var result10 UserWithJSON 128 | if err := DB.First(&result10, datatypes.JSONQuery("attributes").Likes("%dmi%", "role")).Error; err != nil { 129 | t.Fatalf("failed to find user with json value, got error %v", err) 130 | } 131 | AssertEqual(t, result10.Name, users[1].Name) 132 | 133 | // not support for sqlite 134 | // JSONOverlaps 135 | //var result9 UserWithJSON 136 | //if err := DB.First(&result9, datatypes.JSONOverlaps("attributes", `["tag1","tag2"]`)).Error; err != nil { 137 | // t.Fatalf("failed to find user with json value, got error %v", err) 138 | //} 139 | } 140 | } 141 | 142 | func TestJSONSliceScan(t *testing.T) { 143 | if SupportedDriver("sqlite", "mysql", "postgres") { 144 | type Param struct { 145 | ID int 146 | DisplayName string 147 | Config datatypes.JSON 148 | } 149 | 150 | DB.Migrator().DropTable(&Param{}) 151 | if err := DB.Migrator().AutoMigrate(&Param{}); err != nil { 152 | t.Errorf("failed to migrate, got error: %v", err) 153 | } 154 | 155 | cmp1 := Param{ 156 | DisplayName: "TestJSONSliceScan-1", 157 | Config: datatypes.JSON("{\"param1\": 1234, \"param2\": \"test\"}"), 158 | } 159 | 160 | cmp2 := Param{ 161 | DisplayName: "TestJSONSliceScan-2", 162 | Config: datatypes.JSON("{\"param1\": 456, \"param2\": \"test2\"}"), 163 | } 164 | 165 | if err := DB.Create(&cmp1).Error; err != nil { 166 | t.Errorf("Failed to create param %v", err) 167 | } 168 | if err := DB.Create(&cmp2).Error; err != nil { 169 | t.Errorf("Failed to create param %v", err) 170 | } 171 | 172 | var retSingle1 Param 173 | if err := DB.Where("id = ?", cmp2.ID).First(&retSingle1).Error; err != nil { 174 | t.Errorf("Failed to find param %v", err) 175 | } 176 | 177 | var retSingle2 Param 178 | if err := DB.Where("id = ?", cmp2.ID).First(&retSingle2).Error; err != nil { 179 | t.Errorf("Failed to find param %v", err) 180 | } 181 | 182 | AssertEqual(t, retSingle1, cmp2) 183 | AssertEqual(t, retSingle2, cmp2) 184 | 185 | var retMultiple []Param 186 | if err := DB.Find(&retMultiple).Error; err != nil { 187 | t.Errorf("Failed to find param %v", err) 188 | } 189 | 190 | AssertEqual(t, retSingle1, cmp2) 191 | AssertEqual(t, retSingle2, cmp2) 192 | } 193 | } 194 | 195 | func TestPostgresJSONSet(t *testing.T) { 196 | if !SupportedDriver("postgres") { 197 | t.Skip() 198 | } 199 | 200 | type UserWithJSON struct { 201 | gorm.Model 202 | Name string 203 | Attributes datatypes.JSON 204 | } 205 | 206 | DB.Migrator().DropTable(&UserWithJSON{}) 207 | if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil { 208 | t.Errorf("failed to migrate, got error: %v", err) 209 | } 210 | 211 | users := []UserWithJSON{{ 212 | Name: "json-1", 213 | Attributes: datatypes.JSON(`{"name": "json-1", "age": 18, "orgs": {"orga": "orga"}, "tags": ["tag1", "tag2"], "admin": true}`), 214 | }, { 215 | Name: "json-2", 216 | Attributes: datatypes.JSON(`{"name": "json-2", "age": 28, "tags": ["tag1", "tag3"], "role": "admin", "orgs": {"orgb": "orgb"}}`), 217 | }, { 218 | Name: "json-3", 219 | Attributes: datatypes.JSON(`{"name": "json-3"}`), 220 | }, { 221 | Name: "json-4", 222 | Attributes: datatypes.JSON(`{"name": "json-4"}`), 223 | }} 224 | 225 | if err := DB.Create(&users).Error; err != nil { 226 | t.Errorf("Failed to create users %v", err) 227 | } 228 | 229 | tests := []struct { 230 | name string 231 | userName string 232 | path2value map[string]interface{} 233 | expect map[string]interface{} 234 | }{ 235 | { 236 | name: "update int and string", 237 | userName: "json-1", 238 | path2value: map[string]interface{}{ 239 | "{age}": 20, 240 | "{role}": "tester", 241 | }, 242 | expect: map[string]interface{}{ 243 | "age": 20, 244 | "role": "tester", 245 | }, 246 | }, { 247 | name: "update array child", 248 | userName: "json-2", 249 | path2value: map[string]interface{}{ 250 | "{tags, 0}": "tag2", 251 | }, 252 | expect: map[string]interface{}{ 253 | "tags": []string{"tag2", "tag3"}, 254 | }, 255 | }, { 256 | name: "update array", 257 | userName: "json-2", 258 | path2value: map[string]interface{}{ 259 | "{phones}": []string{"10086", "10085"}, 260 | }, 261 | expect: map[string]interface{}{ 262 | "phones": []string{"10086", "10085"}, 263 | }, 264 | }, { 265 | name: "update by expr", 266 | userName: "json-4", 267 | path2value: map[string]interface{}{ 268 | "{extra}": gorm.Expr("?::jsonb", `["a", "b"]`), 269 | }, 270 | expect: map[string]interface{}{ 271 | "extra": []string{"a", "b"}, 272 | }, 273 | }, 274 | } 275 | 276 | for _, test := range tests { 277 | t.Run(test.name, func(t *testing.T) { 278 | jsonSet := datatypes.JSONSet("attributes") 279 | for path, value := range test.path2value { 280 | jsonSet = jsonSet.Set(path, value) 281 | } 282 | 283 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", test.userName).UpdateColumn("attributes", jsonSet).Error; err != nil { 284 | t.Fatalf("failed to update user with json key, got error %v", err) 285 | } 286 | 287 | var result UserWithJSON 288 | if err := DB.First(&result, "name = ?", test.userName).Error; err != nil { 289 | t.Fatalf("failed to find user with json key, got error %v", err) 290 | } 291 | actual := make(map[string]interface{}) 292 | if err := json.Unmarshal(result.Attributes, &actual); err != nil { 293 | t.Fatalf("failed to unmarshal attributes, got err %v", err) 294 | } 295 | 296 | for key, value := range test.expect { 297 | AssertEqual(t, value, test.expect[key]) 298 | } 299 | }) 300 | } 301 | 302 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-3").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{friend}", users[0])).Error; err != nil { 303 | t.Fatalf("failed to update user with json key, got error %v", err) 304 | } 305 | var result UserWithJSON 306 | if err := DB.First(&result, "name = ?", "json-3").Error; err != nil { 307 | t.Fatalf("failed to find user with json key, got error %v", err) 308 | } 309 | actual := make(map[string]json.RawMessage) 310 | if err := json.Unmarshal(result.Attributes, &actual); err != nil { 311 | t.Fatalf("failed to unmarshal attributes, got err %v", err) 312 | } 313 | var friend UserWithJSON 314 | if err := json.Unmarshal(actual["friend"], &friend); err != nil { 315 | t.Fatalf("failed to unmarshal attributes, got err %v", err) 316 | } 317 | AssertEqual(t, friend.ID, users[0].ID) 318 | AssertEqual(t, friend.Name, users[0].Name) 319 | } 320 | 321 | func TestJSONSet(t *testing.T) { 322 | if SupportedDriver("sqlite", "mysql") { 323 | type UserWithJSON struct { 324 | gorm.Model 325 | Name string 326 | Attributes datatypes.JSON 327 | } 328 | 329 | DB.Migrator().DropTable(&UserWithJSON{}) 330 | if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil { 331 | t.Errorf("failed to migrate, got error: %v", err) 332 | } 333 | 334 | var isMariaDB bool 335 | if DB.Dialector.Name() == "mysql" { 336 | if v, ok := DB.Dialector.(*mysql.Dialector); ok { 337 | isMariaDB = strings.Contains(v.ServerVersion, "MariaDB") 338 | } 339 | } 340 | users := []UserWithJSON{{ 341 | Name: "json-1", 342 | Attributes: datatypes.JSON([]byte(`{"name": "json-1", "age": 18, "orgs": {"orga": "orga"}, "tags": ["tag1", "tag2"], "admin": true}`)), 343 | }, { 344 | Name: "json-2", 345 | Attributes: datatypes.JSON([]byte(`{"name": "json-2", "age": 28, "tags": ["tag1", "tag3"], "role": "admin", "orgs": {"orgb": "orgb"}}`)), 346 | }, { 347 | Name: "json-3", 348 | Attributes: datatypes.JSON([]byte(`{"name": "json-3"}`)), 349 | }, { 350 | Name: "json-4", 351 | Attributes: datatypes.JSON([]byte(`{"name": "json-4"}`)), 352 | }} 353 | 354 | if err := DB.Create(&users).Error; err != nil { 355 | t.Errorf("Failed to create users %v", err) 356 | } 357 | 358 | tmp := make(map[string]interface{}) 359 | 360 | // update int, string 361 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("age", 20).Set("role", "tester")).Error; err != nil { 362 | t.Fatalf("failed to update user with json key, got error %v", err) 363 | } 364 | var result UserWithJSON 365 | if err := DB.First(&result, "name = ?", "json-1").Error; err != nil { 366 | t.Fatalf("failed to find user with json key, got error %v", err) 367 | } 368 | _ = json.Unmarshal(result.Attributes, &tmp) 369 | AssertEqual(t, tmp["age"], 20) 370 | AssertEqual(t, tmp["role"], "tester") 371 | 372 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-2").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("tags[0]", "tag2")).Error; err != nil { 373 | t.Fatalf("failed to update user with json key, got error %v", err) 374 | } 375 | var result2 UserWithJSON 376 | if err := DB.First(&result2, "name = ?", "json-2").Error; err != nil { 377 | t.Fatalf("failed to find user with json key, got error %v", err) 378 | } 379 | _ = json.Unmarshal(result2.Attributes, &tmp) 380 | AssertEqual(t, tmp["tags"], []string{"tag2", "tag3"}) 381 | 382 | // MariaDB does not support CAST(? AS JSON), 383 | if isMariaDB { 384 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-2").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", []string{"10086", "10085"})).Error; err != nil { 385 | t.Fatalf("failed to update user with json key, got error %v", err) 386 | } 387 | var result3 UserWithJSON 388 | if err := DB.First(&result3, "name = ?", "json-2").Error; err != nil { 389 | t.Fatalf("failed to find user with json key, got error %v", err) 390 | } 391 | _ = json.Unmarshal(result3.Attributes, &tmp) 392 | AssertEqual(t, tmp["phones"], `["10086","10085"]`) 393 | 394 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-3").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("friend", result)).Error; err != nil { 395 | t.Fatalf("failed to update user with json key, got error %v", err) 396 | } 397 | var result4 UserWithJSON 398 | if err := DB.First(&result4, "name = ?", "json-3").Error; err != nil { 399 | t.Fatalf("failed to find user with json key, got error %v", err) 400 | } 401 | m := make(map[string]interface{}) 402 | 403 | _ = json.Unmarshal(result4.Attributes, &m) 404 | var tmpResult UserWithJSON 405 | _ = json.Unmarshal([]byte(m["friend"].(string)), &tmpResult) 406 | AssertEqual(t, tmpResult.Name, result.Name) 407 | 408 | } else { 409 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-2").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", []string{"10086", "10085"})).Error; err != nil { 410 | t.Fatalf("failed to update user with json key, got error %v", err) 411 | } 412 | var result3 UserWithJSON 413 | if err := DB.First(&result3, "name = ?", "json-2").Error; err != nil { 414 | t.Fatalf("failed to find user with json key, got error %v", err) 415 | } 416 | _ = json.Unmarshal(result3.Attributes, &tmp) 417 | AssertEqual(t, tmp["phones"], []string{"10086", "10085"}) 418 | 419 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-3").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("friend", result)).Error; err != nil { 420 | t.Fatalf("failed to update user with json key, got error %v", err) 421 | } 422 | var result4 UserWithJSON 423 | if err := DB.First(&result4, "name = ?", "json-3").Error; err != nil { 424 | t.Fatalf("failed to find user with json key, got error %v", err) 425 | } 426 | m := make(map[string]UserWithJSON) 427 | _ = json.Unmarshal(result4.Attributes, &m) 428 | AssertEqual(t, m["friend"], result) 429 | 430 | if DB.Dialector.Name() == "mysql" { 431 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-4").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("extra", gorm.Expr("CAST(? AS JSON)", `["a", "b"]`))).Error; err != nil { 432 | t.Fatalf("failed to update user with json key, got error %v", err) 433 | } 434 | } else if DB.Dialector.Name() == "sqlite" { 435 | if err := DB.Model(&UserWithJSON{}).Where("name = ?", "json-4").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("extra", gorm.Expr("JSON(?)", `["a", "b"]`))).Error; err != nil { 436 | t.Fatalf("failed to update user with json key, got error %v", err) 437 | } 438 | } 439 | var result5 UserWithJSON 440 | if err := DB.First(&result5, "name = ?", "json-4").Error; err != nil { 441 | t.Fatalf("failed to find user with json key, got error %v", err) 442 | } 443 | _ = json.Unmarshal(result5.Attributes, &tmp) 444 | AssertEqual(t, tmp["extra"], []string{"a", "b"}) 445 | } 446 | } 447 | } 448 | 449 | func TestJSONArrayQuery(t *testing.T) { 450 | if SupportedDriver("sqlite", "mysql") { 451 | type Param struct { 452 | ID int 453 | DisplayName string 454 | Config datatypes.JSON 455 | } 456 | 457 | DB.Migrator().DropTable(&Param{}) 458 | if err := DB.Migrator().AutoMigrate(&Param{}); err != nil { 459 | t.Errorf("failed to migrate, got error: %v", err) 460 | } 461 | 462 | cmp1 := Param{ 463 | DisplayName: "JSONArray-1", 464 | Config: datatypes.JSON("[\"a\", \"b\"]"), 465 | } 466 | cmp2 := Param{ 467 | DisplayName: "JSONArray-2", 468 | Config: datatypes.JSON("[\"c\", \"a\"]"), 469 | } 470 | cmp3 := Param{ 471 | DisplayName: "JSONArray-3", 472 | Config: datatypes.JSON("{\"test\": [\"a\", \"b\"]}"), 473 | } 474 | cmp4 := Param{ 475 | DisplayName: "JSONArray-4", 476 | Config: datatypes.JSON("{\"test\": \"c\"}"), 477 | } 478 | 479 | if err := DB.Create(&cmp1).Error; err != nil { 480 | t.Errorf("Failed to create param %v", err) 481 | } 482 | if err := DB.Create(&cmp2).Error; err != nil { 483 | t.Errorf("Failed to create param %v", err) 484 | } 485 | if err := DB.Create(&cmp3).Error; err != nil { 486 | t.Errorf("Failed to create param %v", err) 487 | } 488 | if err := DB.Create(&cmp4).Error; err != nil { 489 | t.Errorf("Failed to create param %v", err) 490 | } 491 | 492 | var retSingle1 Param 493 | if err := DB.Where("id = ?", cmp2.ID).First(&retSingle1).Error; err != nil { 494 | t.Errorf("Failed to find param %v", err) 495 | } 496 | 497 | var retSingle2 Param 498 | if err := DB.Where("id = ?", cmp2.ID).First(&retSingle2).Error; err != nil { 499 | t.Errorf("Failed to find param %v", err) 500 | } 501 | 502 | AssertEqual(t, retSingle1, cmp2) 503 | AssertEqual(t, retSingle2, cmp2) 504 | 505 | var retMultiple []Param 506 | 507 | if err := DB.Where(datatypes.JSONArrayQuery("config").Contains("c")).Find(&retMultiple).Error; err != nil { 508 | t.Fatalf("failed to find params with json value, got error %v", err) 509 | } 510 | AssertEqual(t, len(retMultiple), 1) 511 | 512 | if err := DB.Where(datatypes.JSONArrayQuery("config").Contains("a", "test")).Find(&retMultiple).Error; err != nil { 513 | t.Fatalf("failed to find params with json value and keys, got error %v", err) 514 | } 515 | AssertEqual(t, len(retMultiple), 1) 516 | 517 | if err := DB.Where(datatypes.JSONArrayQuery("config").In([]string{"c", "a"})).Find(&retMultiple).Error; err != nil { 518 | t.Fatalf("failed to find params with json value, got error %v", err) 519 | } 520 | AssertEqual(t, len(retMultiple), 1) 521 | 522 | if err := DB.Where(datatypes.JSONArrayQuery("config").In([]string{"c", "d"}, "test")).Find(&retMultiple).Error; err != nil { 523 | t.Fatalf("failed to find params with json value and keys, got error %v", err) 524 | } 525 | AssertEqual(t, len(retMultiple), 1) 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /json_type.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | 11 | "gorm.io/driver/mysql" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/clause" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | // JSONType give a generic data type for json encoded data. 18 | type JSONType[T any] struct { 19 | data T 20 | } 21 | 22 | func NewJSONType[T any](data T) JSONType[T] { 23 | return JSONType[T]{ 24 | data: data, 25 | } 26 | } 27 | 28 | // Data return data with generic Type T 29 | func (j JSONType[T]) Data() T { 30 | return j.data 31 | } 32 | 33 | // Value return json value, implement driver.Valuer interface 34 | func (j JSONType[T]) Value() (driver.Value, error) { 35 | return json.Marshal(j.data) 36 | } 37 | 38 | // Scan scan value into JSONType[T], implements sql.Scanner interface 39 | func (j *JSONType[T]) Scan(value interface{}) error { 40 | var bytes []byte 41 | switch v := value.(type) { 42 | case []byte: 43 | bytes = v 44 | case string: 45 | bytes = []byte(v) 46 | default: 47 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 48 | } 49 | return json.Unmarshal(bytes, &j.data) 50 | } 51 | 52 | // MarshalJSON to output non base64 encoded []byte 53 | func (j JSONType[T]) MarshalJSON() ([]byte, error) { 54 | return json.Marshal(j.data) 55 | } 56 | 57 | // UnmarshalJSON to deserialize []byte 58 | func (j *JSONType[T]) UnmarshalJSON(b []byte) error { 59 | return json.Unmarshal(b, &j.data) 60 | } 61 | 62 | // GormDataType gorm common data type 63 | func (JSONType[T]) GormDataType() string { 64 | return "json" 65 | } 66 | 67 | // GormDBDataType gorm db data type 68 | func (JSONType[T]) GormDBDataType(db *gorm.DB, field *schema.Field) string { 69 | switch db.Dialector.Name() { 70 | case "sqlite": 71 | return "JSON" 72 | case "mysql": 73 | return "JSON" 74 | case "postgres": 75 | return "JSONB" 76 | } 77 | return "" 78 | } 79 | 80 | func (js JSONType[T]) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { 81 | data, _ := js.MarshalJSON() 82 | 83 | switch db.Dialector.Name() { 84 | case "mysql": 85 | if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") { 86 | return gorm.Expr("CAST(? AS JSON)", string(data)) 87 | } 88 | } 89 | 90 | return gorm.Expr("?", string(data)) 91 | } 92 | 93 | // JSONSlice give a generic data type for json encoded slice data. 94 | type JSONSlice[T any] []T 95 | 96 | func NewJSONSlice[T any](s []T) JSONSlice[T] { 97 | return JSONSlice[T](s) 98 | } 99 | 100 | // Value return json value, implement driver.Valuer interface 101 | func (j JSONSlice[T]) Value() (driver.Value, error) { 102 | return json.Marshal(j) 103 | } 104 | 105 | // Scan scan value into JSONType[T], implements sql.Scanner interface 106 | func (j *JSONSlice[T]) Scan(value interface{}) error { 107 | var bytes []byte 108 | switch v := value.(type) { 109 | case []byte: 110 | bytes = v 111 | case string: 112 | bytes = []byte(v) 113 | default: 114 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 115 | } 116 | return json.Unmarshal(bytes, &j) 117 | } 118 | 119 | // GormDataType gorm common data type 120 | func (JSONSlice[T]) GormDataType() string { 121 | return "json" 122 | } 123 | 124 | // GormDBDataType gorm db data type 125 | func (JSONSlice[T]) GormDBDataType(db *gorm.DB, field *schema.Field) string { 126 | switch db.Dialector.Name() { 127 | case "sqlite": 128 | return "JSON" 129 | case "mysql": 130 | return "JSON" 131 | case "postgres": 132 | return "JSONB" 133 | } 134 | return "" 135 | } 136 | 137 | func (j JSONSlice[T]) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { 138 | data, _ := json.Marshal(j) 139 | 140 | switch db.Dialector.Name() { 141 | case "mysql": 142 | if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") { 143 | return gorm.Expr("CAST(? AS JSON)", string(data)) 144 | } 145 | } 146 | 147 | return gorm.Expr("?", string(data)) 148 | } 149 | -------------------------------------------------------------------------------- /json_type_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "testing" 6 | 7 | "gorm.io/datatypes" 8 | "gorm.io/gorm" 9 | . "gorm.io/gorm/utils/tests" 10 | ) 11 | 12 | var _ driver.Valuer = &datatypes.JSONType[[]int]{} 13 | 14 | func newJSONType[T any](b []byte) datatypes.JSONType[T] { 15 | var t datatypes.JSONType[T] 16 | _ = t.UnmarshalJSON(b) 17 | return t 18 | } 19 | 20 | func TestJSONType(t *testing.T) { 21 | if SupportedDriver("sqlite", "mysql", "postgres") { 22 | type Attribute struct { 23 | Sex int 24 | Age int 25 | Orgs map[string]string 26 | Tags []string 27 | Admin bool 28 | Role string 29 | } 30 | type UserWithJSON struct { 31 | gorm.Model 32 | Name string 33 | Attributes datatypes.JSONType[Attribute] 34 | } 35 | 36 | DB.Migrator().DropTable(&UserWithJSON{}) 37 | if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil { 38 | t.Errorf("failed to migrate, got error: %v", err) 39 | } 40 | 41 | // Go's json marshaler removes whitespace & orders keys alphabetically 42 | // use to compare against marshaled []byte of datatypes.JSON 43 | user1Attrs := `{"age":18,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"],"admin":true}` 44 | 45 | users := []UserWithJSON{{ 46 | Name: "json-1", 47 | Attributes: newJSONType[Attribute]([]byte(user1Attrs)), 48 | }, { 49 | Name: "json-2", 50 | Attributes: newJSONType[Attribute]([]byte(`{"name": "json-2", "age": 28, "tags": ["tag1", "tag3"], "role": "admin", "orgs": {"orgb": "orgb"}}`)), 51 | }, { 52 | Name: "json-3", 53 | Attributes: newJSONType[Attribute]([]byte(`{"tags": ["tag1","tag2","tag3"]`)), 54 | }, { 55 | Name: "json-4", 56 | Attributes: datatypes.NewJSONType(Attribute{Tags: []string{"tag1", "tag2", "tag3"}}), 57 | }, { 58 | Name: "json-5", 59 | Attributes: datatypes.NewJSONType(Attribute{Tags: []string{"tag1", "tag2", "tag3"}}), 60 | }} 61 | 62 | if err := DB.Create(&users).Error; err != nil { 63 | t.Errorf("Failed to create users %v", err) 64 | } 65 | 66 | var result UserWithJSON 67 | if err := DB.First(&result, users[1].ID).Error; err != nil { 68 | t.Fatalf("failed to find user with json key, got error %v", err) 69 | } 70 | AssertEqual(t, result.Name, users[1].Name) 71 | AssertEqual(t, result.Attributes.Data().Age, users[1].Attributes.Data().Age) 72 | AssertEqual(t, result.Attributes.Data().Admin, users[1].Attributes.Data().Admin) 73 | AssertEqual(t, len(result.Attributes.Data().Orgs), len(users[1].Attributes.Data().Orgs)) 74 | 75 | // List 76 | var users2 []UserWithJSON 77 | if err := DB.Model(&UserWithJSON{}).Limit(10).Order("id asc").Find(&users2).Error; err != nil { 78 | t.Fatalf("failed to select attribute field, got error %v", err) 79 | } 80 | AssertEqual(t, users2[0].Attributes.Data().Age, 18) 81 | 82 | // Select Field 83 | var singleUser UserWithJSON 84 | if err := DB.Model(&UserWithJSON{}).Select("attributes").Limit(1).Order("id asc").Find(&singleUser).Error; err != nil { 85 | t.Fatalf("failed to select attribute field, got error %v", err) 86 | } 87 | AssertEqual(t, singleUser.Attributes.Data().Age, 18) 88 | 89 | // Pluck 90 | var attr datatypes.JSONType[Attribute] 91 | if err := DB.Model(&UserWithJSON{}).Limit(1).Order("id asc").Pluck("attributes", &attr).Error; err != nil { 92 | t.Fatalf("failed to pluck for field, got error %v", err) 93 | } 94 | var attribute = attr.Data() 95 | AssertEqual(t, attribute.Age, 18) 96 | 97 | // Smart Select Fields 98 | var row struct { 99 | Attributes datatypes.JSONType[Attribute] 100 | } 101 | if err := DB.Model(&UserWithJSON{}).Limit(1).Order("id asc").Find(&row).Error; err != nil { 102 | t.Fatalf("failed to select attribute field, got error %v", err) 103 | } 104 | AssertEqual(t, row.Attributes.Data().Age, 18) 105 | 106 | // FirstOrCreate 107 | jsonMap := UserWithJSON{ 108 | Attributes: newJSONType[Attribute]([]byte(`{"age":19,"name":"json-1","orgs":{"orga":"orga"},"tags":["tag1","tag2"]}`)), 109 | } 110 | if err := DB.Where(&UserWithJSON{Name: "json-1"}).Assign(jsonMap).FirstOrCreate(&UserWithJSON{}).Error; err != nil { 111 | t.Errorf("failed to run FirstOrCreate") 112 | } 113 | 114 | // Update 115 | jsonMap = UserWithJSON{ 116 | Attributes: datatypes.NewJSONType( 117 | Attribute{ 118 | Age: 18, 119 | Sex: 1, 120 | Orgs: map[string]string{"orga": "orga"}, 121 | Tags: []string{"tag1", "tag2", "tag3"}, 122 | }, 123 | ), 124 | } 125 | var result3 UserWithJSON 126 | result3.ID = 1 127 | if err := DB.Model(&result3).Updates(jsonMap).Error; err != nil { 128 | t.Errorf("failed to run FirstOrCreate") 129 | } 130 | } 131 | } 132 | 133 | func TestJSONSlice(t *testing.T) { 134 | if SupportedDriver("sqlite", "mysql", "postgres") { 135 | type Tag struct { 136 | Name string 137 | Score float64 138 | } 139 | type UserWithJSON2 struct { 140 | gorm.Model 141 | Name string 142 | Tags datatypes.JSONSlice[Tag] 143 | } 144 | type UserWithJSON = UserWithJSON2 145 | 146 | DB.Migrator().DropTable(&UserWithJSON{}) 147 | if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil { 148 | t.Errorf("failed to migrate, got error: %v", err) 149 | } 150 | 151 | // Go's json marshaler removes whitespace & orders keys alphabetically 152 | // use to compare against marshaled []byte of datatypes.JSON 153 | var tags = []Tag{{Name: "tag1", Score: 0.1}, {Name: "tag2", Score: 0.2}} 154 | 155 | users := []UserWithJSON{{ 156 | Name: "json-1", 157 | Tags: datatypes.JSONSlice[Tag]{{Name: "tag1", Score: 1.1}, {Name: "tag2", Score: 1.2}}, 158 | }, { 159 | Name: "json-2", 160 | Tags: datatypes.NewJSONSlice([]Tag{{Name: "tag3", Score: 0.3}, {Name: "tag4", Score: 0.4}}), 161 | }, { 162 | Name: "json-3", 163 | Tags: datatypes.JSONSlice[Tag](tags), 164 | }, { 165 | Name: "json-4", 166 | Tags: datatypes.NewJSONSlice(tags), 167 | }} 168 | 169 | if err := DB.Create(&users).Error; err != nil { 170 | t.Errorf("Failed to create users %v", err) 171 | } 172 | 173 | var result UserWithJSON 174 | if err := DB.First(&result, users[0].ID).Error; err != nil { 175 | t.Fatalf("failed to find user with json key, got error %v", err) 176 | } 177 | AssertEqual(t, result.Name, users[0].Name) 178 | AssertEqual(t, result.Tags[0], users[0].Tags[0]) 179 | 180 | // Pluck 181 | /* 182 | var pluckTags datatypes.JSONSlice[Tag] 183 | if err := DB.Model(&UserWithJSON{}).Limit(1).Order("id asc").Pluck("tags", &pluckTags).Error; err != nil { 184 | t.Fatalf("failed to pluck for field, got error %v", err) 185 | } 186 | AssertEqual(t, len(pluckTags), 2) 187 | AssertEqual(t, pluckTags[0].Name, "tag1") 188 | */ 189 | 190 | // Smart Select Fields 191 | var row struct { 192 | Tags datatypes.JSONSlice[Tag] 193 | } 194 | if err := DB.Model(&UserWithJSON{}).Limit(1).Order("id asc").Find(&row).Error; err != nil { 195 | t.Fatalf("failed to select attribute field, got error %v", err) 196 | } 197 | AssertEqual(t, len(row.Tags), 2) 198 | AssertEqual(t, row.Tags[0].Name, "tag1") 199 | 200 | // FirstOrCreate 201 | jsonMap := UserWithJSON{ 202 | Tags: datatypes.NewJSONSlice(tags), 203 | } 204 | if err := DB.Where(&UserWithJSON{Name: "json-1"}).Assign(jsonMap).FirstOrCreate(&UserWithJSON{}).Error; err != nil { 205 | t.Errorf("failed to run FirstOrCreate") 206 | } 207 | 208 | // Update 209 | jsonMap = UserWithJSON{ 210 | Tags: datatypes.NewJSONSlice(tags), 211 | } 212 | var result3 UserWithJSON 213 | result3.ID = 1 214 | if err := DB.Model(&result3).Updates(jsonMap).Error; err != nil { 215 | t.Errorf("failed to run Updates") 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/driver/postgres" 10 | "gorm.io/driver/sqlite" 11 | "gorm.io/driver/sqlserver" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/logger" 14 | ) 15 | 16 | var DB *gorm.DB 17 | 18 | func init() { 19 | var err error 20 | if DB, err = OpenTestConnection(); err != nil { 21 | log.Printf("failed to connect database, got error %v\n", err) 22 | os.Exit(1) 23 | } 24 | } 25 | 26 | func OpenTestConnection() (db *gorm.DB, err error) { 27 | dbDSN := os.Getenv("GORM_DSN") 28 | dialect := os.Getenv("GORM_DIALECT") 29 | switch dialect { 30 | case "mysql": 31 | log.Println("testing mysql...") 32 | if dbDSN == "" { 33 | dbDSN = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=UTC" 34 | } 35 | db, err = gorm.Open(mysql.Open(dbDSN), &gorm.Config{}) 36 | case "postgres", "postgres_simple": 37 | log.Printf("testing %v...", dialect) 38 | if dbDSN == "" { 39 | dbDSN = "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai" 40 | } 41 | if dialect == "postgres" { 42 | db, err = gorm.Open(postgres.Open(dbDSN), &gorm.Config{}) 43 | } else { 44 | db, err = gorm.Open(postgres.New(postgres.Config{DSN: dbDSN, PreferSimpleProtocol: true}), &gorm.Config{}) 45 | } 46 | case "sqlserver": 47 | // CREATE LOGIN gorm WITH PASSWORD = 'LoremIpsum86'; 48 | // CREATE DATABASE gorm; 49 | // USE gorm; 50 | // CREATE USER gorm FROM LOGIN gorm; 51 | // sp_changedbowner 'gorm'; 52 | log.Println("testing sqlserver...") 53 | if dbDSN == "" { 54 | dbDSN = "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" 55 | } 56 | db, err = gorm.Open(sqlserver.Open(dbDSN), &gorm.Config{}) 57 | default: 58 | log.Println("testing sqlite3...") 59 | db, err = gorm.Open(sqlite.Open(filepath.Join(os.TempDir(), "gorm.db")), &gorm.Config{}) 60 | } 61 | 62 | if debug := os.Getenv("DEBUG"); debug == "true" { 63 | db.Logger = db.Logger.LogMode(logger.Info) 64 | } else if debug == "false" { 65 | db.Logger = db.Logger.LogMode(logger.Silent) 66 | } 67 | 68 | return 69 | } 70 | 71 | func SupportedDriver(dialectors ...string) bool { 72 | for _, dialect := range dialectors { 73 | if DB.Dialector.Name() == dialect { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | -------------------------------------------------------------------------------- /null.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // NullString represents a string that may be null. 14 | // NullString implements the [Scanner] interface so 15 | // it can be used as a scan destination: 16 | // 17 | // var s NullString 18 | // err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s) 19 | // ... 20 | // if s.Valid { 21 | // // use s.String 22 | // } else { 23 | // // NULL value 24 | // } 25 | type NullString = Null[string] 26 | 27 | // NullInt64 represents an int64 that may be null. 28 | // NullInt64 implements the [Scanner] interface so 29 | // it can be used as a scan destination, similar to [NullString]. 30 | type NullInt64 = Null[int64] 31 | 32 | // NullInt32 represents an int32 that may be null. 33 | // NullInt32 implements the [Scanner] interface so 34 | // it can be used as a scan destination, similar to [NullString]. 35 | type NullInt32 = Null[int32] 36 | 37 | // NullInt16 represents an int16 that may be null. 38 | // NullInt16 implements the [Scanner] interface so 39 | // it can be used as a scan destination, similar to [NullString]. 40 | type NullInt16 = Null[int16] 41 | 42 | // NullByte represents a byte that may be null. 43 | // NullByte implements the [Scanner] interface so 44 | // it can be used as a scan destination, similar to [NullString]. 45 | type NullByte = Null[byte] 46 | 47 | // NullFloat64 represents a float64 that may be null. 48 | // NullFloat64 implements the [Scanner] interface so 49 | // it can be used as a scan destination, similar to [NullString]. 50 | type NullFloat64 = Null[float64] 51 | 52 | // NullBool represents a bool that may be null. 53 | // NullBool implements the [Scanner] interface so 54 | // it can be used as a scan destination, similar to [NullString]. 55 | type NullBool = Null[bool] 56 | 57 | // NullTime represents a [time.Time] that may be null. 58 | // NullTime implements the [Scanner] interface so 59 | // it can be used as a scan destination, similar to [NullString]. 60 | type NullTime = Null[time.Time] 61 | 62 | // Null represents a value that may be null. 63 | // Null implements the [Scanner] interface so 64 | // it can be used as a scan destination: 65 | // 66 | // var s Null[string] 67 | // err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s) 68 | // ... 69 | // if s.Valid { 70 | // // use s.V 71 | // } else { 72 | // // NULL value 73 | // } 74 | type Null[T any] struct { 75 | V T 76 | Valid bool 77 | } 78 | 79 | func (n *Null[T]) Scan(value any) error { 80 | if value == nil { 81 | n.V, n.Valid = *new(T), false 82 | return nil 83 | } 84 | n.Valid = true 85 | return convertAssign(&n.V, value) 86 | } 87 | 88 | func (n Null[T]) Value() (driver.Value, error) { 89 | if !n.Valid { 90 | return nil, nil 91 | } 92 | return n.V, nil 93 | } 94 | 95 | // NewNull returns a new, non-null Null. 96 | func NewNull[T any](v T) Null[T] { 97 | return Null[T]{V: v, Valid: true} 98 | } 99 | 100 | var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error 101 | 102 | // convertAssign is the same as convertAssignRows, but without the optional 103 | // rows argument. 104 | func convertAssign(dest, src any) error { 105 | return convertAssignRows(dest, src, nil) 106 | } 107 | 108 | // convertAssignRows copies to dest the value in src, converting it if possible. 109 | // An error is returned if the copy would result in loss of information. 110 | // dest should be a pointer type. If rows is passed in, the rows will 111 | // be used as the parent for any cursor values converted from a 112 | // driver.Rows to a *Rows. 113 | func convertAssignRows(dest, src any, rows *sql.Rows) error { 114 | // Common cases, without reflect. 115 | switch s := src.(type) { 116 | case string: 117 | switch d := dest.(type) { 118 | case *string: 119 | if d == nil { 120 | return errNilPtr 121 | } 122 | *d = s 123 | return nil 124 | case *[]byte: 125 | if d == nil { 126 | return errNilPtr 127 | } 128 | *d = []byte(s) 129 | return nil 130 | case *sql.RawBytes: 131 | if d == nil { 132 | return errNilPtr 133 | } 134 | *d = append((*d)[:0], s...) 135 | return nil 136 | } 137 | case []byte: 138 | switch d := dest.(type) { 139 | case *string: 140 | if d == nil { 141 | return errNilPtr 142 | } 143 | *d = string(s) 144 | return nil 145 | case *any: 146 | if d == nil { 147 | return errNilPtr 148 | } 149 | *d = cloneBytes(s) 150 | return nil 151 | case *[]byte: 152 | if d == nil { 153 | return errNilPtr 154 | } 155 | *d = cloneBytes(s) 156 | return nil 157 | case *sql.RawBytes: 158 | if d == nil { 159 | return errNilPtr 160 | } 161 | *d = s 162 | return nil 163 | } 164 | case time.Time: 165 | switch d := dest.(type) { 166 | case *time.Time: 167 | *d = s 168 | return nil 169 | case *string: 170 | *d = s.Format(time.RFC3339Nano) 171 | return nil 172 | case *[]byte: 173 | if d == nil { 174 | return errNilPtr 175 | } 176 | *d = []byte(s.Format(time.RFC3339Nano)) 177 | return nil 178 | case *sql.RawBytes: 179 | if d == nil { 180 | return errNilPtr 181 | } 182 | *d = s.AppendFormat((*d)[:0], time.RFC3339Nano) 183 | return nil 184 | } 185 | case decimalDecompose: 186 | switch d := dest.(type) { 187 | case decimalCompose: 188 | return d.Compose(s.Decompose(nil)) 189 | } 190 | case nil: 191 | switch d := dest.(type) { 192 | case *any: 193 | if d == nil { 194 | return errNilPtr 195 | } 196 | *d = nil 197 | return nil 198 | case *[]byte: 199 | if d == nil { 200 | return errNilPtr 201 | } 202 | *d = nil 203 | return nil 204 | case *sql.RawBytes: 205 | if d == nil { 206 | return errNilPtr 207 | } 208 | *d = nil 209 | return nil 210 | } 211 | } 212 | 213 | var sv reflect.Value 214 | 215 | switch d := dest.(type) { 216 | case *string: 217 | sv = reflect.ValueOf(src) 218 | switch sv.Kind() { 219 | case reflect.Bool, 220 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 221 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 222 | reflect.Float32, reflect.Float64: 223 | *d = asString(src) 224 | return nil 225 | } 226 | case *[]byte: 227 | sv = reflect.ValueOf(src) 228 | if b, ok := asBytes(nil, sv); ok { 229 | *d = b 230 | return nil 231 | } 232 | case *sql.RawBytes: 233 | sv = reflect.ValueOf(src) 234 | if b, ok := asBytes([]byte(*d)[:0], sv); ok { 235 | *d = sql.RawBytes(b) 236 | return nil 237 | } 238 | case *bool: 239 | bv, err := driver.Bool.ConvertValue(src) 240 | if err == nil { 241 | *d = bv.(bool) 242 | } 243 | return err 244 | case *any: 245 | *d = src 246 | return nil 247 | } 248 | 249 | if scanner, ok := dest.(sql.Scanner); ok { 250 | return scanner.Scan(src) 251 | } 252 | 253 | dpv := reflect.ValueOf(dest) 254 | if dpv.Kind() != reflect.Pointer { 255 | return errors.New("destination not a pointer") 256 | } 257 | if dpv.IsNil() { 258 | return errNilPtr 259 | } 260 | 261 | if !sv.IsValid() { 262 | sv = reflect.ValueOf(src) 263 | } 264 | 265 | dv := reflect.Indirect(dpv) 266 | if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) { 267 | switch b := src.(type) { 268 | case []byte: 269 | dv.Set(reflect.ValueOf(cloneBytes(b))) 270 | default: 271 | dv.Set(sv) 272 | } 273 | return nil 274 | } 275 | 276 | if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) { 277 | dv.Set(sv.Convert(dv.Type())) 278 | return nil 279 | } 280 | 281 | // The following conversions use a string value as an intermediate representation 282 | // to convert between various numeric types. 283 | // 284 | // This also allows scanning into user defined types such as "type Int int64". 285 | // For symmetry, also check for string destination types. 286 | switch dv.Kind() { 287 | case reflect.Pointer: 288 | if src == nil { 289 | dv.Set(reflect.Zero(dv.Type())) 290 | return nil 291 | } 292 | dv.Set(reflect.New(dv.Type().Elem())) 293 | return convertAssignRows(dv.Interface(), src, rows) 294 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 295 | if src == nil { 296 | return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind()) 297 | } 298 | s := asString(src) 299 | i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) 300 | if err != nil { 301 | err = strconvErr(err) 302 | return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) 303 | } 304 | dv.SetInt(i64) 305 | return nil 306 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 307 | if src == nil { 308 | return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind()) 309 | } 310 | s := asString(src) 311 | u64, err := strconv.ParseUint(s, 10, dv.Type().Bits()) 312 | if err != nil { 313 | err = strconvErr(err) 314 | return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) 315 | } 316 | dv.SetUint(u64) 317 | return nil 318 | case reflect.Float32, reflect.Float64: 319 | if src == nil { 320 | return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind()) 321 | } 322 | s := asString(src) 323 | f64, err := strconv.ParseFloat(s, dv.Type().Bits()) 324 | if err != nil { 325 | err = strconvErr(err) 326 | return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) 327 | } 328 | dv.SetFloat(f64) 329 | return nil 330 | case reflect.String: 331 | if src == nil { 332 | return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind()) 333 | } 334 | switch v := src.(type) { 335 | case string: 336 | dv.SetString(v) 337 | return nil 338 | case []byte: 339 | dv.SetString(string(v)) 340 | return nil 341 | } 342 | } 343 | 344 | return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) 345 | } 346 | 347 | func strconvErr(err error) error { 348 | if ne, ok := err.(*strconv.NumError); ok { 349 | return ne.Err 350 | } 351 | return err 352 | } 353 | 354 | func asString(src any) string { 355 | switch v := src.(type) { 356 | case string: 357 | return v 358 | case []byte: 359 | return string(v) 360 | } 361 | rv := reflect.ValueOf(src) 362 | switch rv.Kind() { 363 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 364 | return strconv.FormatInt(rv.Int(), 10) 365 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 366 | return strconv.FormatUint(rv.Uint(), 10) 367 | case reflect.Float64: 368 | return strconv.FormatFloat(rv.Float(), 'g', -1, 64) 369 | case reflect.Float32: 370 | return strconv.FormatFloat(rv.Float(), 'g', -1, 32) 371 | case reflect.Bool: 372 | return strconv.FormatBool(rv.Bool()) 373 | } 374 | return fmt.Sprintf("%v", src) 375 | } 376 | 377 | func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) { 378 | switch rv.Kind() { 379 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 380 | return strconv.AppendInt(buf, rv.Int(), 10), true 381 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 382 | return strconv.AppendUint(buf, rv.Uint(), 10), true 383 | case reflect.Float32: 384 | return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true 385 | case reflect.Float64: 386 | return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true 387 | case reflect.Bool: 388 | return strconv.AppendBool(buf, rv.Bool()), true 389 | case reflect.String: 390 | s := rv.String() 391 | return append(buf, s...), true 392 | } 393 | return 394 | } 395 | 396 | type decimalDecompose interface { 397 | // Decompose returns the internal decimal state in parts. 398 | // If the provided buf has sufficient capacity, buf may be returned as the coefficient with 399 | // the value set and length set as appropriate. 400 | Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) 401 | } 402 | 403 | type decimalCompose interface { 404 | // Compose sets the internal decimal value from parts. If the value cannot be 405 | // represented then an error should be returned. 406 | Compose(form byte, negative bool, coefficient []byte, exponent int32) error 407 | } 408 | 409 | // cloneBytes returns a copy of b[:len(b)]. 410 | // The result may have additional unused capacity. 411 | // cloneBytes(nil) returns nil. 412 | func cloneBytes(b []byte) []byte { 413 | if b == nil { 414 | return nil 415 | } 416 | return append([]byte{}, b...) 417 | } 418 | -------------------------------------------------------------------------------- /null_test.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql/driver" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestNull_Scan(t *testing.T) { 10 | type args struct { 11 | value any 12 | } 13 | type testCase[T any] struct { 14 | name string 15 | n Null[T] 16 | args args 17 | wantErr bool 18 | } 19 | tests := []testCase[int64]{ 20 | { 21 | name: "test", 22 | n: Null[int64]{}, 23 | args: args{value: "test"}, 24 | wantErr: true, 25 | }, { 26 | name: "test2", 27 | n: Null[int64]{}, 28 | args: args{value: "6"}, 29 | wantErr: false, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if err := tt.n.Scan(tt.args.value); (err != nil) != tt.wantErr { 35 | t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestNull_Value(t *testing.T) { 42 | type testCase[T any] struct { 43 | name string 44 | n Null[T] 45 | want driver.Value 46 | wantErr bool 47 | } 48 | var ( 49 | v1 int64 = 1 50 | v2 int64 = 2 51 | ) 52 | tests := []testCase[int64]{ 53 | { 54 | name: "test", 55 | n: Null[int64]{V: v1, Valid: true}, 56 | want: v1, 57 | wantErr: false, 58 | }, { 59 | name: "test", 60 | n: Null[int64]{V: v2, Valid: false}, 61 | want: nil, 62 | wantErr: false, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | got, err := tt.n.Value() 68 | if (err != nil) != tt.wantErr { 69 | t.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if !reflect.DeepEqual(got, tt.want) { 73 | t.Errorf("Value() got = %v, want %v", got, tt.want) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestNullInt64_Value(t *testing.T) { 80 | type testCase[T any] struct { 81 | name string 82 | n NullInt64 83 | want driver.Value 84 | wantErr bool 85 | } 86 | var ( 87 | v1 int64 = 1 88 | v2 int64 = 2 89 | ) 90 | tests := []testCase[int64]{ 91 | { 92 | name: "test", 93 | n: NullInt64{V: v1, Valid: true}, 94 | want: v1, 95 | wantErr: false, 96 | }, { 97 | name: "test", 98 | n: NullInt64{V: v2, Valid: false}, 99 | want: nil, 100 | wantErr: false, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | got, err := tt.n.Value() 106 | if (err != nil) != tt.wantErr { 107 | t.Errorf("Int64_Value() error = %v, wantErr %v", err, tt.wantErr) 108 | return 109 | } 110 | if !reflect.DeepEqual(got, tt.want) { 111 | t.Errorf("Int64_Value() got = %v, want %v", got, tt.want) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | dialects=("postgres" "postgres_simple" "mysql" "sqlserver" "sqlite") 4 | 5 | for dialect in "${dialects[@]}" ; do 6 | if [ "$GORM_DIALECT" = "" ] || [ "$GORM_DIALECT" = "${dialect}" ] 7 | then 8 | GORM_DIALECT=${dialect} go test --tags "json1" 9 | fi 10 | done 11 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/schema" 13 | ) 14 | 15 | // Time is time data type. 16 | type Time time.Duration 17 | 18 | // NewTime is a constructor for Time and returns new Time. 19 | func NewTime(hour, min, sec, nsec int) Time { 20 | return newTime(hour, min, sec, nsec) 21 | } 22 | 23 | func newTime(hour, min, sec, nsec int) Time { 24 | return Time( 25 | time.Duration(hour)*time.Hour + 26 | time.Duration(min)*time.Minute + 27 | time.Duration(sec)*time.Second + 28 | time.Duration(nsec)*time.Nanosecond, 29 | ) 30 | } 31 | 32 | // GormDataType returns gorm common data type. This type is used for the field's column type. 33 | func (Time) GormDataType() string { 34 | return "time" 35 | } 36 | 37 | // GormDBDataType returns gorm DB data type based on the current using database. 38 | func (Time) GormDBDataType(db *gorm.DB, field *schema.Field) string { 39 | switch db.Dialector.Name() { 40 | case "mysql": 41 | return "TIME" 42 | case "postgres": 43 | return "TIME" 44 | case "sqlserver": 45 | return "TIME" 46 | case "sqlite": 47 | return "TEXT" 48 | default: 49 | return "" 50 | } 51 | } 52 | 53 | // Scan implements sql.Scanner interface and scans value into Time, 54 | func (t *Time) Scan(src interface{}) error { 55 | switch v := src.(type) { 56 | case []byte: 57 | t.setFromString(string(v)) 58 | case string: 59 | t.setFromString(v) 60 | case time.Time: 61 | t.setFromTime(v) 62 | default: 63 | return errors.New(fmt.Sprintf("failed to scan value: %v", v)) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (t *Time) setFromString(str string) { 70 | var h, m, s, n int 71 | fmt.Sscanf(str, "%02d:%02d:%02d.%09d", &h, &m, &s, &n) 72 | *t = newTime(h, m, s, n) 73 | } 74 | 75 | func (t *Time) setFromTime(src time.Time) { 76 | *t = newTime(src.Hour(), src.Minute(), src.Second(), src.Nanosecond()) 77 | } 78 | 79 | // Value implements driver.Valuer interface and returns string format of Time. 80 | func (t Time) Value() (driver.Value, error) { 81 | return t.String(), nil 82 | } 83 | 84 | // String implements fmt.Stringer interface. 85 | func (t Time) String() string { 86 | if nsec := t.nanoseconds(); nsec > 0 { 87 | return fmt.Sprintf("%02d:%02d:%02d.%09d", t.hours(), t.minutes(), t.seconds(), nsec) 88 | } else { 89 | // omit nanoseconds unless any value is specified 90 | return fmt.Sprintf("%02d:%02d:%02d", t.hours(), t.minutes(), t.seconds()) 91 | } 92 | } 93 | 94 | func (t Time) hours() int { 95 | return int(time.Duration(t).Truncate(time.Hour).Hours()) 96 | } 97 | 98 | func (t Time) minutes() int { 99 | return int((time.Duration(t) % time.Hour).Truncate(time.Minute).Minutes()) 100 | } 101 | 102 | func (t Time) seconds() int { 103 | return int((time.Duration(t) % time.Minute).Truncate(time.Second).Seconds()) 104 | } 105 | 106 | func (t Time) nanoseconds() int { 107 | return int((time.Duration(t) % time.Second).Nanoseconds()) 108 | } 109 | 110 | // MarshalJSON implements json.Marshaler to convert Time to json serialization. 111 | func (t Time) MarshalJSON() ([]byte, error) { 112 | return json.Marshal(t.String()) 113 | } 114 | 115 | // UnmarshalJSON implements json.Unmarshaler to deserialize json data. 116 | func (t *Time) UnmarshalJSON(data []byte) error { 117 | // ignore null 118 | if string(data) == "null" { 119 | return nil 120 | } 121 | t.setFromString(strings.Trim(string(data), `"`)) 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "gorm.io/datatypes" 7 | . "gorm.io/gorm/utils/tests" 8 | ) 9 | 10 | func TestTime(t *testing.T) { 11 | if SupportedDriver("mysql", "postgres", "sqlite", "sqlserver") { 12 | type UserWithTime struct { 13 | ID uint 14 | Name string 15 | Time datatypes.Time 16 | } 17 | 18 | DB.Migrator().DropTable(&UserWithTime{}) 19 | if err := DB.Migrator().AutoMigrate(&UserWithTime{}); err != nil { 20 | t.Fatalf("failed to migrate, got error: %v", err) 21 | } 22 | 23 | user := UserWithTime{Name: "user1", Time: datatypes.NewTime(1, 2, 3, 0)} 24 | DB.Create(&user) 25 | 26 | result := UserWithTime{} 27 | if err := DB.First(&result, "name = ? AND time = ?", "user1", datatypes.NewTime(1, 2, 3, 0)).Error; err != nil { 28 | t.Fatalf("failed to find record with time, got error: %v", err) 29 | } 30 | 31 | AssertEqual(t, result.Time, datatypes.NewTime(1, 2, 3, 0)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/schema" 13 | ) 14 | 15 | type URL url.URL 16 | 17 | func (u URL) Value() (driver.Value, error) { 18 | return u.String(), nil 19 | } 20 | 21 | func (u *URL) Scan(value interface{}) error { 22 | var us string 23 | switch v := value.(type) { 24 | case []byte: 25 | us = string(v) 26 | case string: 27 | us = v 28 | default: 29 | return errors.New(fmt.Sprint("Failed to parse URL:", value)) 30 | } 31 | uu, err := url.Parse(us) 32 | if err != nil { 33 | return err 34 | } 35 | *u = URL(*uu) 36 | return nil 37 | } 38 | 39 | func (URL) GormDataType() string { 40 | return "url" 41 | } 42 | 43 | func (URL) GormDBDataType(db *gorm.DB, field *schema.Field) string { 44 | return "TEXT" 45 | } 46 | 47 | func (u *URL) String() string { 48 | return (*url.URL)(u).String() 49 | } 50 | 51 | func (u URL) MarshalJSON() ([]byte, error) { 52 | return json.Marshal(u.String()) 53 | } 54 | 55 | func (u *URL) UnmarshalJSON(data []byte) error { 56 | // ignore null 57 | if string(data) == "null" { 58 | return nil 59 | } 60 | uu, err := url.Parse(strings.Trim(string(data), `"'`)) 61 | if err != nil { 62 | return err 63 | } 64 | *u = URL(*uu) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "gorm.io/datatypes" 8 | . "gorm.io/gorm/utils/tests" 9 | ) 10 | 11 | func TestURL(t *testing.T) { 12 | type StructWithURL struct { 13 | ID uint 14 | FileName string 15 | Storage datatypes.URL 16 | } 17 | _ = DB.Migrator().DropTable(&StructWithURL{}) 18 | if err := DB.Migrator().AutoMigrate(&StructWithURL{}); err != nil { 19 | t.Fatalf("failed to migrate, got error: %v", err) 20 | } 21 | f1 := StructWithURL{ 22 | FileName: "FLocal1", 23 | Storage: datatypes.URL{ 24 | Scheme: "file", 25 | Path: "/tmp/f1", 26 | }, 27 | } 28 | us := "sftp://user:pwd@127.0.0.1/f2?query=1#frag" 29 | u2, _ := url.Parse(us) 30 | f2 := StructWithURL{ 31 | FileName: "FRemote2", 32 | Storage: datatypes.URL(*u2), 33 | } 34 | 35 | uf1 := url.URL(f1.Storage) 36 | uf2 := url.URL(f2.Storage) 37 | DB.Create(&f1) 38 | DB.Create(&f2) 39 | 40 | result := StructWithURL{} 41 | if err := DB.First( 42 | &result, "file_name = ? AND storage LIKE ?", 43 | "FLocal1", 44 | datatypes.URL{ 45 | Scheme: "file", 46 | Path: "/tmp/f1", 47 | }).Error; err != nil { 48 | t.Fatalf("failed to find record with url, got error: %v", err) 49 | } 50 | AssertEqual(t, uf1.String(), result.Storage.String()) 51 | 52 | result = StructWithURL{} 53 | if err := DB.First( 54 | &result, "file_name = ? AND storage LIKE ?", 55 | "FRemote2", 56 | datatypes.URL{ 57 | Scheme: "sftp", 58 | User: url.UserPassword("user", "pwd"), 59 | Host: "127.0.0.1", 60 | Path: "/f2", 61 | RawPath: "should not affects", 62 | RawQuery: "query=1", 63 | Fragment: "frag", 64 | RawFragment: "should not affects", 65 | }).Error; err != nil { 66 | t.Fatalf("failed to find record with url, got error: %v", err) 67 | } 68 | AssertEqual(t, u2.String(), uf2.String()) 69 | AssertEqual(t, uf2.String(), result.Storage.String()) 70 | AssertEqual(t, us, result.Storage.String()) 71 | 72 | result = StructWithURL{} 73 | if err := DB.First( 74 | &result, "file_name = ? AND storage LIKE ?", 75 | "FRemote2", 76 | datatypes.URL{ 77 | Scheme: "sftp", 78 | Opaque: "//user:pwd@127.0.0.1/f2", 79 | RawQuery: "query=1", 80 | Fragment: "frag", 81 | }).Error; err != nil { 82 | t.Fatalf("failed to find record with url, got error: %v", err) 83 | } 84 | AssertEqual(t, us, result.Storage.String()) 85 | 86 | result = StructWithURL{} 87 | if err := DB.First( 88 | &result, "file_name = ? AND storage LIKE ?", 89 | "FRemote2", 90 | datatypes.URL{ 91 | Scheme: "sftp", 92 | User: url.User("user"), 93 | Host: "127.0.0.1", 94 | Path: "/f2", 95 | RawQuery: "query=1", 96 | Fragment: "frag", 97 | }).Error; err == nil { 98 | t.Fatalf("record couldn't have been identical: %v vs %v", result.Storage, us) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package datatypes 2 | 3 | import ( 4 | "database/sql/driver" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/schema" 9 | ) 10 | 11 | // This datatype stores the uuid in the database as a string. To store the uuid 12 | // in the database as a binary (byte) array, please refer to datatypes.BinUUID. 13 | type UUID uuid.UUID 14 | 15 | // NewUUIDv1 generates a UUID version 1, panics on generation failure. 16 | func NewUUIDv1() UUID { 17 | return UUID(uuid.Must(uuid.NewUUID())) 18 | } 19 | 20 | // NewUUIDv4 generates a UUID version 4, panics on generation failure. 21 | func NewUUIDv4() UUID { 22 | return UUID(uuid.Must(uuid.NewRandom())) 23 | } 24 | 25 | // GormDataType gorm common data type. 26 | func (UUID) GormDataType() string { 27 | return "string" 28 | } 29 | 30 | // GormDBDataType gorm db data type. 31 | func (UUID) GormDBDataType(db *gorm.DB, field *schema.Field) string { 32 | switch db.Dialector.Name() { 33 | case "mysql": 34 | return "LONGTEXT" 35 | case "postgres": 36 | return "UUID" 37 | case "sqlserver": 38 | return "NVARCHAR(128)" 39 | case "sqlite": 40 | return "TEXT" 41 | default: 42 | return "" 43 | } 44 | } 45 | 46 | // Scan is the scanner function for this datatype. 47 | func (u *UUID) Scan(value interface{}) error { 48 | var result uuid.UUID 49 | if err := result.Scan(value); err != nil { 50 | return err 51 | } 52 | *u = UUID(result) 53 | return nil 54 | } 55 | 56 | // Value is the valuer function for this datatype. 57 | func (u UUID) Value() (driver.Value, error) { 58 | return uuid.UUID(u).Value() 59 | } 60 | 61 | // String returns the string form of the UUID. 62 | func (u UUID) String() string { 63 | return uuid.UUID(u).String() 64 | } 65 | 66 | // Equals returns true if string form of UUID matches other, false otherwise. 67 | func (u UUID) Equals(other UUID) bool { 68 | return u.String() == other.String() 69 | } 70 | 71 | // Length returns the number of characters in string form of UUID. 72 | func (u UUID) Length() int { 73 | return len(u.String()) 74 | } 75 | 76 | // IsNil returns true if the UUID is a nil UUID (all zeroes), false otherwise. 77 | func (u UUID) IsNil() bool { 78 | return uuid.UUID(u) == uuid.Nil 79 | } 80 | 81 | // IsEmpty returns true if UUID is nil UUID or of zero length, false otherwise. 82 | func (u UUID) IsEmpty() bool { 83 | return u.IsNil() || u.Length() == 0 84 | } 85 | 86 | // IsNilPtr returns true if caller UUID ptr is nil, false otherwise. 87 | func (u *UUID) IsNilPtr() bool { 88 | return u == nil 89 | } 90 | 91 | // IsEmptyPtr returns true if caller UUID ptr is nil or it's value is empty. 92 | func (u *UUID) IsEmptyPtr() bool { 93 | return u.IsNilPtr() || u.IsEmpty() 94 | } 95 | -------------------------------------------------------------------------------- /uuid_test.go: -------------------------------------------------------------------------------- 1 | package datatypes_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/datatypes" 9 | "gorm.io/gorm" 10 | . "gorm.io/gorm/utils/tests" 11 | ) 12 | 13 | var _ driver.Valuer = &datatypes.UUID{} 14 | 15 | func TestUUID(t *testing.T) { 16 | if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { 17 | type UserWithUUID struct { 18 | gorm.Model 19 | Name string 20 | UserUUID datatypes.UUID 21 | } 22 | 23 | DB.Migrator().DropTable(&UserWithUUID{}) 24 | if err := DB.Migrator().AutoMigrate(&UserWithUUID{}); err != nil { 25 | t.Errorf("failed to migrate, got error: %v", err) 26 | } 27 | 28 | users := []UserWithUUID{{ 29 | Name: "uuid-1", 30 | UserUUID: datatypes.NewUUIDv1(), 31 | }, { 32 | Name: "uuid-2", 33 | UserUUID: datatypes.NewUUIDv1(), 34 | }, { 35 | Name: "uuid-3", 36 | UserUUID: datatypes.NewUUIDv4(), 37 | }, { 38 | Name: "uuid-4", 39 | UserUUID: datatypes.NewUUIDv4(), 40 | }} 41 | 42 | if err := DB.Create(&users).Error; err != nil { 43 | t.Errorf("Failed to create users %v", err) 44 | } 45 | 46 | for _, user := range users { 47 | result := UserWithUUID{} 48 | if err := DB.First( 49 | &result, "name = ? AND user_uuid = ?", 50 | user.Name, 51 | user.UserUUID, 52 | ).Error; err != nil { 53 | t.Fatalf("failed to find user with uuid, got error: %v", err) 54 | } 55 | AssertEqual(t, !result.UserUUID.IsEmpty(), true) 56 | AssertEqual(t, user.UserUUID.Equals(result.UserUUID), true) 57 | valueUser, err := user.UserUUID.Value() 58 | if err != nil { 59 | t.Fatalf("failed to get user value, got error: %v", err) 60 | } 61 | valueResult, err := result.UserUUID.Value() 62 | if err != nil { 63 | t.Fatalf("failed to get result value, got error: %v", err) 64 | } 65 | AssertEqual(t, valueUser, valueResult) 66 | AssertEqual(t, user.UserUUID.Length(), 36) 67 | } 68 | 69 | var tx *gorm.DB 70 | user1 := users[0] 71 | AssertEqual(t, user1.UserUUID.IsNil(), false) 72 | AssertEqual(t, user1.UserUUID.IsEmpty(), false) 73 | tx = DB.Model(&user1).Updates( 74 | map[string]interface{}{"user_uuid": uuid.Nil}, 75 | ) 76 | AssertEqual(t, tx.Error, nil) 77 | AssertEqual(t, user1.UserUUID.IsNil(), true) 78 | AssertEqual(t, user1.UserUUID.IsEmpty(), true) 79 | user1NewUUID := datatypes.NewUUIDv4() 80 | tx = DB.Model(&user1).Updates( 81 | map[string]interface{}{ 82 | "user_uuid": user1NewUUID, 83 | }, 84 | ) 85 | AssertEqual(t, tx.Error, nil) 86 | AssertEqual(t, user1.UserUUID, user1NewUUID) 87 | 88 | user2 := users[1] 89 | AssertEqual(t, user2.UserUUID.IsNil(), false) 90 | AssertEqual(t, user2.UserUUID.IsEmpty(), false) 91 | tx = DB.Model(&user2).Updates( 92 | map[string]interface{}{"user_uuid": nil}, 93 | ) 94 | AssertEqual(t, tx.Error, nil) 95 | AssertEqual(t, user2.UserUUID.IsNil(), true) 96 | AssertEqual(t, user2.UserUUID.IsEmpty(), true) 97 | user2NewUUID := datatypes.NewUUIDv4() 98 | tx = DB.Model(&user2).Updates( 99 | map[string]interface{}{ 100 | "user_uuid": user2NewUUID, 101 | }, 102 | ) 103 | AssertEqual(t, tx.Error, nil) 104 | AssertEqual(t, user2.UserUUID, user2NewUUID) 105 | } 106 | } 107 | 108 | func TestUUIDPtr(t *testing.T) { 109 | if SupportedDriver("sqlite", "mysql", "postgres", "sqlserver") { 110 | type UserWithUUIDPtr struct { 111 | gorm.Model 112 | Name string 113 | UserUUID *datatypes.UUID 114 | } 115 | 116 | DB.Migrator().DropTable(&UserWithUUIDPtr{}) 117 | if err := DB.Migrator().AutoMigrate(&UserWithUUIDPtr{}); err != nil { 118 | t.Errorf("failed to migrate, got error: %v", err) 119 | } 120 | 121 | uuid1 := datatypes.NewUUIDv1() 122 | uuid2 := datatypes.NewUUIDv1() 123 | uuid3 := datatypes.NewUUIDv4() 124 | uuid4 := datatypes.NewUUIDv4() 125 | 126 | users := []UserWithUUIDPtr{{ 127 | Name: "uuid-1", 128 | UserUUID: &uuid1, 129 | }, { 130 | Name: "uuid-2", 131 | UserUUID: &uuid2, 132 | }, { 133 | Name: "uuid-3", 134 | UserUUID: &uuid3, 135 | }, { 136 | Name: "uuid-4", 137 | UserUUID: &uuid4, 138 | }} 139 | 140 | if err := DB.Create(&users).Error; err != nil { 141 | t.Errorf("Failed to create users %v", err) 142 | } 143 | 144 | for _, user := range users { 145 | result := UserWithUUIDPtr{} 146 | if err := DB.First( 147 | &result, "name = ? AND user_uuid = ?", 148 | user.Name, 149 | *user.UserUUID, 150 | ).Error; err != nil { 151 | t.Fatalf("failed to find user with uuid, got error: %v", err) 152 | } 153 | AssertEqual(t, !result.UserUUID.IsEmpty(), true) 154 | AssertEqual(t, user.UserUUID, result.UserUUID) 155 | valueUser, err := user.UserUUID.Value() 156 | if err != nil { 157 | t.Fatalf("failed to get user value, got error: %v", err) 158 | } 159 | valueResult, err := result.UserUUID.Value() 160 | if err != nil { 161 | t.Fatalf("failed to get result value, got error: %v", err) 162 | } 163 | AssertEqual(t, valueUser, valueResult) 164 | AssertEqual(t, user.UserUUID.Length(), 36) 165 | } 166 | 167 | user1 := users[0] 168 | AssertEqual(t, user1.UserUUID.IsNilPtr(), false) 169 | AssertEqual(t, user1.UserUUID.IsEmptyPtr(), false) 170 | tx := DB.Model(&user1).Updates(map[string]interface{}{"user_uuid": nil}) 171 | AssertEqual(t, tx.Error, nil) 172 | AssertEqual(t, user1.UserUUID.IsNilPtr(), true) 173 | AssertEqual(t, user1.UserUUID.IsEmptyPtr(), true) 174 | } 175 | } 176 | --------------------------------------------------------------------------------