From 307c6ec30798b8947f5e3dbf13094bab44afc0c7 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:52:41 +0800 Subject: [PATCH] test(contrib/drivers/mysql): enhance complex query tests (#4707) ## Summary - Add comprehensive JOIN tests (Inner/Left/Right Join) - Add SubQuery tests - Add complex WHERE condition tests (Or/Group/Having) - Add advanced query builder tests **Test coverage added:** ~26 test functions across 3 files Ref #4689 ## Test plan ```bash cd contrib/drivers/mysql go test -v -run "TestModel_Join|TestModel_SubQuery|TestModel_Where.*Complex" ``` --- .../mysql_z_unit_feature_model_join_test.go | 336 +++++++++++++ ...ysql_z_unit_feature_model_subquery_test.go | 245 ++++++++++ .../mysql/mysql_z_unit_model_where_test.go | 460 ++++++++++++++++++ 3 files changed, 1041 insertions(+) diff --git a/contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go index af1acd993..8967fcc44 100644 --- a/contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_feature_model_join_test.go @@ -175,3 +175,339 @@ func Test_Model_FieldsPrefix(t *testing.T) { t.Assert(r[0]["nickname"], "name_1") }) } + +// Test_Model_Join_FiveTables tests complex join with 5+ tables +func Test_Model_Join_FiveTables(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + table3 = gtime.TimestampNanoStr() + "_table3" + table4 = gtime.TimestampNanoStr() + "_table4" + table5 = gtime.TimestampNanoStr() + "_table5" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + createInitTable(table3) + defer dropTable(table3) + createInitTable(table4) + defer dropTable(table4) + createInitTable(table5) + defer dropTable(table5) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id", "nickname"). + FieldsPrefix("t2", "passport"). + InnerJoin(table2+" AS t2", "t1.id = t2.id"). + InnerJoin(table3+" AS t3", "t2.id = t3.id"). + InnerJoin(table4+" AS t4", "t3.id = t4.id"). + InnerJoin(table5+" AS t5", "t4.id = t5.id"). + Where("t1.id IN(?)", g.Slice{1, 2, 3}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[0]["nickname"], "name_1") + t.Assert(r[0]["passport"], "user_1") + t.Assert(r[2]["id"], "3") + }) + + gtest.C(t, func(t *gtest.T) { + // 6 tables with mixed join types + table6 := gtime.TimestampNanoStr() + "_table6" + createInitTable(table6) + defer dropTable(table6) + + r, err := db.Model(table1).As("t1"). + Fields("t1.id"). + InnerJoin(table2+" AS t2", "t1.id = t2.id"). + LeftJoin(table3+" AS t3", "t2.id = t3.id"). + InnerJoin(table4+" AS t4", "t3.id = t4.id"). + RightJoin(table5+" AS t5", "t4.id = t5.id"). + LeftJoin(table6+" AS t6", "t5.id = t6.id"). + Where("t1.id", 5). + One() + t.AssertNil(err) + + t.Assert(r["id"], "5") + }) +} + +// Test_Model_Join_SelfJoin tests self-join scenarios +func Test_Model_Join_SelfJoin(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Self-join to find pairs where a.id < b.id + r, err := db.Model(table).As("a"). + Fields("a.id AS a_id", "b.id AS b_id"). + InnerJoin(table+" AS b", "a.id < b.id"). + Where("a.id", 1). + Where("b.id <=", 3). + Order("b.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 2) + t.Assert(r[0]["a_id"], "1") + t.Assert(r[0]["b_id"], "2") + t.Assert(r[1]["b_id"], "3") + }) + + gtest.C(t, func(t *gtest.T) { + // Self-join with multiple conditions + r, err := db.Model(table).As("a"). + Fields("a.id", "a.nickname", "b.nickname AS other_nickname"). + LeftJoin(table+" AS b", "a.id = b.id - 1"). + Where("a.id IN(?)", g.Slice{1, 2}). + Order("a.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "1") + t.Assert(r[0]["nickname"], "name_1") + t.Assert(r[0]["other_nickname"], "name_2") + t.Assert(r[1]["id"], "2") + t.Assert(r[1]["other_nickname"], "name_3") + }) +} + +// Test_Model_Join_LeftJoinNull tests LEFT JOIN NULL handling +func Test_Model_Join_LeftJoinNull(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + + // Create table2 with only partial data + createTable(table2) + defer dropTable(table2) + _, err := db.Insert(ctx, table2, g.List{ + {"id": 1, "passport": "user_1", "nickname": "name_1"}, + {"id": 2, "passport": "user_2", "nickname": "name_2"}, + }) + if err != nil { + gtest.Fatal(err) + } + + gtest.C(t, func(t *gtest.T) { + // LEFT JOIN - table1 has all records, table2 only has id 1,2 + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + FieldsPrefix("t2", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t1.id IN(?)", g.Slice{1, 2, 3}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[0]["nickname"], "name_1") // matched + t.Assert(r[1]["id"], "2") + t.Assert(r[1]["nickname"], "name_2") // matched + t.Assert(r[2]["id"], "3") + // r[2]["nickname"] should be NULL/empty from t2 + }) + + gtest.C(t, func(t *gtest.T) { + // Find records where RIGHT table is NULL + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t2.id IS NULL"). + Where("t1.id IN(?)", g.Slice{1, 2, 3, 4}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // Should return id 3,4 (not in table2) + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "3") + t.Assert(r[0]["nickname"], "name_3") + t.Assert(r[1]["id"], "4") + }) +} + +// Test_Model_Join_RightJoinNull tests RIGHT JOIN NULL handling +func Test_Model_Join_RightJoinNull(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + // table1 has partial data + createTable(table1) + defer dropTable(table1) + _, err := db.Insert(ctx, table1, g.List{ + {"id": 1, "passport": "user_1", "nickname": "name_1"}, + {"id": 2, "passport": "user_2", "nickname": "name_2"}, + }) + if err != nil { + gtest.Fatal(err) + } + + // table2 has all data + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + // RIGHT JOIN - table1 only has id 1,2, table2 has all + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t2", "id"). + FieldsPrefix("t1", "nickname"). + RightJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t2.id IN(?)", g.Slice{1, 2, 3}). + Order("t2.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[0]["nickname"], "name_1") // matched + t.Assert(r[1]["id"], "2") + t.Assert(r[1]["nickname"], "name_2") // matched + t.Assert(r[2]["id"], "3") + // r[2]["nickname"] should be NULL/empty from t1 + }) + + gtest.C(t, func(t *gtest.T) { + // Find records where LEFT table is NULL + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t2", "id", "nickname"). + RightJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t1.id IS NULL"). + Where("t2.id IN(?)", g.Slice{1, 2, 3, 4}). + Order("t2.id asc"). + All() + t.AssertNil(err) + + // Should return id 3,4 (not in table1) + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "3") + t.Assert(r[0]["nickname"], "name_3") + t.Assert(r[1]["id"], "4") + }) +} + +// Test_Model_Join_OnVsWhere tests difference between ON and WHERE conditions +func Test_Model_Join_OnVsWhere(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + // INNER JOIN: ON and WHERE behave the same + r1, err := db.Model(table1).As("t1"). + Fields("t1.id"). + InnerJoin(table2+" AS t2", "t1.id = t2.id AND t2.id <= 3"). + Order("t1.id asc"). + All() + t.AssertNil(err) + + r2, err := db.Model(table1).As("t1"). + Fields("t1.id"). + InnerJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t2.id <=", 3). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // For INNER JOIN, results should be identical + t.Assert(len(r1), 3) + t.Assert(len(r2), 3) + t.Assert(r1[0]["id"], r2[0]["id"]) + }) + + gtest.C(t, func(t *gtest.T) { + // LEFT JOIN: ON filter in join condition vs WHERE filter after join + // ON condition: filters t2 before join (keeps all t1 rows) + r1, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + FieldsPrefix("t2", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id AND t2.id <= 2"). + Where("t1.id <=", 4). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // WHERE condition: filters result after join (removes t1 rows where t2 is NULL) + r2, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + FieldsPrefix("t2", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t1.id <=", 4). + Where("t2.id <=", 2). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // r1: all t1 rows (1,2,3,4), t2 data only for id 1,2 + t.Assert(len(r1), 4) + t.Assert(r1[0]["id"], "1") + t.Assert(r1[0]["nickname"], "name_1") + t.Assert(r1[2]["id"], "3") + // r1[2]["nickname"] is NULL from t2 + + // r2: only rows where t2.id <= 2, so only id 1,2 + t.Assert(len(r2), 2) + t.Assert(r2[0]["id"], "1") + t.Assert(r2[1]["id"], "2") + }) +} + +// Test_Model_Join_ComplexConditions tests joins with complex ON conditions +func Test_Model_Join_ComplexConditions(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + // Multiple AND conditions in ON clause + r, err := db.Model(table1).As("t1"). + Fields("t1.id", "t1.nickname"). + InnerJoin( + table2+" AS t2", + "t1.id = t2.id AND t1.nickname = t2.nickname AND t1.id BETWEEN 2 AND 4", + ). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "2") + t.Assert(r[2]["id"], "4") + }) + + gtest.C(t, func(t *gtest.T) { + // OR conditions in ON clause (need to use Where for OR in join) + r, err := db.Model(table1).As("t1"). + Fields("t1.id"). + InnerJoin(table2+" AS t2", "t1.id = t2.id"). + Where("t2.id = 1 OR t2.id = 5"). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "5") + }) +} diff --git a/contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go b/contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go index 2159c0d97..a993727da 100644 --- a/contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_feature_model_subquery_test.go @@ -7,6 +7,7 @@ package mysql_test import ( + "fmt" "testing" "github.com/gogf/gf/v2/frame/g" @@ -64,3 +65,247 @@ func Test_Model_SubQuery_Model(t *testing.T) { t.Assert(r[0]["id"], 5) }) } + +// Test_Model_SubQuery_Correlated tests scalar subquery and correlated subquery with EXISTS +func Test_Model_SubQuery_Correlated(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Scalar subquery: find users whose id is greater than average id + subQuery := db.Model(table + " AS inner_table").Fields("AVG(id)") + r, err := db.Model(table).Where( + "id > (?)", + subQuery, + ).OrderAsc("id").All() + t.AssertNil(err) + + // Average of 1-10 is 5.5, so expect ids 6-10 + t.Assert(len(r), 5) + t.Assert(r[0]["id"], 6) + t.Assert(r[4]["id"], 10) + }) + + gtest.C(t, func(t *gtest.T) { + // Correlated subquery with EXISTS: find users with id matching their own id + r, err := db.Model(table+" AS outer_table"). + Where( + fmt.Sprintf("EXISTS (SELECT 1 FROM %s AS inner_table WHERE inner_table.id = outer_table.id AND inner_table.id <= ?)", table), + 3, + ). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 1) + t.Assert(r[2]["id"], 3) + }) +} + +// Test_Model_SubQuery_From tests subquery in FROM clause +func Test_Model_SubQuery_From(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Subquery in FROM clause + subQuery := db.Model(table).Where("id <=", 5) + r, err := db.Model("(?) AS sub", subQuery). + Fields("sub.id", "sub.nickname"). + Where("sub.id >", 2). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 3) + t.Assert(r[0]["nickname"], "name_3") + t.Assert(r[2]["id"], 5) + }) + + gtest.C(t, func(t *gtest.T) { + // Multiple subqueries in FROM clause with JOIN + subQuery1 := db.Model(table).Fields("id", "nickname").Where("id <=", 3) + subQuery2 := db.Model(table).Fields("id", "passport").Where("id >=", 3) + + r, err := db.Model("? AS a, ? AS b", subQuery1, subQuery2). + Fields("a.id", "a.nickname", "b.passport"). + Where("a.id = b.id"). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 3) + t.Assert(r[0]["nickname"], "name_3") + t.Assert(r[0]["passport"], "user_3") + }) +} + +// Test_Model_SubQuery_Select tests subquery in SELECT clause +func Test_Model_SubQuery_Select(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Subquery in SELECT clause for scalar value + r, err := db.Model(table). + Fields("id", "nickname", fmt.Sprintf("(SELECT MAX(id) FROM %s) AS max_id", table)). + Where("id", 1). + One() + t.AssertNil(err) + + t.Assert(r["id"], 1) + t.Assert(r["nickname"], "name_1") + t.Assert(r["max_id"], 10) + }) + + gtest.C(t, func(t *gtest.T) { + // Multiple subqueries in SELECT clause + r, err := db.Model(table). + Fields( + "id", + fmt.Sprintf("(SELECT MAX(id) FROM %s) AS max_id", table), + fmt.Sprintf("(SELECT MIN(id) FROM %s) AS min_id", table), + ). + Where("id", 5). + One() + t.AssertNil(err) + + t.Assert(r["id"], 5) + t.Assert(r["max_id"], 10) + t.Assert(r["min_id"], 1) + }) +} + +// Test_Model_SubQuery_Nested tests multi-level nested subqueries (3+ levels) +func Test_Model_SubQuery_Nested(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // 3-level nested subquery + // Level 3: innermost - get ids <= 8 + level3 := db.Model(table).Fields("id").Where("id <=", 8) + + // Level 2: middle - filter from level 3 where id >= 3 + level2 := db.Model("(?) AS l3", level3).Fields("l3.id").Where("l3.id >=", 3) + + // Level 1: outermost - filter from level 2 where id <= 6 + r, err := db.Model(table). + Where("id IN (?)", level2). + Where("id <=", 6). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 4) + t.Assert(r[0]["id"], 3) + t.Assert(r[3]["id"], 6) + }) + + gtest.C(t, func(t *gtest.T) { + // 4-level nested subquery with aggregates + // Level 4: get all ids + level4 := db.Model(table).Fields("id") + + // Level 3: get ids > 5 from level 4 + level3 := db.Model("(?) AS l4", level4).Fields("l4.id").Where("l4.id >", 5) + + // Level 2: get MIN(id) from level 3 + level2 := db.Model("(?) AS l3", level3).Fields("MIN(l3.id)") + + // Level 1: find records >= the minimum from level 2 + r, err := db.Model(table). + Where("id >= (?)", level2). + OrderAsc("id"). + All() + t.AssertNil(err) + + // MIN(id) from level 3 should be 6, so expect ids 6-10 + t.Assert(len(r), 5) + t.Assert(r[0]["id"], 6) + t.Assert(r[4]["id"], 10) + }) +} + +// Test_Model_SubQuery_WhereIn tests subquery with WHERE IN +func Test_Model_SubQuery_WhereIn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Simple WHERE IN with subquery + subQuery := db.Model(table).Fields("id").Where("id IN(?)", g.Slice{2, 4, 6}) + r, err := db.Model(table). + Where("id IN(?)", subQuery). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 2) + t.Assert(r[1]["id"], 4) + t.Assert(r[2]["id"], 6) + }) + + gtest.C(t, func(t *gtest.T) { + // Multiple WHERE IN subqueries combined + subQuery1 := db.Model(table).Fields("id").Where("id <=", 5) + subQuery2 := db.Model(table).Fields("id").Where("id >=", 3) + + r, err := db.Model(table). + Where("id IN(?)", subQuery1). + Where("id IN(?)", subQuery2). + OrderAsc("id"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 3) + t.Assert(r[2]["id"], 5) + }) +} + +// Test_Model_SubQuery_Complex tests complex subquery combinations +func Test_Model_SubQuery_Complex(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Combine subquery in WHERE, FROM, and SELECT + whereSubQuery := db.Model(table).Fields("AVG(id)") + fromSubQuery := db.Model(table).Where("id <=", 7) + + r, err := db.Model("(?) AS sub", fromSubQuery). + Fields("sub.id", "sub.nickname"). + Where("sub.id > (?)", whereSubQuery). + OrderAsc("id"). + All() + t.AssertNil(err) + + // AVG(1-10) = 5.5, filter sub.id > 5.5 from ids 1-7 + t.Assert(len(r), 2) + t.Assert(r[0]["id"], 6) + t.Assert(r[1]["id"], 7) + }) + + gtest.C(t, func(t *gtest.T) { + // Subquery with GROUP BY and HAVING + subQuery := db.Model(table). + Fields("id % 3 AS mod_group", "COUNT(*) AS cnt"). + Group("mod_group"). + Having("COUNT(*) >=", 3) + + r, err := db.Model(table). + Where("id % 3 IN(?)", db.Model("(?) AS sub", subQuery).Fields("sub.mod_group")). + OrderAsc("id"). + All() + t.AssertNil(err) + + // id % 3: 0(3,6,9), 1(1,4,7,10), 2(2,5,8) + // Groups with count >= 3: 0(3 items), 1(4 items), 2(3 items) - all qualify + t.Assert(len(r), 10) + }) +} diff --git a/contrib/drivers/mysql/mysql_z_unit_model_where_test.go b/contrib/drivers/mysql/mysql_z_unit_model_where_test.go index c0007c688..40436f90d 100644 --- a/contrib/drivers/mysql/mysql_z_unit_model_where_test.go +++ b/contrib/drivers/mysql/mysql_z_unit_model_where_test.go @@ -1242,3 +1242,463 @@ CREATE TABLE %s ( t.Assert(r[0]["id"].Int(), 1) // Should include id 1 }) } + +// Test_Model_WherePrefixIn tests WherePrefix with IN clause +func Test_Model_WherePrefixIn(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixIn("t1", "id", g.Slice{1, 2, 3}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[2]["id"], "3") + }) +} + +// Test_Model_WherePrefixNotIn tests WherePrefix with NOT IN clause +func Test_Model_WherePrefixNotIn(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixNotIn("t1", "id", g.Slice{1, 2, 3, 4, 5, 6, 7}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "8") + t.Assert(r[2]["id"], "10") + }) +} + +// Test_Model_WherePrefixBetween tests WherePrefix with BETWEEN clause +func Test_Model_WherePrefixBetween(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixBetween("t1", "id", 3, 6). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 4) + t.Assert(r[0]["id"], "3") + t.Assert(r[3]["id"], "6") + }) +} + +// Test_Model_WherePrefixNotBetween tests WherePrefix with NOT BETWEEN clause +func Test_Model_WherePrefixNotBetween(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixNotBetween("t1", "id", 3, 7). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // NOT BETWEEN 3 AND 7 returns: 1, 2, 8, 9, 10 (5 records) + t.Assert(len(r), 5) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + t.Assert(r[2]["id"], "8") + t.Assert(r[3]["id"], "9") + t.Assert(r[4]["id"], "10") + }) +} + +// Test_Model_WherePrefixLT tests WherePrefix with < operator +func Test_Model_WherePrefixLT(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixLT("t1", "id", 4). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "1") + t.Assert(r[2]["id"], "3") + }) +} + +// Test_Model_WherePrefixLTE tests WherePrefix with <= operator +func Test_Model_WherePrefixLTE(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixLTE("t1", "id", 4). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 4) + t.Assert(r[0]["id"], "1") + t.Assert(r[3]["id"], "4") + }) +} + +// Test_Model_WherePrefixGT tests WherePrefix with > operator +func Test_Model_WherePrefixGT(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixGT("t1", "id", 7). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "8") + t.Assert(r[2]["id"], "10") + }) +} + +// Test_Model_WherePrefixGTE tests WherePrefix with >= operator +func Test_Model_WherePrefixGTE(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixGTE("t1", "id", 7). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 4) + t.Assert(r[0]["id"], "7") + t.Assert(r[3]["id"], "10") + }) +} + +// Test_Model_WherePrefixNotLike tests WherePrefix with NOT LIKE operator +func Test_Model_WherePrefixNotLike(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id", "nickname"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixNotLike("t1", "nickname", "name_1%"). + Where("t1.id <", 5). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // Should exclude name_1 and name_10 (but id 10 filtered by WHERE anyway) + t.Assert(len(r), 3) + t.Assert(r[0]["id"], "2") + t.Assert(r[0]["nickname"], "name_2") + t.Assert(r[2]["id"], "4") + }) +} + +// Test_Model_WherePrefixNull tests WherePrefix with IS NULL +func Test_Model_WherePrefixNull(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + // table2 has partial data, will cause NULLs in LEFT JOIN + createTable(table2) + defer dropTable(table2) + _, _ = db.Insert(ctx, table2, g.List{ + {"id": 1, "passport": "user_1", "nickname": "name_1"}, + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixNull("t2", "nickname"). + Where("t1.id <=", 3). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // t2 only has id=1, so id 2,3 should have NULL in t2.nickname + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "2") + t.Assert(r[1]["id"], "3") + }) +} + +// Test_Model_WherePrefixNotNull tests WherePrefix with IS NOT NULL +func Test_Model_WherePrefixNotNull(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + // table2 has partial data + createTable(table2) + defer dropTable(table2) + _, _ = db.Insert(ctx, table2, g.List{ + {"id": 1, "passport": "user_1", "nickname": "name_1"}, + {"id": 2, "passport": "user_2", "nickname": "name_2"}, + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WherePrefixNotNull("t2", "nickname"). + Where("t1.id <=", 4). + Order("t1.id asc"). + All() + t.AssertNil(err) + + // t2 has id 1,2, so only these should have NOT NULL in t2.nickname + t.Assert(len(r), 2) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + }) +} + +// Test_Model_WhereOrPrefixIn tests WhereOrPrefix with IN clause +func Test_Model_WhereOrPrefixIn(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WhereOrPrefixIn("t1", "id", g.Slice{1, 2}). + WhereOrPrefixIn("t2", "id", g.Slice{8, 9}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 4) + t.Assert(r[0]["id"], "1") + t.Assert(r[1]["id"], "2") + t.Assert(r[2]["id"], "8") + t.Assert(r[3]["id"], "9") + }) +} + +// Test_Model_WhereOrPrefixNotIn tests WhereOrPrefix with NOT IN clause +func Test_Model_WhereOrPrefixNotIn(t *testing.T) { + var ( + table1 = gtime.TimestampNanoStr() + "_table1" + table2 = gtime.TimestampNanoStr() + "_table2" + ) + createInitTable(table1) + defer dropTable(table1) + createInitTable(table2) + defer dropTable(table2) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table1).As("t1"). + FieldsPrefix("t1", "id"). + LeftJoin(table2+" AS t2", "t1.id = t2.id"). + WhereOrPrefixNotIn("t1", "id", g.Slice{1}). + WhereOrPrefixNotIn("t2", "id", g.Slice{2, 3, 4, 5, 6, 7, 8, 9, 10}). + Order("t1.id asc"). + All() + t.AssertNil(err) + + t.Assert(len(r), 10) // All records match one OR condition + t.Assert(r[0]["id"], "1") + }) +} + +// Test_Model_Having_Aggregate tests HAVING clause with aggregate functions +func Test_Model_Having_Aggregate(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // HAVING with COUNT + r, err := db.Model(table). + Fields("id % 3 AS mod_group", "COUNT(*) AS cnt"). + Group("mod_group"). + Having("COUNT(*) >= ?", 3). + Order("mod_group asc"). + All() + t.AssertNil(err) + + // mod 0: 3,6,9 (3 items) + // mod 1: 1,4,7,10 (4 items) + // mod 2: 2,5,8 (3 items) + t.Assert(len(r), 3) + t.Assert(r[0]["mod_group"], "0") + t.Assert(r[0]["cnt"], "3") + t.Assert(r[1]["cnt"], "4") + }) + + gtest.C(t, func(t *gtest.T) { + // HAVING with SUM + r, err := db.Model(table). + Fields("id % 2 AS parity", "SUM(id) AS total"). + Group("parity"). + Having("SUM(id) > ?", 25). + Order("parity asc"). + All() + t.AssertNil(err) + + // even (2,4,6,8,10): sum=30 + // odd (1,3,5,7,9): sum=25 + t.Assert(len(r), 1) + t.Assert(r[0]["parity"], "0") + t.Assert(r[0]["total"], "30") + }) + + gtest.C(t, func(t *gtest.T) { + // HAVING with AVG + r, err := db.Model(table). + Fields("id DIV 5 AS group_key", "AVG(id) AS avg_id"). + Group("group_key"). + Having("AVG(id) >= ?", 5). + Order("group_key asc"). + All() + t.AssertNil(err) + + // group 0 (id 1-4): avg=2.5 + // group 1 (id 5-9): avg=7 + // group 2 (id 10): avg=10 + t.Assert(len(r), 2) + t.Assert(r[0]["group_key"], "1") + }) +} + +// Test_Model_Having_MultipleConditions tests HAVING with multiple conditions +func Test_Model_Having_MultipleConditions(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Multiple HAVING conditions + r, err := db.Model(table). + Fields("id % 3 AS mod_group", "COUNT(*) AS cnt", "SUM(id) AS total"). + Group("mod_group"). + Having("COUNT(*) >= ?", 3). + Having("SUM(id) < ?", 30). + Order("mod_group asc"). + All() + t.AssertNil(err) + + // mod 0: cnt=3, sum=18 (3+6+9) ✓ + // mod 1: cnt=4, sum=22 (1+4+7+10) ✓ + // mod 2: cnt=3, sum=15 (2+5+8) ✓ + t.Assert(len(r), 3) + }) + + gtest.C(t, func(t *gtest.T) { + // HAVING with complex expression + r, err := db.Model(table). + Fields("id DIV 3 AS group_key", "MAX(id) - MIN(id) AS range_val"). + Group("group_key"). + Having("MAX(id) - MIN(id) >= ?", 2). + Order("group_key asc"). + All() + t.AssertNil(err) + + // group 0 (1,2): range=1 + // group 1 (3,4,5): range=2 ✓ + // group 2 (6,7,8): range=2 ✓ + // group 3 (9,10): range=1 + t.Assert(len(r), 2) + }) +}