jet/tests/mysql/select_json_test.go
Eli Ribble 44d981fbfc
Update references to jet v2 in tests
I'm guessing people don't run these anymore...
2026-05-14 16:30:22 +00:00

551 lines
15 KiB
Go

package mysql
import (
"context"
"slices"
"strings"
"testing"
"source.gleipnir.technology/Gleipnir/jet/qrm"
"source.gleipnir.technology/Gleipnir/jet/internal/testutils"
. "source.gleipnir.technology/Gleipnir/jet/mysql"
"source.gleipnir.technology/Gleipnir/jet/v2/tests/.gentestdata/mysql/dvds/model"
. "source.gleipnir.technology/Gleipnir/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"
}
`)
}