| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- package vrtxsql
- // JOIN 支持测试: INNER/LEFT [OUTER] JOIN、链式多表、限定名列、WHERE 下推与
- // 残余谓词 (跨表列比较 / 跨表 OR)、* 投影与重名列去重、歧义列/非法 ON 报错。
- // 全部走标准 database/sql 全链路, 数据与断言一一对应 join.go 的执行策略。
- import (
- "database/sql"
- "strings"
- "testing"
- )
- // joinFixture 建 users/orders/refunds 三张表并灌入固定数据:
- //
- // users: 1 alice 30 / 2 bob 25 / 3 carl 40
- // orders: 10 ->u1 99.5 book / 11 ->u1 20 pen / 12 ->u2 7.5 cup
- // refunds: 100 ->o10 broken / 101 ->o10 late
- func joinFixture(t *testing.T, db *sql.DB) {
- t.Helper()
- exec := func(q string, args ...interface{}) {
- t.Helper()
- if _, err := db.Exec(q, args...); err != nil {
- t.Fatalf("%s: %v", q, err)
- }
- }
- exec(`CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(32) NOT NULL, age INT)`)
- exec(`CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, amount DOUBLE, title VARCHAR(32))`)
- exec(`CREATE TABLE refunds (id BIGINT PRIMARY KEY, order_id BIGINT, reason VARCHAR(32))`)
- exec(`INSERT INTO users (id, name, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)`,
- 1, "alice", 30, 2, "bob", 25, 3, "carl", 40)
- exec(`INSERT INTO orders (id, user_id, amount, title) VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)`,
- 10, 1, 99.5, "book", 11, 1, 20.0, "pen", 12, 2, 7.5, "cup")
- exec(`INSERT INTO refunds (id, order_id, reason) VALUES (?, ?, ?), (?, ?, ?)`,
- 100, 10, "broken", 101, 10, "late")
- }
- type joinHit struct {
- name, title string
- amount float64
- }
- // joinRows 执行 JOIN 查询并收集 (name, title, amount) 三元组。
- func joinRows(t *testing.T, db *sql.DB, query string, args ...interface{}) []joinHit {
- t.Helper()
- rows, err := db.Query(query, args...)
- if err != nil {
- t.Fatalf("query: %v", err)
- }
- defer rows.Close()
- var got []joinHit
- for rows.Next() {
- var h joinHit
- var name, title []byte
- if err := rows.Scan(&name, &title, &h.amount); err != nil {
- t.Fatalf("scan: %v", err)
- }
- h.name, h.title = string(name), string(title)
- got = append(got, h)
- }
- if err := rows.Err(); err != nil {
- t.Fatal(err)
- }
- return got
- }
- // TestJoinInner INNER JOIN 命中: users/orders 按 user_id 关联, 验证行数与列值。
- func TestJoinInner(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- rows, err := db.Query(`SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id ASC`)
- if err != nil {
- t.Fatalf("inner join: %v", err)
- }
- cols, err := rows.Columns()
- if err != nil {
- t.Fatal(err)
- }
- if strings.Join(cols, ",") != "name,title,amount" {
- t.Fatalf("列名=%v, 想要 [name title amount]", cols)
- }
- rows.Close()
- got := joinRows(t, db, `SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.id ASC`)
- want := []joinHit{{"alice", "book", 99.5}, {"alice", "pen", 20}, {"bob", "cup", 7.5}}
- if len(got) != len(want) {
- t.Fatalf("行数=%d, 想要 %d (%+v)", len(got), len(want), got)
- }
- for i := range want {
- if got[i] != want[i] {
- t.Fatalf("行 %d = %+v, 想要 %+v", i, got[i], want[i])
- }
- }
- // INNER JOIN 显式关键字 + 表名限定 (不写别名)
- got = joinRows(t, db, `SELECT users.name, orders.title, orders.amount FROM users INNER JOIN orders ON users.id = orders.user_id ORDER BY orders.id ASC`)
- if len(got) != len(want) {
- t.Fatalf("INNER JOIN 行数=%d, 想要 %d (%+v)", len(got), len(want), got)
- }
- }
- // TestJoinLeftNullRow LEFT OUTER JOIN 右表无匹配: 右列全 NULL 保留行。
- func TestJoinLeftNullRow(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- rows, err := db.Query(`SELECT u.name, o.title FROM users u LEFT OUTER JOIN orders o ON u.id = o.user_id ORDER BY u.id ASC, o.id ASC`)
- if err != nil {
- t.Fatalf("left join: %v", err)
- }
- defer rows.Close()
- type row struct {
- name string
- title sql.NullString
- }
- var got []row
- for rows.Next() {
- var r row
- var name []byte
- if err := rows.Scan(&name, &r.title); err != nil {
- t.Fatalf("scan: %v", err)
- }
- r.name = string(name)
- got = append(got, r)
- }
- if err := rows.Err(); err != nil {
- t.Fatal(err)
- }
- if len(got) != 4 {
- t.Fatalf("行数=%d, 想要 4 (%+v)", len(got), got)
- }
- if got[3].name != "carl" || got[3].title.Valid {
- t.Fatalf("carl 应为右表无匹配 (title NULL), 实际 %+v", got[3])
- }
- if got[0].title.String != "book" || got[1].title.String != "pen" || got[2].title.String != "cup" {
- t.Fatalf("匹配行不对: %+v", got)
- }
- }
- // TestJoinWhereOrderLimit JOIN + WHERE(限定名) + ORDER BY + LIMIT/OFFSET。
- func TestJoinWhereOrderLimit(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- // u.age 下推 users 表, o.amount 下推 orders 表
- got := joinRows(t, db,
- `SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id
- WHERE u.age >= ? AND o.amount > ? ORDER BY o.amount DESC LIMIT 1`, 26, 50)
- want := []joinHit{{"alice", "book", 99.5}}
- if len(got) != 1 || got[0] != want[0] {
- t.Fatalf("got %+v, 想要 %+v", got, want)
- }
- // 非限定列名 (唯一归属 age 在 users) + LIMIT offset,count 写法
- // (age>=26 只剩 alice: ASC 序 [pen 20, book 99.5], LIMIT 1,1 = 跳过 1 行取 1 行)
- got = joinRows(t, db,
- `SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id
- WHERE age >= ? ORDER BY o.amount ASC LIMIT 1, 1`, 26)
- if len(got) != 1 || got[0] != (joinHit{"alice", "book", 99.5}) {
- t.Fatalf("LIMIT offset,count got %+v, 想要 [{alice book 99.5}]", got)
- }
- // OFFSET 关键字写法 (ASC 序 [cup 7.5, pen 20, book 99.5], 跳 1 行取 1 行)
- got = joinRows(t, db,
- `SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id
- ORDER BY o.amount ASC LIMIT 1 OFFSET 1`)
- if len(got) != 1 || got[0] != (joinHit{"alice", "pen", 20}) {
- t.Fatalf("OFFSET got %+v, 想要 [{alice pen 20}]", got)
- }
- }
- // TestJoinThreeTables 3 表链式 JOIN: users → orders → refunds。
- func TestJoinThreeTables(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- rows, err := db.Query(
- `SELECT u.name, o.title, r.reason FROM users u
- JOIN orders o ON u.id = o.user_id
- JOIN refunds r ON o.id = r.order_id
- ORDER BY r.id ASC`)
- if err != nil {
- t.Fatalf("3 表 join: %v", err)
- }
- defer rows.Close()
- var got [][2]string
- for rows.Next() {
- var name, title, reason []byte
- if err := rows.Scan(&name, &title, &reason); err != nil {
- t.Fatalf("scan: %v", err)
- }
- got = append(got, [2]string{string(name) + "/" + string(title), string(reason)})
- }
- if err := rows.Err(); err != nil {
- t.Fatal(err)
- }
- want := [][2]string{{"alice/book", "broken"}, {"alice/book", "late"}}
- if len(got) != len(want) {
- t.Fatalf("行数=%d, 想要 %d (%+v)", len(got), len(want), got)
- }
- for i := range want {
- if got[i] != want[i] {
- t.Fatalf("行 %d = %+v, 想要 %+v", i, got[i], want[i])
- }
- }
- }
- // TestJoinStarAndDedup * 投影跨表展开, 重名列 (id) 用 别名_列名 去重。
- func TestJoinStarAndDedup(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- rs, err := db.Query(`SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = 1 ORDER BY o.id ASC`)
- if err != nil {
- t.Fatalf("star join: %v", err)
- }
- defer rs.Close()
- cols, err := rs.Columns()
- if err != nil {
- t.Fatal(err)
- }
- wantCols := "id,name,age,o_id,user_id,amount,title"
- if strings.Join(cols, ",") != wantCols {
- t.Fatalf("列=%v, 想要 %v", cols, wantCols)
- }
- n := 0
- for rs.Next() {
- vals := make([]interface{}, len(cols))
- ptrs := make([]interface{}, len(cols))
- for i := range vals {
- ptrs[i] = &vals[i]
- }
- if err := rs.Scan(ptrs...); err != nil {
- t.Fatalf("scan: %v", err)
- }
- if n == 0 {
- // 第一行: alice 的 book 订单
- if vals[0] != int64(1) || string(vals[1].([]byte)) != "alice" || vals[2] != int64(30) ||
- vals[3] != int64(10) || vals[4] != int64(1) || vals[5] != float64(99.5) ||
- string(vals[6].([]byte)) != "book" {
- t.Fatalf("行值不对: %#v", vals)
- }
- }
- n++
- }
- if err := rs.Err(); err != nil {
- t.Fatal(err)
- }
- if n != 2 {
- t.Fatalf("行数=%d, 想要 2", n)
- }
- // t.* 与限定列混用 (u.* = id,name,age 三列 + o.amount)
- rs2, err := db.Query(`SELECT u.*, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.id = 12`)
- if err != nil {
- t.Fatalf("t.* 混用: %v", err)
- }
- defer rs2.Close()
- if !rs2.Next() {
- t.Fatal("t.* 混用无行")
- }
- var id, age int64
- var uname []byte
- var amount float64
- if err := rs2.Scan(&id, &uname, &age, &amount); err != nil {
- t.Fatalf("scan: %v", err)
- }
- if id != 2 || string(uname) != "bob" || age != 25 || amount != 7.5 {
- t.Fatalf("t.* 混用行 = %d,%s,%d,%v", id, uname, age, amount)
- }
- }
- // TestJoinResidualPredicate 跨表条件作为残余谓词在组合行上过滤:
- // 列与列比较 (o.amount > u.age) 与跨表 OR (u.name = ? OR o.amount > ?)。
- func TestJoinResidualPredicate(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- // 列比较: alice 99.5>30 命中, 20>30 / 7.5>25 不命中
- got := joinRows(t, db,
- `SELECT u.name, o.title, o.amount FROM users u JOIN orders o ON u.id = o.user_id
- WHERE o.amount > u.age ORDER BY o.id ASC`)
- want := []joinHit{{"alice", "book", 99.5}}
- if len(got) != len(want) || got[0] != want[0] {
- t.Fatalf("列比较 got %+v, 想要 %+v", got, want)
- }
- // 跨表 OR (结构条件无法下推): alice 两行 + amount>50 的 alice/book
- got = joinRows(t, db,
- `SELECT u.name, o.title, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id
- WHERE u.name = ? OR o.amount > ? ORDER BY o.id ASC`, "alice", 50)
- want = []joinHit{{"alice", "book", 99.5}, {"alice", "pen", 20}}
- if len(got) != len(want) {
- t.Fatalf("跨表 OR 行数=%d, 想要 %d (%+v)", len(got), len(want), got)
- }
- for i := range want {
- if got[i] != want[i] {
- t.Fatalf("行 %d = %+v, 想要 %+v", i, got[i], want[i])
- }
- }
- }
- // TestJoinErrors 歧义列 / 非法 ON / 未知限定的明确报错。
- func TestJoinErrors(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- // SELECT 非限定歧义列 (users/orders 都有 id)
- if _, err := db.Query(`SELECT id FROM users u JOIN orders o ON u.id = o.user_id`); err == nil ||
- !strings.Contains(err.Error(), "不明确") {
- t.Fatalf("SELECT 歧义列应报 不明确, 实际 %v", err)
- }
- // WHERE 非限定歧义列 (age 只属 users, id 两表都有)
- if _, err := db.Query(`SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE id > 10`); err == nil ||
- !strings.Contains(err.Error(), "不明确") {
- t.Fatalf("WHERE 歧义列应报 不明确, 实际 %v", err)
- }
- // ON 非等值
- if _, err := db.Query(`SELECT * FROM users u JOIN orders o ON u.id > o.user_id`); err == nil ||
- !strings.Contains(err.Error(), "JOIN ON 仅支持等值条件") {
- t.Fatalf("ON 非等值应报错, 实际 %v", err)
- }
- // ON 未知限定
- if _, err := db.Query(`SELECT * FROM users u JOIN orders o ON x.id = o.user_id`); err == nil {
- t.Fatalf("ON 未知限定应报错")
- }
- // ON 列不存在
- if _, err := db.Query(`SELECT * FROM users u JOIN orders o ON u.nope = o.user_id`); err == nil {
- t.Fatalf("ON 未知列应报错")
- }
- // JOIN 与聚合同用
- if _, err := db.Query(`SELECT COUNT(*) FROM users u JOIN orders o ON u.id = o.user_id`); err == nil {
- t.Fatalf("JOIN+聚合应报错")
- }
- }
- // TestJoinQualifiedSingleTable 单表查询的限定名列/t.* (兼容路径, 解限定后走标准翻译)。
- func TestJoinQualifiedSingleTable(t *testing.T) {
- db := openTestDB(t)
- joinFixture(t, db)
- var name []byte
- if err := db.QueryRow(`SELECT u.name FROM users u WHERE u.age > ? ORDER BY u.id LIMIT 1`, 26).Scan(&name); err != nil {
- t.Fatalf("限定名 WHERE: %v", err)
- }
- if string(name) != "alice" {
- t.Fatalf("name=%q, 想要 alice", name)
- }
- if err := db.QueryRow(`SELECT name FROM users AS u WHERE u.age >= ? ORDER BY u.id DESC LIMIT 1`, 26).Scan(&name); err != nil {
- t.Fatalf("AS 别名 + 限定名: %v", err)
- }
- if string(name) != "carl" {
- t.Fatalf("name=%q, 想要 carl", name)
- }
- // t.* 单表展开 + 聚合限定列
- var age int64
- if err := db.QueryRow(`SELECT COUNT(u.id) FROM users u WHERE u.age > ?`, 26).Scan(&age); err != nil {
- t.Fatalf("聚合限定列: %v", err)
- }
- if age != 2 {
- t.Fatalf("count=%d, 想要 2", age)
- }
- // 限定词不是本表 → 明确报错
- if _, err := db.Query(`SELECT o.name FROM users u WHERE u.age > 1`); err == nil {
- t.Fatalf("限定词不匹配应报错")
- }
- }
|