2023-04-01 12:48:24 +03:00
package libsq_test
2023-04-01 11:38:32 +03:00
import (
2023-07-03 18:34:19 +03:00
"fmt"
2023-04-01 11:38:32 +03:00
"testing"
2023-11-20 04:06:36 +03:00
_ "github.com/mattn/go-sqlite3"
"github.com/samber/lo"
"github.com/neilotoole/sq/drivers/mysql"
2023-07-03 18:34:19 +03:00
"github.com/neilotoole/sq/drivers/postgres"
"github.com/neilotoole/sq/drivers/sqlite3"
"github.com/neilotoole/sq/drivers/sqlserver"
"github.com/neilotoole/sq/libsq/core/jointype"
2023-11-21 00:42:38 +03:00
"github.com/neilotoole/sq/libsq/source/drivertype"
2023-04-01 11:38:32 +03:00
"github.com/neilotoole/sq/testh/sakila"
2024-01-15 04:45:34 +03:00
"github.com/neilotoole/sq/testh/tu"
2023-04-01 11:38:32 +03:00
)
2023-07-03 18:34:19 +03:00
func TestQuery_join_args ( t * testing . T ) {
testCases := [ ] queryTestCase {
{
name : "error/missing-predicate" ,
in : ` @sakila | .actor | join(.film_actor) ` ,
wantErr : true ,
repeatReplace : predicateJoinNames ,
} ,
{
name : "error/unwanted-predicate" ,
in : ` @sakila | .actor | join(.film_actor) ` ,
wantErr : true ,
repeatReplace : noPredicateJoinNames ,
} ,
{
name : "error/too-many-args" ,
in : ` @sakila | .actor | join(.film_actor, .actor_id, .first_name) ` ,
wantErr : true ,
repeatReplace : jointype . AllValues ( ) ,
} ,
{
name : "error/no-args" ,
in : ` @sakila | .store | join() ` ,
wantErr : true ,
repeatReplace : jointype . AllValues ( ) ,
} ,
}
for _ , tc := range testCases {
tc := tc
t . Run ( tc . name , func ( t * testing . T ) {
execQueryTestCase ( t , tc )
} )
}
}
//nolint:exhaustive,lll
func TestQuery_join_inner ( t * testing . T ) {
testCases := [ ] queryTestCase {
{
name : "n1/equals-no-alias" ,
in : ` @sakila | .store | join(.address, .store.address_id == .address.address_id) ` ,
wantRecCount : 2 ,
} ,
{
name : "n1/equals-with-alias" ,
in : ` @sakila | .store:s | join(.address:a, .s.address_id == .a.address_id) ` ,
wantSQL : ` SELECT * FROM "store" AS "s" INNER JOIN "address" AS "a" ON "s"."address_id" = "a"."address_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` AS `s` INNER JOIN `address` AS `a` ON `s`.`address_id` = `a`.`address_id`" } ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
} ,
{
name : "n2/equals-with-alias/unqualified-cols" ,
in : ` @sakila | .actor:a | join(.film_actor:fa, .a.actor_id == .fa.actor_id) | join(.film:f, .fa.film_id == .f.film_id) | .first_name, .last_name, .title ` ,
wantSQL : ` SELECT "first_name", "last_name", "title" FROM "actor" AS "a" INNER JOIN "film_actor" AS "fa" ON "a"."actor_id" = "fa"."actor_id" INNER JOIN "film" AS "f" ON "fa"."film_id" = "f"."film_id" ` ,
override : driverMap { mysql . Type : "SELECT `first_name`, `last_name`, `title` FROM `actor` AS `a` INNER JOIN `film_actor` AS `fa` ON `a`.`actor_id` = `fa`.`actor_id` INNER JOIN `film` AS `f` ON `fa`.`film_id` = `f`.`film_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : innerJoins ,
} ,
{
name : "n2/equals-with-alias/qualified-cols" ,
in : ` @sakila | .actor:a | join(.film_actor:fa, .a.actor_id == .fa.actor_id) | join(.film:f, .fa.film_id == .f.film_id) | .a.first_name, .a.last_name, .f.title ` ,
wantSQL : ` SELECT "a"."first_name", "a"."last_name", "f"."title" FROM "actor" AS "a" INNER JOIN "film_actor" AS "fa" ON "a"."actor_id" = "fa"."actor_id" INNER JOIN "film" AS "f" ON "fa"."film_id" = "f"."film_id" ` ,
override : driverMap { mysql . Type : "SELECT `a`.`first_name`, `a`.`last_name`, `f`.`title` FROM `actor` AS `a` INNER JOIN `film_actor` AS `fa` ON `a`.`actor_id` = `fa`.`actor_id` INNER JOIN `film` AS `f` ON `fa`.`film_id` = `f`.`film_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : innerJoins ,
} ,
{
name : "n1/single-selector-no-alias" ,
in : ` @sakila | .store | join(.address, .address_id) ` ,
wantSQL : ` SELECT * FROM "store" INNER JOIN "address" ON "store"."address_id" = "address"."address_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` INNER JOIN `address` ON `store`.`address_id` = `address`.`address_id`" } ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
} ,
{
name : "n1/table-handle-single-selector-no-alias" ,
in : ` @sakila.store | join(.address, .address_id) ` ,
wantSQL : ` SELECT * FROM "store" INNER JOIN "address" ON "store"."address_id" = "address"."address_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` INNER JOIN `address` ON `store`.`address_id` = `address`.`address_id`" } ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
} ,
{
name : "n1/single-selector-with-alias" ,
in : ` @sakila | .store:s | join(.address:a, .address_id) ` ,
wantSQL : ` SELECT * FROM "store" AS "s" INNER JOIN "address" AS "a" ON "s"."address_id" = "a"."address_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` AS `s` INNER JOIN `address` AS `a` ON `s`.`address_id` = `a`.`address_id`" } ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
} ,
{
name : "cross-join/n1/no-constraint" ,
in : ` @sakila | .store | cross_join(.address) ` ,
wantSQL : ` SELECT * FROM "store" CROSS JOIN "address" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` CROSS JOIN `address`" } ,
wantRecCount : 1206 ,
repeatReplace : [ ] string { string ( jointype . Cross ) , jointype . CrossAlias } ,
} ,
}
for _ , tc := range testCases {
tc := tc
t . Run ( tc . name , func ( t * testing . T ) {
execQueryTestCase ( t , tc )
} )
}
}
//nolint:lll
func TestQuery_join_multi_source ( t * testing . T ) {
testCases := [ ] queryTestCase {
{
name : "n1/equals-no-alias" ,
in : fmt . Sprintf (
` @sakila | .store | join(%s.address, .store.address_id == .address.address_id) ` ,
sakila . SL3 ,
) ,
wantSQL : ` SELECT * FROM "store" INNER JOIN "address" ON "store"."address_id" = "address"."address_id" ` ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinStoreAddress ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n1/table-handle-equals-no-alias" ,
in : fmt . Sprintf (
` @sakila.store | join(%s.address, .store.address_id == .address.address_id) ` ,
sakila . SL3 ,
) ,
wantSQL : ` SELECT * FROM "store" INNER JOIN "address" ON "store"."address_id" = "address"."address_id" ` ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinStoreAddress ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n1/equals-with-alias" ,
in : fmt . Sprintf (
` @sakila | .store:s | join(%s.address:a, .s.address_id == .a.address_id) ` ,
sakila . Pg ,
) ,
wantRecCount : 2 ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinStoreAddress ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n2/two-sources" ,
in : fmt . Sprintf (
` @sakila | .actor | join(%s.film_actor, .actor_id) | join(.film, .film_id) | .first_name, .last_name, .title ` ,
sakila . Pg ,
) ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( "first_name" , "last_name" , "title" ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n2/three-sources-no-alias-no-col-alias" ,
in : fmt . Sprintf (
` @sakila | .actor | join(%s.film_actor, .actor_id) | join(%s.film, .film_id) | .first_name, .last_name, .title ` ,
sakila . Pg ,
sakila . My ,
) ,
wantSQL : ` SELECT "first_name", "last_name", "title" FROM "actor" INNER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" INNER JOIN "film" ON "film_actor"."film_id" = "film"."film_id" ` ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( "first_name" , "last_name" , "title" ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n2/three-sources-no-alias-all-cols" ,
in : fmt . Sprintf (
` @sakila | .actor | join(%s.film_actor, .actor_id) | join(%s.film, .film_id) ` ,
sakila . Pg ,
sakila . My ,
) ,
wantSQL : ` SELECT * FROM "actor" INNER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" INNER JOIN "film" ON "film_actor"."film_id" = "film"."film_id" ` ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : innerJoins ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActorFilm ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "n2/equals-with-alias/unqualified-cols" ,
in : fmt . Sprintf (
` @sakila | .actor:a | join(%s.film_actor:fa, .a.actor_id == .fa.actor_id) | join(%s.film:f, .fa.film_id == .f.film_id) | .first_name, .last_name, .title ` ,
sakila . Pg ,
sakila . My ,
) ,
wantRecCount : sakila . TblFilmActorCount ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( "first_name" , "last_name" , "title" ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
}
for _ , tc := range testCases {
tc := tc
t . Run ( tc . name , func ( t * testing . T ) {
execQueryTestCase ( t , tc )
} )
}
}
// TestQuery_join_others tests the join types other than INNER JOIN.
//
//nolint:exhaustive,lll
func TestQuery_join_others ( t * testing . T ) {
testCases := [ ] queryTestCase {
{
name : "left_join" ,
in : ` @sakila | .actor | left_join(.film_actor, .actor_id) ` ,
wantSQL : ` SELECT * FROM "actor" LEFT JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `actor` LEFT JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : [ ] string { string ( jointype . Left ) , jointype . LeftAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "left_outer_join" ,
in : ` @sakila | .actor | left_outer_join(.film_actor, .actor_id) ` ,
wantSQL : ` SELECT * FROM "actor" LEFT OUTER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `actor` LEFT OUTER JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : [ ] string { string ( jointype . LeftOuter ) , jointype . LeftOuterAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "right_join" ,
in : ` @sakila | .actor | right_join(.film_actor, .actor_id) ` ,
wantSQL : ` SELECT * FROM "actor" RIGHT JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `actor` RIGHT JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : [ ] string { string ( jointype . Right ) , jointype . RightAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "right_outer_join" ,
in : ` @sakila | .actor | right_outer_join(.film_actor, .actor_id) ` ,
wantSQL : ` SELECT * FROM "actor" RIGHT OUTER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `actor` RIGHT OUTER JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`" } ,
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : [ ] string { string ( jointype . RightOuter ) , jointype . RightOuterAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "full_outer_join" ,
in : ` @sakila | .actor | full_outer_join(.film_actor, .actor_id) ` ,
wantSQL : ` SELECT * FROM "actor" FULL OUTER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id" ` ,
// Note that MySQL doesn't support full outer join.
2023-11-21 00:42:38 +03:00
onlyFor : [ ] drivertype . Type { sqlite3 . Type , postgres . Type , sqlserver . Type } ,
2023-07-03 18:34:19 +03:00
wantRecCount : sakila . TblFilmActorCount ,
repeatReplace : [ ] string { string ( jointype . FullOuter ) , jointype . FullOuterAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
{
name : "full_outer_join/error-mysql" ,
in : ` @sakila | .actor | full_outer_join(.film_actor, .actor_id) ` ,
// Note that MySQL doesn't support full outer join.
2023-11-21 00:42:38 +03:00
onlyFor : [ ] drivertype . Type { mysql . Type } ,
2023-07-03 18:34:19 +03:00
wantErr : true ,
repeatReplace : [ ] string { string ( jointype . FullOuter ) , jointype . FullOuterAlias } ,
} ,
{
name : "cross/store-address" ,
in : ` @sakila | .store | cross_join(.address) ` ,
wantSQL : ` SELECT * FROM "store" CROSS JOIN "address" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` CROSS JOIN `address`" } ,
wantRecCount : 1206 ,
repeatReplace : [ ] string { string ( jointype . Cross ) , jointype . CrossAlias } ,
} ,
{
name : "cross/store-staff" ,
in : ` @sakila | .store | cross_join(.staff) ` ,
wantSQL : ` SELECT * FROM "store" CROSS JOIN "staff" ` ,
override : driverMap { mysql . Type : "SELECT * FROM `store` CROSS JOIN `staff`" } ,
wantRecCount : 4 ,
repeatReplace : [ ] string { string ( jointype . Cross ) , jointype . CrossAlias } ,
} ,
{
name : "cross/actor-film_actor/no-constraint" ,
in : ` @sakila | .actor | cross_join(.film_actor) | .[0:10] ` ,
wantRecCount : 10 ,
repeatReplace : [ ] string { string ( jointype . Cross ) , jointype . CrossAlias } ,
sinkFns : [ ] SinkTestFunc {
2023-07-27 07:19:11 +03:00
assertSinkColMungedNames ( colsJoinActorFilmActor ... ) ,
2023-07-03 18:34:19 +03:00
} ,
} ,
}
for _ , tc := range testCases {
tc := tc
t . Run ( tc . name , func ( t * testing . T ) {
execQueryTestCase ( t , tc )
} )
}
}
// TestQuery_table_alias is tested with the joins, because table aliases
// are primarily for use with join.
//
2023-06-25 19:29:24 +03:00
//nolint:exhaustive
func TestQuery_table_alias ( t * testing . T ) {
testCases := [ ] queryTestCase {
{
name : "table-alias" ,
in : ` @sakila | .actor:a | .a.first_name ` ,
2023-07-03 18:34:19 +03:00
wantSQL : ` SELECT "a"."first_name" FROM "actor" AS "a" ` ,
override : driverMap { mysql . Type : "SELECT `a`.`first_name` FROM `actor` AS `a`" } ,
2023-06-25 19:29:24 +03:00
wantRecCount : sakila . TblActorCount ,
sinkFns : [ ] SinkTestFunc {
assertSinkColName ( 0 , "first_name" ) ,
} ,
} ,
{
name : "table-whitespace-alias" ,
in : ` @sakila | .actor:"oy vey" | ."oy vey".first_name ` ,
2023-07-03 18:34:19 +03:00
wantSQL : ` SELECT "oy vey"."first_name" FROM "actor" AS "oy vey" ` ,
override : driverMap { mysql . Type : "SELECT `oy vey`.`first_name` FROM `actor` AS `oy vey`" } ,
2023-06-25 19:29:24 +03:00
wantRecCount : sakila . TblActorCount ,
sinkFns : [ ] SinkTestFunc {
assertSinkColName ( 0 , "first_name" ) ,
} ,
} ,
{
name : "table-whitespace-alias-with-col-alias" ,
in : ` @sakila | .actor:"oy vey" | ."oy vey".first_name:given_name ` ,
2023-07-03 18:34:19 +03:00
wantSQL : ` SELECT "oy vey"."first_name" AS "given_name" FROM "actor" AS "oy vey" ` ,
override : driverMap { mysql . Type : "SELECT `oy vey`.`first_name` AS `given_name` FROM `actor` AS `oy vey`" } ,
2023-06-25 19:29:24 +03:00
wantRecCount : sakila . TblActorCount ,
sinkFns : [ ] SinkTestFunc {
assertSinkColName ( 0 , "given_name" ) ,
} ,
} ,
{
name : "table-whitespace-alias-with-col-whitespace-alias" ,
in : ` @sakila | .actor:"oy vey" | ."oy vey".first_name:"oy vey" ` ,
2023-07-03 18:34:19 +03:00
wantSQL : ` SELECT "oy vey"."first_name" AS "oy vey" FROM "actor" AS "oy vey" ` ,
override : driverMap { mysql . Type : "SELECT `oy vey`.`first_name` AS `oy vey` FROM `actor` AS `oy vey`" } ,
2023-06-25 19:29:24 +03:00
wantRecCount : sakila . TblActorCount ,
sinkFns : [ ] SinkTestFunc {
assertSinkColName ( 0 , "oy vey" ) ,
} ,
} ,
}
for i , tc := range testCases {
tc := tc
2024-01-15 04:45:34 +03:00
t . Run ( tu . Name ( i , tc . name ) , func ( t * testing . T ) {
2023-06-25 19:29:24 +03:00
execQueryTestCase ( t , tc )
} )
}
}
2023-07-03 18:34:19 +03:00
var (
noPredicateJoinNames = [ ] string {
string ( jointype . Cross ) ,
jointype . CrossAlias ,
2023-04-01 11:38:32 +03:00
}
2023-07-03 18:34:19 +03:00
innerJoins = [ ] string {
jointype . JoinAlias ,
string ( jointype . Inner ) ,
2023-04-01 11:38:32 +03:00
}
2023-07-03 18:34:19 +03:00
predicateJoinNames = lo . Without ( jointype . AllValues ( ) , noPredicateJoinNames ... )
colsJoinActorFilmActor = [ ] string {
"actor_id" ,
"first_name" ,
"last_name" ,
"last_update" ,
"actor_id_1" ,
"film_id" ,
"last_update_1" ,
}
colsJoinActorFilmActorFilm = [ ] string {
"actor_id" ,
"first_name" ,
"last_name" ,
"last_update" ,
"actor_id_1" ,
"film_id" ,
"last_update_1" ,
"film_id_1" ,
"title" ,
"description" ,
"release_year" ,
"language_id" ,
"original_language_id" ,
"rental_duration" ,
"rental_rate" ,
"length" ,
"replacement_cost" ,
"rating" ,
"special_features" ,
"last_update_2" ,
}
colsJoinStoreAddress = [ ] string {
"store_id" ,
"manager_staff_id" ,
"address_id" ,
"last_update" ,
"address_id_1" ,
"address" ,
"address2" ,
"district" ,
"city_id" ,
"postal_code" ,
"phone" ,
"last_update_1" ,
}
)