package mysql import ( "context" "slices" "strings" "testing" "github.com/go-jet/jet/v2/qrm" "github.com/go-jet/jet/v2/internal/testutils" . "github.com/go-jet/jet/v2/mysql" "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/model" . "github.com/go-jet/jet/v2/tests/.gentestdata/mysql/dvds/table" "github.com/stretchr/testify/require" ) var ctx = context.Background() func TestSelectJsonObj(t *testing.T) { stmt := SELECT_JSON_OBJ(Actor.AllColumns). FROM(Actor). WHERE(Actor.ActorID.EQ(Int(2))) testutils.AssertStatementSql(t, stmt, ` SELECT JSON_OBJECT( 'actorID', actor.actor_id, 'firstName', actor.first_name, 'lastName', actor.last_name, 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') ) AS "json" FROM dvds.actor WHERE actor.actor_id = ?; `, int64(2)) var dest model.Actor err := stmt.Query(db, &dest) require.Nil(t, err) testutils.AssertDeepEqual(t, dest, actor2) requireLogged(t, stmt) requireQueryLogged(t, stmt, 1) } func TestSelectJsonObj_NestedObj(t *testing.T) { stmt := SELECT_JSON_OBJ( Actor.AllColumns, SELECT_JSON_OBJ(Film.AllColumns). FROM(FilmActor.INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID))). WHERE(Actor.ActorID.EQ(FilmActor.ActorID)). ORDER_BY(Film.Length.DESC()). LIMIT(1).OFFSET(3).AS("LongestFilm"), ).FROM( Actor, ).WHERE( Actor.ActorID.EQ(Int(2)), ) testutils.AssertStatementSql(t, stmt, ` SELECT JSON_OBJECT( 'actorID', actor.actor_id, 'firstName', actor.first_name, 'lastName', actor.last_name, 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ'), 'LongestFilm', ( SELECT JSON_OBJECT( 'filmID', film.film_id, 'title', film.title, 'description', film.description, 'releaseYear', film.release_year, 'languageID', film.language_id, 'originalLanguageID', film.original_language_id, 'rentalDuration', film.rental_duration, 'rentalRate', film.rental_rate, 'length', film.length, 'replacementCost', film.replacement_cost, 'rating', film.rating, 'specialFeatures', film.special_features, 'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') ) AS "json" FROM dvds.film_actor INNER JOIN dvds.film ON (film.film_id = film_actor.film_id) WHERE actor.actor_id = film_actor.actor_id ORDER BY film.length DESC LIMIT ? OFFSET ? ) ) AS "json" FROM dvds.actor WHERE actor.actor_id = ?; `) var dest struct { model.Actor LongestFilm model.Film } err := stmt.QueryContext(ctx, db, &dest) require.Nil(t, err) testutils.AssertJSON(t, dest, ` { "ActorID": 2, "FirstName": "NICK", "LastName": "WAHLBERG", "LastUpdate": "2006-02-15T04:34:33Z", "LongestFilm": { "FilmID": 754, "Title": "RUSHMORE MERMAID", "Description": "A Boring Story of a Woman And a Moose who must Reach a Husband in A Shark Tank", "ReleaseYear": 2006, "LanguageID": 1, "OriginalLanguageID": null, "RentalDuration": 6, "RentalRate": 2.99, "Length": 150, "ReplacementCost": 17.99, "Rating": "PG-13", "SpecialFeatures": "Trailers,Commentaries,Deleted Scenes", "LastUpdate": "2006-02-15T05:03:42Z" } } `) } func TestSelectJsonArr(t *testing.T) { onlyMariaDB(t) var savedActors []model.Actor testutils.ReadJSONFile(t, "./testdata/results/mysql/all_actors.json", &savedActors) t.Run("order by", func(t *testing.T) { stmt := SELECT_JSON_ARR(Actor.AllColumns). FROM(Actor). ORDER_BY( Actor.LastName.DESC(), Actor.FirstName.ASC(), Actor.ActorID.ASC(), ) testutils.AssertDebugStatementSql(t, stmt, ` SELECT JSON_ARRAYAGG(JSON_OBJECT( 'actorID', actor.actor_id, 'firstName', actor.first_name, 'lastName', actor.last_name, 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') ) ORDER BY actor.last_name DESC, actor.first_name ASC, actor.actor_id ASC) AS "json" FROM dvds.actor; `) var dest []model.Actor err := stmt.Query(db, &dest) require.Nil(t, err) // sort by actor.LastName desc slices.SortFunc(savedActors, func(a, b model.Actor) int { if l := strings.Compare(b.LastName, a.LastName); l != 0 { return l } if f := strings.Compare(a.FirstName, b.FirstName); f != 0 { return f } return int(a.ActorID) - int(b.ActorID) }) require.Equal(t, dest, savedActors) requireLogged(t, stmt) requireQueryLogged(t, stmt, 1) }) t.Run("order by, limit, offset", func(t *testing.T) { stmt := SELECT_JSON_ARR(Actor.AllColumns). FROM(Actor). ORDER_BY(Actor.ActorID.DESC()). LIMIT(5). OFFSET(10) testutils.AssertStatementSql(t, stmt, ` SELECT JSON_ARRAYAGG(JSON_OBJECT( 'actorID', actor.actor_id, 'firstName', actor.first_name, 'lastName', actor.last_name, 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') ) ORDER BY actor.actor_id DESC LIMIT ? OFFSET ?) AS "json" FROM dvds.actor; `) var dest []model.Actor err := stmt.Query(db, &dest) require.Nil(t, err) slices.SortFunc(savedActors, func(a, b model.Actor) int { return int(b.ActorID) - int(a.ActorID) }) require.Equal(t, dest, savedActors[10:15]) }) } func TestSelectJsonArr_NestedArr(t *testing.T) { onlyMariaDB(t) stmt := SELECT_JSON_ARR( Actor.AllColumns, SELECT_JSON_ARR( Film.AllColumns, ).FROM( FilmActor.INNER_JOIN( Film, Film.FilmID.EQ(FilmActor.FilmID).AND( Actor.ActorID.EQ(FilmActor.ActorID)), ), ).WHERE( Film.FilmID.MOD(Int(17)).EQ(Int(0)), ).ORDER_BY( Film.Length.DESC(), ).AS("Films"), ).FROM( Actor, ).WHERE( Actor.ActorID.BETWEEN(Int(1), Int(3)), ).ORDER_BY( Actor.ActorID, ) testutils.AssertDebugStatementSql(t, stmt, ` SELECT JSON_ARRAYAGG(JSON_OBJECT( 'actorID', actor.actor_id, 'firstName', actor.first_name, 'lastName', actor.last_name, 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ'), 'Films', ( SELECT JSON_ARRAYAGG(JSON_OBJECT( 'filmID', film.film_id, 'title', film.title, 'description', film.description, 'releaseYear', film.release_year, 'languageID', film.language_id, 'originalLanguageID', film.original_language_id, 'rentalDuration', film.rental_duration, 'rentalRate', film.rental_rate, 'length', film.length, 'replacementCost', film.replacement_cost, 'rating', film.rating, 'specialFeatures', film.special_features, 'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') ) ORDER BY film.length DESC) AS "json" FROM dvds.film_actor INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id)) WHERE (film.film_id % 17) = 0 ) ) ORDER BY actor.actor_id) AS "json" FROM dvds.actor WHERE actor.actor_id BETWEEN 1 AND 3; `) var dest []struct { model.Actor Films []model.Film } err := stmt.QueryContext(ctx, db, &dest) require.NoError(t, err) testutils.AssertJSON(t, dest, ` [ { "ActorID": 1, "FirstName": "PENELOPE", "LastName": "GUINESS", "LastUpdate": "2006-02-15T04:34:33Z", "Films": null }, { "ActorID": 2, "FirstName": "NICK", "LastName": "WAHLBERG", "LastUpdate": "2006-02-15T04:34:33Z", "Films": [ { "FilmID": 561, "Title": "MASK PEACH", "Description": "A Boring Character Study of a Student And a Robot who must Meet a Woman in California", "ReleaseYear": 2006, "LanguageID": 1, "OriginalLanguageID": null, "RentalDuration": 6, "RentalRate": 2.99, "Length": 123, "ReplacementCost": 26.99, "Rating": "NC-17", "SpecialFeatures": "Commentaries,Deleted Scenes", "LastUpdate": "2006-02-15T05:03:42Z" }, { "FilmID": 357, "Title": "GILBERT PELICAN", "Description": "A Fateful Tale of a Man And a Feminist who must Conquer a Crocodile in A Manhattan Penthouse", "ReleaseYear": 2006, "LanguageID": 1, "OriginalLanguageID": null, "RentalDuration": 7, "RentalRate": 0.99, "Length": 114, "ReplacementCost": 13.99, "Rating": "G", "SpecialFeatures": "Trailers,Commentaries", "LastUpdate": "2006-02-15T05:03:42Z" } ] }, { "ActorID": 3, "FirstName": "ED", "LastName": "CHASE", "LastUpdate": "2006-02-15T04:34:33Z", "Films": [ { "FilmID": 17, "Title": "ALONE TRIP", "Description": "A Fast-Paced Character Study of a Composer And a Dog who must Outgun a Boat in An Abandoned Fun House", "ReleaseYear": 2006, "LanguageID": 1, "OriginalLanguageID": null, "RentalDuration": 3, "RentalRate": 0.99, "Length": 82, "ReplacementCost": 14.99, "Rating": "R", "SpecialFeatures": "Trailers,Behind the Scenes", "LastUpdate": "2006-02-15T05:03:42Z" }, { "FilmID": 289, "Title": "EVE RESURRECTION", "Description": "A Awe-Inspiring Yarn of a Pastry Chef And a Database Administrator who must Challenge a Teacher in A Baloon", "ReleaseYear": 2006, "LanguageID": 1, "OriginalLanguageID": null, "RentalDuration": 5, "RentalRate": 4.99, "Length": 66, "ReplacementCost": 25.99, "Rating": "G", "SpecialFeatures": "Trailers,Commentaries,Deleted Scenes", "LastUpdate": "2006-02-15T05:03:42Z" } ] } ] `) } func TestSelectJson_GroupBy(t *testing.T) { skipForMariaDB(t) // scope issues with select without FROM subQuery := SELECT( Customer.AllColumns, SUM(Payment.Amount).AS("sum"), AVG(Payment.Amount).AS("avg"), MAX(Payment.Amount).AS("max"), MIN(Payment.Amount).AS("min"), COUNT(Payment.Amount).AS("count"), ).FROM( Payment. INNER_JOIN(Customer, Customer.CustomerID.EQ(Payment.CustomerID)), ).GROUP_BY( Customer.CustomerID, ).HAVING( SUMf(Payment.Amount).GT(Float(125)), ).ORDER_BY( Customer.CustomerID, SUM(Payment.Amount).ASC(), ).AsTable("customers_info") stmt := SELECT_JSON_ARR( Customer.AllColumns.From(subQuery), SELECT_JSON_OBJ( FloatColumn("sum").From(subQuery), FloatColumn("avg").From(subQuery), FloatColumn("max").From(subQuery), FloatColumn("min").From(subQuery), FloatColumn("count").From(subQuery), ).AS("amount"), ).FROM(subQuery) testutils.AssertDebugStatementSql(t, stmt, strings.ReplaceAll(` SELECT JSON_ARRAYAGG(JSON_OBJECT( 'customerID', customers_info.''customer.customer_id'', 'storeID', customers_info.''customer.store_id'', 'firstName', customers_info.''customer.first_name'', 'lastName', customers_info.''customer.last_name'', 'email', customers_info.''customer.email'', 'addressID', customers_info.''customer.address_id'', 'active', (customers_info.''customer.active'' = 1), 'createDate', DATE_FORMAT(customers_info.''customer.create_date'','%Y-%m-%dT%H:%i:%s.%fZ'), 'lastUpdate', DATE_FORMAT(customers_info.''customer.last_update'','%Y-%m-%dT%H:%i:%s.%fZ'), 'amount', ( SELECT JSON_OBJECT( 'sum', customers_info.sum, 'avg', customers_info.avg, 'max', customers_info.max, 'min', customers_info.min, 'count', customers_info.count ) AS "json" ) )) AS "json" FROM ( SELECT customer.customer_id AS "customer.customer_id", customer.store_id AS "customer.store_id", customer.first_name AS "customer.first_name", customer.last_name AS "customer.last_name", customer.email AS "customer.email", customer.address_id AS "customer.address_id", customer.active AS "customer.active", customer.create_date AS "customer.create_date", customer.last_update AS "customer.last_update", SUM(payment.amount) AS "sum", AVG(payment.amount) AS "avg", MAX(payment.amount) AS "max", MIN(payment.amount) AS "min", COUNT(payment.amount) AS "count" FROM dvds.payment INNER JOIN dvds.customer ON (customer.customer_id = payment.customer_id) GROUP BY customer.customer_id HAVING SUM(payment.amount) > 125 ORDER BY customer.customer_id, SUM(payment.amount) ASC ) AS customers_info; `, "''", "`")) var dest []struct { model.Customer Amount struct { Sum float64 Avg float64 Max float64 Min float64 Count int64 } } err := stmt.QueryContext(ctx, db, &dest) require.Nil(t, err) testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/customer_payment_sum.json") requireLogged(t, stmt) } func TestSelectJsonObject_EmptyResult(t *testing.T) { t.Run("json obj", func(t *testing.T) { stmt := SELECT_JSON_OBJ(Actor.AllColumns). FROM(Actor). WHERE(Actor.FirstName.EQ(String("Kowalski"))) var dest model.Actor err := stmt.QueryContext(ctx, db, &dest) require.ErrorIs(t, err, qrm.ErrNoRows) }) t.Run("json arr", func(t *testing.T) { stmt := SELECT_JSON_ARR(Actor.AllColumns). FROM(Actor). WHERE(Actor.FirstName.EQ(String("Kowalski"))) var dest []model.Actor err := stmt.QueryContext(ctx, db, &dest) require.NoError(t, err) require.Empty(t, dest) }) } func TestSelectJson_ProjectionNotAliased(t *testing.T) { t.Run("expression not aliased", func(t *testing.T) { testutils.AssertPanicErr(t, func() { stmt := SELECT_JSON_ARR( Int(2).ADD(Customer.CustomerID), ).FROM(Customer) stmt.DebugSql() }, "jet: expression need to be aliased when used as SELECT JSON projection.") }) } func TestSelectJsonObject_EscapesJsonKeys(t *testing.T) { stmt := SELECT_JSON_OBJ( String("value").AS("author"), String("value").AS("author's name"), String("value").AS("author''s name"), String("value").AS("author \"name\""), String("value").AS(`C:\tmp\file`), String("value").AS("hello\nworld"), String("value").AS("a'b\\\\c\\nd\\r\\x00e\\x1af"), String("value").AS("žika 😀"), ) testutils.AssertDebugStatementSql(t, stmt, ` SELECT JSON_OBJECT( 'author', 'value', 'author''s name', 'value', 'author''''s name', 'value', 'author "name"', 'value', 'C:\tmp\file', 'value', 'hello world', 'value', 'a''b\\c\nd\r\x00e\x1af', 'value', 'žika 😀', 'value' ) AS "json"; `) var dest map[string]any err := stmt.QueryContext(ctx, db, &dest) require.NoError(t, err) testutils.AssertJSON(t, dest, ` { "C:\tmpfile": "value", "a'b\\c\nd\rx00ex1af": "value", "author": "value", "author \"name\"": "value", "author''s name": "value", "author's name": "value", "hello\nworld": "value", "žika 😀": "value" } `) }