From aff647dd37999db93ea43435556cbc4acdfc7f8d Mon Sep 17 00:00:00 2001 From: Antonio Pagano <645522+paganotoni@users.noreply.github.com> Date: Sat, 14 May 2022 21:26:16 -0500 Subject: [PATCH] Latest Development (#716) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Latest from master (#620) * Latest from development (#617) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * adding goreleaser syntaz (#619) Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Resolve issues in UPDATE and DELETE when using schemas (#618) * Resolve MySQL issues and improve test migrations * Bump CockroachDB to maintained and supported versions Version 2.1 has reached EoL in 2019 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use `PaginatorPageKey` and `PaginatorPerPageKey` variables (#615) * update pagination_test * Pass Time structure into timestamp update functions. (#625) Closes #624 * Allow nullable JSONB and resolve MySQL regression (#639) * Allow passing args to `Order` (#630) * Added connection maximum idle time configuration (#635) This PR add the possibility to configure the connection maximum idle time (https://golang.org/pkg/database/sql/#DB.SetConnMaxIdleTime). Closes #632 BREAKING CHANGE: Requires Go 1.15 from now on. * Bump sqlite to 3.35.4 / 1.14.7 (#642) * Update pg, pgx, sqlx (#643) - `jackc/pgx` to version `v4.11.0`. - `jmoiron/sqlx` to version`v1.3.3` - `lib/pq` to version`v1.10.1` * Fix Inner has many associations when passing on multiple arguments (#633) * Fix Inner has many associations when passing on multiple arguments for inner fields * Fix broken tests * clean up extractFieldAndInnerFields function Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Remove many to many TX condition for EagerPreload (#645) * Remove the need to use Tx when loading many to many associations * replace TX access to create a new tx.Store.Transaction() object * Added fix/tests for has_many with pointer foreign key (#647) Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Export WhereID, Alias, WhereNamedID (#637) This patch export some model convenience functions which are useful when constructing queries outside of pop: custom updates, deletes, inserts, ... Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * fix: log model values everywhere (#656) Some SQL logs were missing the values as argument. This adds all places * Add delete to query builder (#658) This allows writing delete queries without knowing the exact primary key or for composite keys. `Destroy` only allows to delete by primary key, but there are many cases where you want to delete multiple rows or by some other query than the ID. See #29 * Sort down migrations (#657) Basically, just reversing the up migration order does not work, as that puts "all" migrations before specific ones. Therefore, I added implemented the proper `Less` function for down migrations explicitly. Related #533 Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> * Preserve eager information when validating models (#664) (#665) Co-authored-by: Karl Haas * Migrate from packr to fs (#667) * Updating Pgx (#660) * adding goreleaser syntaz * updating pgx now really * Migrate from packr to fs * Migrate to v6 * Use old build tags * Update error handling * Fix error after rebase * Fix error handling * Fix filenames for embed Go 1.16 usage Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Task merging master (#669) * v5.3.4 (#644) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Latest from master (#620) * Latest from development (#617) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * adding goreleaser syntaz (#619) Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Resolve issues in UPDATE and DELETE when using schemas (#618) * Resolve MySQL issues and improve test migrations * Bump CockroachDB to maintained and supported versions Version 2.1 has reached EoL in 2019 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use `PaginatorPageKey` and `PaginatorPerPageKey` variables (#615) * update pagination_test * Pass Time structure into timestamp update functions. (#625) Closes #624 * Allow nullable JSONB and resolve MySQL regression (#639) * Allow passing args to `Order` (#630) * Added connection maximum idle time configuration (#635) This PR add the possibility to configure the connection maximum idle time (https://golang.org/pkg/database/sql/#DB.SetConnMaxIdleTime). Closes #632 BREAKING CHANGE: Requires Go 1.15 from now on. * Bump sqlite to 3.35.4 / 1.14.7 (#642) * Update pg, pgx, sqlx (#643) - `jackc/pgx` to version `v4.11.0`. - `jmoiron/sqlx` to version`v1.3.3` - `lib/pq` to version`v1.10.1` * Fix Inner has many associations when passing on multiple arguments (#633) * Fix Inner has many associations when passing on multiple arguments for inner fields * Fix broken tests * clean up extractFieldAndInnerFields function Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Remove many to many TX condition for EagerPreload (#645) * Remove the need to use Tx when loading many to many associations * replace TX access to create a new tx.Store.Transaction() object * Added fix/tests for has_many with pointer foreign key (#647) Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin * Updating Pgx (#660) * adding goreleaser syntaz * updating pgx now really * tidying Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin * Replace removed command The command has been removed from the CLI. This patch introduces a new mechanism to reliably dump the SQL schema for CockroachDB. * Resolve `EagerPreload` panic caused for pointer references Resolves a panic where `EagerPreload` tried to set `reflect.Struct` for a `reflect.Pointer` on 1.. associations. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve `EagerPreload` panic caused by NullUUID Resolves a panic where `EagerPreload` was trying to set UUID into NullUUID. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Support pointers in n+1 `Eager` loading Resolves an issue where n+1 eager associations would error with a double pointer in `associations.ForStruct`. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Improve error message of associations.ForStruct Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Add test cases for `IsZeroOfUnderlyingType` Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve an obscure bug where empty structs got loaded for NULL foreign keys Closes https://github.com/gobuffalo/pop/issues/139 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve association regression in finders.go Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use dedicated migrations for preloading regression test Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix code regression Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix test code regressions Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix sql migration order Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve order issue in test * Ignore order when testing for nil values in test * Pass Context during exec in create. (#688) * feat: support embedded struct fields (#691) * test: use `T.TempDir` to create temporary test directory This commit replaces `ioutil.TempDir` with `t.TempDir` in tests. The directory created by `t.TempDir` is automatically removed when the test and all its subtests complete. Prior to this commit, temporary directory created using `ioutil.TempDir` needs to be removed manually by calling `os.RemoveAll`, which is omitted in some tests. The error handling boilerplate e.g. defer func() { if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } } is also tedious, but `t.TempDir` handles this for us nicely. Reference: https://pkg.go.dev/testing#T.TempDir Signed-off-by: Eng Zer Jun * task: adding next version number * fix: associations for embedded fields * test: add other fields to duplicate type, because of some kind of query builder cache * feat: implement UpdateQuery This commit introduces a new function, UpdateQuery, that enables updating all rows matched by a query. It can be used for conditional updates. * feat: allow customizing the time used for CreatedAt/UpdatedAt * feat: allow using SQLite without built tag via include (#662) * preparing go1.18 support (package specific workflow) * feat: darwin arm64 binary (#690) * feat: arm64 binary * v6.0.2 (#704) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Latest from master (#620) * Latest from development (#617) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * adding goreleaser syntaz (#619) Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Resolve issues in UPDATE and DELETE when using schemas (#618) * Resolve MySQL issues and improve test migrations * Bump CockroachDB to maintained and supported versions Version 2.1 has reached EoL in 2019 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use `PaginatorPageKey` and `PaginatorPerPageKey` variables (#615) * update pagination_test * Pass Time structure into timestamp update functions. (#625) Closes #624 * Allow nullable JSONB and resolve MySQL regression (#639) * Allow passing args to `Order` (#630) * Added connection maximum idle time configuration (#635) This PR add the possibility to configure the connection maximum idle time (https://golang.org/pkg/database/sql/#DB.SetConnMaxIdleTime). Closes #632 BREAKING CHANGE: Requires Go 1.15 from now on. * Bump sqlite to 3.35.4 / 1.14.7 (#642) * Update pg, pgx, sqlx (#643) - `jackc/pgx` to version `v4.11.0`. - `jmoiron/sqlx` to version`v1.3.3` - `lib/pq` to version`v1.10.1` * Fix Inner has many associations when passing on multiple arguments (#633) * Fix Inner has many associations when passing on multiple arguments for inner fields * Fix broken tests * clean up extractFieldAndInnerFields function Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Remove many to many TX condition for EagerPreload (#645) * Remove the need to use Tx when loading many to many associations * replace TX access to create a new tx.Store.Transaction() object * Added fix/tests for has_many with pointer foreign key (#647) Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Export WhereID, Alias, WhereNamedID (#637) This patch export some model convenience functions which are useful when constructing queries outside of pop: custom updates, deletes, inserts, ... Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * fix: log model values everywhere (#656) Some SQL logs were missing the values as argument. This adds all places * Add delete to query builder (#658) This allows writing delete queries without knowing the exact primary key or for composite keys. `Destroy` only allows to delete by primary key, but there are many cases where you want to delete multiple rows or by some other query than the ID. See #29 * Sort down migrations (#657) Basically, just reversing the up migration order does not work, as that puts "all" migrations before specific ones. Therefore, I added implemented the proper `Less` function for down migrations explicitly. Related #533 Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> * Preserve eager information when validating models (#664) (#665) Co-authored-by: Karl Haas * Migrate from packr to fs (#667) * Updating Pgx (#660) * adding goreleaser syntaz * updating pgx now really * Migrate from packr to fs * Migrate to v6 * Use old build tags * Update error handling * Fix error after rebase * Fix error handling * Fix filenames for embed Go 1.16 usage Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Task merging master (#669) * v5.3.4 (#644) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Latest from master (#620) * Latest from development (#617) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * adding goreleaser syntaz (#619) Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan * Resolve issues in UPDATE and DELETE when using schemas (#618) * Resolve MySQL issues and improve test migrations * Bump CockroachDB to maintained and supported versions Version 2.1 has reached EoL in 2019 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use `PaginatorPageKey` and `PaginatorPerPageKey` variables (#615) * update pagination_test * Pass Time structure into timestamp update functions. (#625) Closes #624 * Allow nullable JSONB and resolve MySQL regression (#639) * Allow passing args to `Order` (#630) * Added connection maximum idle time configuration (#635) This PR add the possibility to configure the connection maximum idle time (https://golang.org/pkg/database/sql/#DB.SetConnMaxIdleTime). Closes #632 BREAKING CHANGE: Requires Go 1.15 from now on. * Bump sqlite to 3.35.4 / 1.14.7 (#642) * Update pg, pgx, sqlx (#643) - `jackc/pgx` to version `v4.11.0`. - `jmoiron/sqlx` to version`v1.3.3` - `lib/pq` to version`v1.10.1` * Fix Inner has many associations when passing on multiple arguments (#633) * Fix Inner has many associations when passing on multiple arguments for inner fields * Fix broken tests * clean up extractFieldAndInnerFields function Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> * Remove many to many TX condition for EagerPreload (#645) * Remove the need to use Tx when loading many to many associations * replace TX access to create a new tx.Store.Transaction() object * Added fix/tests for has_many with pointer foreign key (#647) Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin * Updating Pgx (#660) * adding goreleaser syntaz * updating pgx now really * tidying Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin * Replace removed command The command has been removed from the CLI. This patch introduces a new mechanism to reliably dump the SQL schema for CockroachDB. * Resolve `EagerPreload` panic caused for pointer references Resolves a panic where `EagerPreload` tried to set `reflect.Struct` for a `reflect.Pointer` on 1.. associations. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve `EagerPreload` panic caused by NullUUID Resolves a panic where `EagerPreload` was trying to set UUID into NullUUID. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Support pointers in n+1 `Eager` loading Resolves an issue where n+1 eager associations would error with a double pointer in `associations.ForStruct`. Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Improve error message of associations.ForStruct Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Add test cases for `IsZeroOfUnderlyingType` Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve an obscure bug where empty structs got loaded for NULL foreign keys Closes https://github.com/gobuffalo/pop/issues/139 Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve association regression in finders.go Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Use dedicated migrations for preloading regression test Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix code regression Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix test code regressions Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Fix sql migration order Signed-off-by: aeneasr <3372410+aeneasr@users.noreply.github.com> * Resolve order issue in test * Ignore order when testing for nil values in test * Pass Context during exec in create. (#688) * feat: support embedded struct fields (#691) * test: use `T.TempDir` to create temporary test directory This commit replaces `ioutil.TempDir` with `t.TempDir` in tests. The directory created by `t.TempDir` is automatically removed when the test and all its subtests complete. Prior to this commit, temporary directory created using `ioutil.TempDir` needs to be removed manually by calling `os.RemoveAll`, which is omitted in some tests. The error handling boilerplate e.g. defer func() { if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } } is also tedious, but `t.TempDir` handles this for us nicely. Reference: https://pkg.go.dev/testing#T.TempDir Signed-off-by: Eng Zer Jun * task: adding next version number Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin Co-authored-by: karlhaas Co-authored-by: Karl Haas Co-authored-by: Matthias Fasching Co-authored-by: Martin Eigenbrodt Co-authored-by: Eng Zer Jun * play time * chore: updates * chore: reset formatting Co-authored-by: Antonio Pagano <645522+paganotoni@users.noreply.github.com> Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin Co-authored-by: karlhaas Co-authored-by: Karl Haas Co-authored-by: Matthias Fasching Co-authored-by: Martin Eigenbrodt Co-authored-by: Eng Zer Jun * task: adding release-env in the gitignore * task: its release-env * task: adding .release-env to the release-dry-run make * task: changing the order of release steps * task:changing the token Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Brian Buchholz <4773480+bhb603@users.noreply.github.com> Co-authored-by: Mike Pontillo Co-authored-by: Benjamin Blattberg Co-authored-by: Jonathan Duck Co-authored-by: Arthur Knoepflin Co-authored-by: karlhaas Co-authored-by: Karl Haas Co-authored-by: Matthias Fasching Co-authored-by: Martin Eigenbrodt Co-authored-by: Eng Zer Jun Co-authored-by: Grant Zvolsky Co-authored-by: Andrew Hobson Co-authored-by: Yonghwan SO Co-authored-by: Joey Freeland <30938344+jfreeland@users.noreply.github.com> --- .github/workflows/release.yml | 21 +- .github/workflows/tests.yml | 5 + .gitignore | 1 + .goreleaser.yml | 22 +- Makefile | 42 +++- associations/associations_for_struct.go | 27 ++- dialect.go | 1 + dialect_cockroach.go | 4 + dialect_common.go | 27 +++ dialect_mysql.go | 9 + dialect_postgresql.go | 4 + dialect_sqlite.go | 44 +++- dialect_sqlite_shim.go | 23 -- dialect_sqlite_tag.go | 8 + docker-compose.yml | 4 +- executors.go | 29 +++ executors_test.go | 277 ++++++++++++++++++++++++ model.go | 5 + pop_test.go | 2 +- 19 files changed, 505 insertions(+), 50 deletions(-) delete mode 100644 dialect_sqlite_shim.go create mode 100644 dialect_sqlite_tag.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4eb7821c..d9ca426f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,22 +9,21 @@ jobs: name: Release runs-on: ubuntu-latest steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Go 1.17 uses: actions/setup-go@v3 with: go-version: 1.17 - id: go - - - name: Checkout Code - uses: actions/checkout@v3 - name: Fetch tags run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} - with: - version: latest - args: release --rm-dist + - name: setup release environment + run: |- + echo 'GITHUB_TOKEN=${{secrets.GORELEASER_TOKEN }}' > .release-env + + - name: release publish + run: make release + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e98acfe..d3f9c48e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,7 @@ jobs: go-version: - "1.16.x" - "1.17.x" + - "1.18.x" services: mysql: @@ -57,6 +58,7 @@ jobs: go-version: - "1.16.x" - "1.17.x" + - "1.18.x" services: postgres: @@ -102,6 +104,7 @@ jobs: go-version: - "1.16.x" - "1.17.x" + - "1.18.x" steps: - uses: actions/checkout@v3 @@ -145,6 +148,7 @@ jobs: go-version: - "1.16.x" - "1.17.x" + - "1.18.x" steps: - uses: actions/checkout@v3 @@ -185,6 +189,7 @@ jobs: go-version: - "1.16.x" - "1.17.x" + - "1.18.x" os: - "macos-latest" - "windows-latest" diff --git a/.gitignore b/.gitignore index d587321e..74d9ca99 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ testdata/migrations/schema.sql .grifter/ vendor/ .env +.release-env # test data cockroach-data/ diff --git a/.goreleaser.yml b/.goreleaser.yml index f22cfd3f..5588bc8a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,6 @@ # GoReleaser config +--- before: hooks: - go mod tidy @@ -14,17 +15,32 @@ builds: - amd64 env: - CGO_ENABLED=1 - - CC=o64-clang - - CXX=o64-clang++ + - CC=/osxcross/target/bin/o64-clang + - CXX=/osxcross/target/bin/o64-clang++ flags: - -tags - sqlite -- id: pop_linux +- id: pop_darwin_arm64 binary: soda main: soda/main.go + goos: + - darwin + goarch: + - arm64 env: - CGO_ENABLED=1 + - CC=/osxcross/target/bin/oa64-clang + - CXX=/osxcross/target/bin/oa64-clang++ + flags: + - -tags + - sqlite + +- id: pop_linux + binary: soda + main: soda/main.go + env: + - CGO_ENABLED=0 flags: - -tags - sqlite diff --git a/Makefile b/Makefile index 893e46e8..24d4eab5 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +PACKAGE_NAME := github.com/gobuffalo/pop +GOLANG_CROSS_VERSION ?= v1.17.6 + TAGS ?= "sqlite" GO_BIN ?= go @@ -37,5 +40,42 @@ endif release-test: ./test.sh +.PHONY: sysroot-pack +sysroot-pack: + @tar cf - $(SYSROOT_DIR) -P | pv -s $[$(du -sk $(SYSROOT_DIR) | awk '{print $1}') * 1024] | pbzip2 > $(SYSROOT_ARCHIVE) + +.PHONY: sysroot-unpack +sysroot-unpack: + @pv $(SYSROOT_ARCHIVE) | pbzip2 -cd | tar -xf - + +.PHONY: release-dry-run +release-dry-run: + @docker run \ + --rm \ + --privileged \ + -e CGO_ENABLED=1 \ + --env-file .release-env \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`:/go/src/$(PACKAGE_NAME) \ + -v `pwd`/sysroot:/sysroot \ + -w /go/src/$(PACKAGE_NAME) \ + goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ + --rm-dist --skip-validate --skip-publish --snapshot + +.PHONY: release release: - release -y -f soda/cmd/version.go + @if [ ! -f ".release-env" ]; then \ + echo "\033[91m.release-env is required for release\033[0m";\ + exit 1;\ + fi + docker run \ + --rm \ + --privileged \ + -e CGO_ENABLED=1 \ + --env-file .release-env \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`:/go/src/$(PACKAGE_NAME) \ + -v `pwd`/sysroot:/sysroot \ + -w /go/src/$(PACKAGE_NAME) \ + goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ + release --rm-dist diff --git a/associations/associations_for_struct.go b/associations/associations_for_struct.go index bdd7e180..529b210b 100644 --- a/associations/associations_for_struct.go +++ b/associations/associations_for_struct.go @@ -27,6 +27,11 @@ var associationBuilders = map[string]associationBuilder{} // it throws an error when it finds a field that does // not exist for a model. func ForStruct(s interface{}, fields ...string) (Associations, error) { + return forStruct(s, s, fields) +} + +// forStruct is a recursive helper that passes the root model down for embedded fields +func forStruct(parent, s interface{}, fields []string) (Associations, error) { t, v := getModelDefinition(s) if t.Kind() != reflect.Struct { return nil, fmt.Errorf("could not get struct associations: not a struct but %T", s) @@ -74,7 +79,20 @@ func ForStruct(s interface{}, fields ...string) (Associations, error) { // inline embedded field if f.Anonymous { - innerAssociations, err := ForStruct(v.Field(i).Interface(), fields...) + field := v.Field(i) + // we need field to be a pointer, so that we can later set the value + // if the embedded field is of type struct {...}, we have to take its address + if field.Kind() != reflect.Ptr { + field = field.Addr() + } + if fieldIsNil(field) { + // initialize zero value + field = reflect.New(field.Type().Elem()) + // we can only get in this case if v.Field(i) is a pointer type because it could not be nil otherwise + // => it is safe to set it here as is + v.Field(i).Set(field) + } + innerAssociations, err := forStruct(parent, field.Interface(), fields) if err != nil { return nil, err } @@ -92,11 +110,12 @@ func ForStruct(s interface{}, fields ...string) (Associations, error) { for name, builder := range associationBuilders { tag := tags.Find(name) if !tag.Empty() { + pt, pv := getModelDefinition(parent) params := associationParams{ field: f, - model: s, - modelType: t, - modelValue: v, + model: parent, + modelType: pt, + modelValue: pv, popTags: tags, innerAssociations: fieldsWithInnerAssociation[f.Name], } diff --git a/dialect.go b/dialect.go index 1b87847d..0e49b168 100644 --- a/dialect.go +++ b/dialect.go @@ -12,6 +12,7 @@ type crudable interface { SelectMany(store, *Model, Query) error Create(store, *Model, columns.Columns) error Update(store, *Model, columns.Columns) error + UpdateQuery(store, *Model, columns.Columns, Query) (int64, error) Destroy(store, *Model) error Delete(store, *Model, Query) error } diff --git a/dialect_cockroach.go b/dialect_cockroach.go index 16d4d96b..5fb7e324 100644 --- a/dialect_cockroach.go +++ b/dialect_cockroach.go @@ -106,6 +106,10 @@ func (p *cockroach) Update(s store, model *Model, cols columns.Columns) error { return genericUpdate(s, model, cols, p) } +func (p *cockroach) UpdateQuery(s store, model *Model, cols columns.Columns, query Query) (int64, error) { + return genericUpdateQuery(s, model, cols, p, query, sqlx.DOLLAR) +} + func (p *cockroach) Destroy(s store, model *Model) error { stmt := p.TranslateSQL(fmt.Sprintf("DELETE FROM %s AS %s WHERE %s", p.Quote(model.TableName()), model.Alias(), model.WhereID())) _, err := genericExec(s, stmt, model.ID()) diff --git a/dialect_common.go b/dialect_common.go index d0876295..5b1d76f4 100644 --- a/dialect_common.go +++ b/dialect_common.go @@ -14,6 +14,7 @@ import ( "github.com/gobuffalo/pop/v6/columns" "github.com/gobuffalo/pop/v6/logging" "github.com/gofrs/uuid" + "github.com/jmoiron/sqlx" ) func init() { @@ -110,6 +111,32 @@ func genericUpdate(s store, model *Model, cols columns.Columns, quoter quotable) return nil } +func genericUpdateQuery(s store, model *Model, cols columns.Columns, quoter quotable, query Query, bindType int) (int64, error) { + q := fmt.Sprintf("UPDATE %s AS %s SET %s", quoter.Quote(model.TableName()), model.Alias(), cols.Writeable().QuotedUpdateString(quoter)) + + q, updateArgs, err := sqlx.Named(q, model.Value) + if err != nil { + return 0, err + } + + sb := query.toSQLBuilder(model) + q = sb.buildWhereClauses(q) + + q = sqlx.Rebind(bindType, q) + + result, err := genericExec(s, q, append(updateArgs, sb.args...)...) + if err != nil { + return 0, err + } + + n, err := result.RowsAffected() + if err != nil { + return 0, err + } + + return n, err +} + func genericDestroy(s store, model *Model, quoter quotable) error { stmt := fmt.Sprintf("DELETE FROM %s AS %s WHERE %s", quoter.Quote(model.TableName()), model.Alias(), model.WhereID()) _, err := genericExec(s, stmt, model.ID()) diff --git a/dialect_mysql.go b/dialect_mysql.go index 1cfda3cf..7b5022b4 100644 --- a/dialect_mysql.go +++ b/dialect_mysql.go @@ -13,6 +13,7 @@ import ( "github.com/gobuffalo/pop/v6/columns" "github.com/gobuffalo/pop/v6/internal/defaults" "github.com/gobuffalo/pop/v6/logging" + "github.com/jmoiron/sqlx" ) const nameMySQL = "mysql" @@ -94,6 +95,14 @@ func (m *mysql) Update(s store, model *Model, cols columns.Columns) error { return nil } +func (m *mysql) UpdateQuery(s store, model *Model, cols columns.Columns, query Query) (int64, error) { + if n, err := genericUpdateQuery(s, model, cols, m, query, sqlx.QUESTION); err != nil { + return n, fmt.Errorf("mysql update query: %w", err) + } else { + return n, nil + } +} + func (m *mysql) Destroy(s store, model *Model) error { stmt := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", m.Quote(model.TableName()), model.IDField()) _, err := genericExec(s, stmt, model.ID()) diff --git a/dialect_postgresql.go b/dialect_postgresql.go index fe4d0236..6ce61569 100644 --- a/dialect_postgresql.go +++ b/dialect_postgresql.go @@ -90,6 +90,10 @@ func (p *postgresql) Update(s store, model *Model, cols columns.Columns) error { return genericUpdate(s, model, cols, p) } +func (p *postgresql) UpdateQuery(s store, model *Model, cols columns.Columns, query Query) (int64, error) { + return genericUpdateQuery(s, model, cols, p, query, sqlx.DOLLAR) +} + func (p *postgresql) Destroy(s store, model *Model) error { stmt := p.TranslateSQL(fmt.Sprintf("DELETE FROM %s AS %s WHERE %s", p.Quote(model.TableName()), model.Alias(), model.WhereID())) _, err := genericExec(s, stmt, model.ID()) diff --git a/dialect_sqlite.go b/dialect_sqlite.go index 1ac8691f..d8400a50 100644 --- a/dialect_sqlite.go +++ b/dialect_sqlite.go @@ -1,9 +1,9 @@ -// +build sqlite - package pop import ( + "database/sql" "database/sql/driver" + "errors" "fmt" "io" "net/url" @@ -19,8 +19,7 @@ import ( "github.com/gobuffalo/pop/v6/columns" "github.com/gobuffalo/pop/v6/internal/defaults" "github.com/gobuffalo/pop/v6/logging" - "github.com/mattn/go-sqlite3" - _ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver + "github.com/jmoiron/sqlx" ) const nameSQLite3 = "sqlite3" @@ -41,6 +40,15 @@ type sqlite struct { smGil *sync.Mutex } +func requireSQLite3() error { + for _, driverName := range sql.Drivers() { + if driverName == nameSQLite3 { + return nil + } + } + return errors.New("sqlite3 support was not compiled into the binary") +} + func (m *sqlite) Name() string { return nameSQLite3 } @@ -109,6 +117,20 @@ func (m *sqlite) Update(s store, model *Model, cols columns.Columns) error { }) } +func (m *sqlite) UpdateQuery(s store, model *Model, cols columns.Columns, query Query) (int64, error) { + rowsAffected := int64(0) + err := m.locker(m.smGil, func() error { + if n, err := genericUpdateQuery(s, model, cols, m, query, sqlx.QUESTION); err != nil { + rowsAffected = n + return fmt.Errorf("sqlite update query: %w", err) + } else { + rowsAffected = n + return nil + } + }) + return rowsAffected, err +} + func (m *sqlite) Destroy(s store, model *Model) error { return m.locker(m.smGil, func() error { if err := genericDestroy(s, model, m); err != nil { @@ -247,6 +269,10 @@ func (m *sqlite) TruncateAll(tx *Connection) error { } func newSQLite(deets *ConnectionDetails) (dialect, error) { + err := requireSQLite3() + if err != nil { + return nil, err + } deets.URL = fmt.Sprintf("sqlite3://%s", deets.Database) cd := &sqlite{ gil: &sync.Mutex{}, @@ -312,5 +338,13 @@ func finalizerSQLite(cd *ConnectionDetails) { } func newSQLiteDriver() (driver.Driver, error) { - return new(sqlite3.SQLiteDriver), nil + err := requireSQLite3() + if err != nil { + return nil, err + } + db, err := sql.Open(nameSQLite3, ":memory:?cache=newSQLiteDriver_temporary") + if err != nil { + return nil, err + } + return db.Driver(), db.Close() } diff --git a/dialect_sqlite_shim.go b/dialect_sqlite_shim.go deleted file mode 100644 index 0c51baa1..00000000 --- a/dialect_sqlite_shim.go +++ /dev/null @@ -1,23 +0,0 @@ -// +build !sqlite - -package pop - -import ( - "database/sql/driver" - "errors" -) - -const nameSQLite3 = "sqlite3" - -func init() { - dialectSynonyms["sqlite"] = nameSQLite3 - newConnection[nameSQLite3] = newSQLite -} - -func newSQLite(deets *ConnectionDetails) (dialect, error) { - return nil, errors.New("sqlite3 support was not compiled into the binary") -} - -func newSQLiteDriver() (driver.Driver, error) { - return nil, errors.New("sqlite3 support was not compiled into the binary") -} diff --git a/dialect_sqlite_tag.go b/dialect_sqlite_tag.go new file mode 100644 index 00000000..286dd783 --- /dev/null +++ b/dialect_sqlite_tag.go @@ -0,0 +1,8 @@ +//go:build sqlite +// +build sqlite + +package pop + +import ( + _ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver +) diff --git a/docker-compose.yml b/docker-compose.yml index 54b90986..2201bec8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: '2.1' services: mysql: @@ -25,7 +25,7 @@ services: - ./sqldumps:/docker-entrypoint-initdb.d cockroach: image: cockroachdb/cockroach:v20.2.4 - user: ${CURRENT_UID:?"Please run as follows 'CURRENT_UID=$(id -u):$(id -g) docker-compose up'"} + user: ${CURRENT_UID:?"Please run as follows 'CURRENT_UID=$$(id -u):$$(id -g) docker-compose up'"} ports: - "26257:26257" volumes: diff --git a/executors.go b/executors.go index 04478f66..37b03b35 100644 --- a/executors.go +++ b/executors.go @@ -380,6 +380,35 @@ func (c *Connection) Update(model interface{}, excludeColumns ...string) error { }) } +// UpdateQuery updates all rows matched by the query. The new values are read +// from the first argument, which must be a struct. The column names to be +// updated must be listed explicitly in subsequent arguments. The ID and +// CreatedAt columns are never updated. The UpdatedAt column is updated +// automatically. +// +// UpdateQuery does not execute (before|after)(Create|Update|Save) callbacks. +// +// Calling UpdateQuery with no columnNames will result in only the UpdatedAt +// column being updated. +func (q *Query) UpdateQuery(model interface{}, columnNames ...string) (int64, error) { + sm := NewModel(model, q.Connection.Context()) + modelKind := reflect.TypeOf(reflect.Indirect(reflect.ValueOf(model))).Kind() + if modelKind != reflect.Struct { + return 0, fmt.Errorf("model must be a struct; got %s", modelKind) + } + + cols := columns.NewColumnsWithAlias(sm.TableName(), sm.As, sm.IDField()) + cols.Add(columnNames...) + if _, err := sm.fieldByName("UpdatedAt"); err == nil { + cols.Add("updated_at") + } + cols.Remove(sm.IDField(), "created_at") + + now := nowFunc().Truncate(time.Microsecond) + sm.setUpdatedAt(now) + return q.Connection.Dialect.UpdateQuery(q.Connection.Store, sm, cols, *q) +} + // UpdateColumns writes changes from an entry to the database, including only the given columns // or all columns if no column names are provided. // It updates the `updated_at` column automatically if you include `updated_at` in columnNames. diff --git a/executors_test.go b/executors_test.go index d428e364..d96f2506 100644 --- a/executors_test.go +++ b/executors_test.go @@ -556,6 +556,13 @@ func Test_Embedded_Struct(t *testing.T) { r.NoError(tx.Find(&actual, entry.ID)) r.Equal(entry.AdditionalField, actual.AdditionalField) + entry.AdditionalField = entry.AdditionalField + "; updated again" + count, err := tx.Where("id = ?", entry.ID).UpdateQuery(entry, "additional_field") + r.NoError(err) + require.Equal(t, int64(1), count) + r.NoError(tx.Find(&actual, entry.ID)) + r.Equal(entry.AdditionalField, actual.AdditionalField) + r.NoError(tx.Destroy(entry)) }) } @@ -1231,6 +1238,150 @@ func Test_Eager_Creation_Without_Associations(t *testing.T) { }) } +func Test_Eager_Embedded_Struct(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + type AssocFields struct { + Books Books `has_many:"books" order_by:"title asc"` + FavoriteSong Song `has_one:"song" fk_id:"u_id"` + Houses Addresses `many_to_many:"users_addresses"` + } + + type User struct { + ID int `db:"id"` + UserName string `db:"user_name"` + Email string `db:"email"` + Name nulls.String `db:"name"` + Alive nulls.Bool `db:"alive"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + BirthDate nulls.Time `db:"birth_date"` + Bio nulls.String `db:"bio"` + Price nulls.Float64 `db:"price"` + FullName nulls.String `db:"full_name" select:"name as full_name"` + + AssocFields + } + + count, _ := tx.Count(&User{}) + user := User{ + UserName: "dumb-dumb", + Name: nulls.NewString("Arthur Dent"), + AssocFields: AssocFields{ + Books: Books{{Title: "The Hitchhiker's Guide to the Galaxy", Description: "Comedy Science Fiction somewhere in Space", Isbn: "PB42"}}, + FavoriteSong: Song{Title: "Wish You Were Here", ComposedBy: Composer{Name: "Pink Floyd"}}, + Houses: Addresses{ + Address{HouseNumber: 155, Street: "Country Lane"}, + }, + }, + } + + err := tx.Eager().Create(&user) + r.NoError(err) + r.NotZero(user.ID) + + ctx, _ := tx.Count(&User{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Book{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Song{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Address{}) + r.Equal(count+1, ctx) + + u := User{} + q := tx.Eager().Where("name = ?", user.Name.String) + err = q.First(&u) + r.NoError(err) + + r.Equal(user.Name.String, u.Name.String) + r.Len(u.Books, 1) + r.Equal(user.Books[0].Title, u.Books[0].Title) + r.Equal(user.FavoriteSong.Title, u.FavoriteSong.Title) + r.Len(u.Houses, 1) + r.Equal(user.Houses[0].Street, u.Houses[0].Street) + }) +} + +func Test_Eager_Embedded_Ptr_Struct(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + type AssocFields struct { + Books Books `has_many:"books" order_by:"title asc"` + FavoriteSong Song `has_one:"song" fk_id:"u_id"` + Houses Addresses `many_to_many:"users_addresses"` + } + + type User struct { + ID int `db:"id"` + UserName string `db:"user_name"` + Email string `db:"email"` + Name nulls.String `db:"name"` + Alive nulls.Bool `db:"alive"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + BirthDate nulls.Time `db:"birth_date"` + Bio nulls.String `db:"bio"` + Price nulls.Float64 `db:"price"` + FullName nulls.String `db:"full_name" select:"name as full_name"` + + *AssocFields + } + + count, _ := tx.Count(&User{}) + user := User{ + UserName: "dumb-dumb", + Name: nulls.NewString("Arthur Dent"), + AssocFields: &AssocFields{ + Books: Books{{Title: "The Hitchhiker's Guide to the Galaxy", Description: "Comedy Science Fiction somewhere in Space", Isbn: "PB42"}}, + FavoriteSong: Song{Title: "Wish You Were Here", ComposedBy: Composer{Name: "Pink Floyd"}}, + Houses: Addresses{ + Address{HouseNumber: 155, Street: "Country Lane"}, + }, + }, + } + + err := tx.Eager().Create(&user) + r.NoError(err) + r.NotZero(user.ID) + + ctx, _ := tx.Count(&User{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Book{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Song{}) + r.Equal(count+1, ctx) + + ctx, _ = tx.Count(&Address{}) + r.Equal(count+1, ctx) + + u := User{} + q := tx.Eager().Where("name = ?", user.Name.String) + err = q.First(&u) + r.NoError(err) + + r.Equal(user.Name.String, u.Name.String) + r.Len(u.Books, 1) + r.Equal(user.Books[0].Title, u.Books[0].Title) + r.Equal(user.FavoriteSong.Title, u.FavoriteSong.Title) + r.Len(u.Houses, 1) + r.Equal(user.Houses[0].Street, u.Houses[0].Street) + }) +} + func Test_Create_UUID(t *testing.T) { if PDB == nil { t.Skip("skipping integration tests") @@ -1349,6 +1500,107 @@ func Test_UpdateColumns(t *testing.T) { }) } +func Test_UpdateQuery_NoUpdatedAt(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + r.NoError(PDB.Create(&NonStandardID{OutfacingID: "must-change"})) + count, err := PDB.Where("true").UpdateQuery(&NonStandardID{OutfacingID: "has-changed"}, "id") + r.NoError(err) + r.Equal(int64(1), count) + entity := NonStandardID{} + r.NoError(PDB.First(&entity)) + r.Equal("has-changed", entity.OutfacingID) + }) +} + +func Test_UpdateQuery_NoTransaction(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + + r := require.New(t) + u1 := User{Name: nulls.NewString("Foo"), Bio: nulls.NewString("must-not-change-1")} + r.NoError(PDB.Create(&u1)) + r.NoError(PDB.Reload(&u1)) + count, err := PDB.Where("name = ?", "Nemo").UpdateQuery(&User{Bio: nulls.NewString("did-change")}, "bio") + r.NoError(err) + require.Equal(t, int64(0), count) + + count, err = PDB.Where("name = ?", "Foo").UpdateQuery(&User{Name: nulls.NewString("Bar")}, "name") + r.NoError(err) + r.Equal(int64(1), count) + + require.NoError(t, PDB.Destroy(&u1)) +} + +func Test_UpdateQuery(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + u1 := User{Name: nulls.NewString("Foo"), Bio: nulls.NewString("must-not-change-1")} + u2 := User{Name: nulls.NewString("Foo"), Bio: nulls.NewString("must-not-change-2")} + u3 := User{Name: nulls.NewString("Baz"), Bio: nulls.NewString("must-not-change-3")} + tx.Create(&u1) + tx.Create(&u2) + tx.Create(&u3) + r.NoError(tx.Reload(&u1)) + r.NoError(tx.Reload(&u2)) + r.NoError(tx.Reload(&u3)) + time.Sleep(time.Millisecond * 1) + + // No affected rows + count, err := tx.Where("name = ?", "Nemo").UpdateQuery(&User{Bio: nulls.NewString("did-change")}, "bio") + r.NoError(err) + require.Equal(t, int64(0), count) + mustUnchanged := &User{} + r.NoError(tx.Find(mustUnchanged, u1.ID)) + r.Equal(u1.Bio, mustUnchanged.Bio) + r.Equal(u1.UpdatedAt, mustUnchanged.UpdatedAt) + + // Correct rows are updated, including updated_at + count, err = tx.Where("name = ?", "Foo").UpdateQuery(&User{Name: nulls.NewString("Bar")}, "name") + r.NoError(err) + r.Equal(int64(2), count) + + u1b, u2b, u3b := &User{}, &User{}, &User{} + r.NoError(tx.Find(u1b, u1.ID)) + r.NoError(tx.Find(u2b, u2.ID)) + r.NoError(tx.Find(u3b, u3.ID)) + r.Equal(u1b.Name.String, "Bar") + r.Equal(u2b.Name.String, "Bar") + r.Equal(u3b.Name.String, "Baz") + r.Equal(u1b.Bio.String, "must-not-change-1") + r.Equal(u2b.Bio.String, "must-not-change-2") + r.Equal(u3b.Bio.String, "must-not-change-3") + if tx.Dialect.Name() != nameMySQL { // MySQL timestamps are in seconds + r.NotEqual(u1.UpdatedAt, u1b.UpdatedAt) + r.NotEqual(u2.UpdatedAt, u2b.UpdatedAt) + } + r.Equal(u3.UpdatedAt, u3b.UpdatedAt) + + // ID is ignored + count, err = tx.Where("true").UpdateQuery(&User{ID: 123, Name: nulls.NewString("Bar")}, "id", "name") + r.NoError(tx.Find(u1b, u1.ID)) + r.NoError(tx.Find(u2b, u2.ID)) + r.NoError(tx.Find(u3b, u3.ID)) + r.Equal(u1b.Name.String, "Bar") + r.Equal(u2b.Name.String, "Bar") + r.Equal(u3b.Name.String, "Bar") + + // Invalid column yields an error + count, err = tx.Where("name = ?", "Foo").UpdateQuery(&User{Name: nulls.NewString("Bar")}, "mistake") + r.Contains(err.Error(), "could not find name mistake") + + tx.Where("true").Delete(&User{}) + }) +} + func Test_UpdateColumns_UpdatedAt(t *testing.T) { if PDB == nil { t.Skip("skipping integration tests") @@ -1712,3 +1964,28 @@ func Test_Delete(t *testing.T) { r.Equal(count, ctx) }) } + +func Test_Create_Timestamps_With_NowFunc(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + originalNowFunc := nowFunc + // ensure the original function is restored + defer func() { + nowFunc = originalNowFunc + }() + + fakeNow, _ := time.Parse(time.RFC3339, "2019-07-14T00:00:00Z") + SetNowFunc(func() time.Time { return fakeNow }) + + friend := Friend{FirstName: "Yester", LastName: "Day"} + err := tx.Create(&friend) + r.NoError(err) + + r.Equal(fakeNow, friend.CreatedAt) + r.Equal(fakeNow, friend.UpdatedAt) + }) +} diff --git a/model.go b/model.go index dddbec66..7b03d78f 100644 --- a/model.go +++ b/model.go @@ -15,6 +15,11 @@ import ( var nowFunc = time.Now +// SetNowFunc allows an override of time.Now for customizing CreatedAt/UpdatedAt +func SetNowFunc(f func() time.Time) { + nowFunc = f +} + // Value is the contents of a `Model`. type Value interface{} diff --git a/pop_test.go b/pop_test.go index 629ea35a..95fd8648 100644 --- a/pop_test.go +++ b/pop_test.go @@ -52,7 +52,7 @@ func init() { dialect := os.Getenv("SODA_DIALECT") if dialect == "" { - log(logging.Info, "Skipping integration tests") + log(logging.Info, "Skipping integration tests because SODA_DIALECT is blank or unset") return }