package dailyusage import ( "context" "reflect" "sort" "strings" "testing" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao" ) // SQLite runs the SAME predicate and ordering with only the two MySQL hints // removed. This checks full eligibility/TaskID parity, not MySQL access plans. // Production EXPLAIN must still validate the unmodified channelPageSQL. type sqliteChannelScanner struct{ DatabaseScanner } func (s *sqliteChannelScanner) ScanChannel(ctx context.Context, key string, last int64, size int) ([]Card, error) { query := strings.ReplaceAll(channelPageSQL, "FORCE INDEX (idx_card_chan_key)", "") query = strings.ReplaceAll(query, "STRAIGHT_JOIN", "INNER JOIN") var rows []struct { ID int64 `orm:"id"` ICCID string `orm:"iccid"` MSISDN string `orm:"msisdn"` IMSI string `orm:"imsi"` ChanKey string `orm:"chan_key"` PackageCode string `orm:"current_package_code"` } if err := dao.TSimCardInfo.DB().Raw(query, key, last, size).Ctx(ctx).Scan(&rows); err != nil { return nil, err } cards := make([]Card, 0, len(rows)) for _, row := range rows { cards = append(cards, Card{ID: row.ID, ICCID: row.ICCID, MSISDN: row.MSISDN, IMSI: row.IMSI, ChanKey: row.ChanKey, PackageCode: row.PackageCode}) } return cards, nil } func TestDatabaseQualificationAndCompleteTaskIDParity(t *testing.T) { // No application DB/config: isolated SQLite file and loopback Redis only. if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(" + t.TempDir() + "/cards.db)"}}}); err != nil { t.Fatal(err) } ctx := contextForTest(t) db := dao.TSimCardInfo.DB() t.Cleanup(func() { _ = db.Close(context.Background()) }) for _, sql := range []string{ `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, deleted_at DATETIME)`, `CREATE TABLE t_sim_card_info (id INTEGER PRIMARY KEY, iccid TEXT, msisdn TEXT, imsi TEXT, chan_key TEXT, current_package_code TEXT, dept_id INTEGER, tenant_id TEXT)`, `CREATE INDEX idx_card_chan_key ON t_sim_card_info(chan_key)`, `CREATE TABLE t_sim_channels (chan_key TEXT, temp_key TEXT, sync_enabled INTEGER, operator INTEGER)`, `INSERT INTO sys_dept VALUES (1,'t1',NULL),(2,'t2',NULL),(3,'t1','2026-01-01')`, `INSERT INTO t_sim_channels VALUES ('a','sim_yd_v5',0,1),('b','sim_dx_v1',0,2),('disabled','sim_yd_v5',1,1),('no-template','',0,1)`, `INSERT INTO t_sim_card_info VALUES (1,'i1','m1','s1','a','pkg',1,'t1'), (2,'i2','m2','s2','b','pkg',2,'t2'), (3,'i3','','','a','pkg',1,'t1'), (4,'','','','a','pkg',1,'t1'), (5,'i5','','','a','',1,'t1'), (6,'i6','','','a','pkg',1,'wrong-tenant'), (7,'i7','','','a','pkg',3,'t1'), (8,'i8','','','b','pkg',99,'t2'), (9,'i9','','','disabled','pkg',1,'t1'), (10,'i10','','','no-template','pkg',1,'t1'), (11,'i11','','','unknown','pkg',1,'t1'), (12,'i12','','','','pkg',1,'t1'), (13,'i13','','','b','pkg',2,'t2')`, } { if _, err := db.Exec(ctx, sql); err != nil { t.Fatal(err) } } store := testStore(t) for _, scope := range [][]string{nil, {"a"}, {"b", "disabled", "no-template", "unknown"}, {"unknown"}} { var sets [][]string for _, mode := range []string{"serial", "channel"} { scanner := &sqliteChannelScanner{} p := testProducer(t, scanner, store, Config{ScanMode: mode, PageSize: 2}) run := "db-" + mode if err := p.Produce(ctx, scopedMessage(run, scope...)); err != nil { t.Fatal(err) } ids := taskIDs(t, store, run) for i, id := range ids { ids[i] = strings.TrimPrefix(id, run+":") } sort.Strings(ids) sets = append(sets, ids) stateKey, _ := store.TaskStateKey(run) metaKey, _ := store.RunMetaKey(run) chanKey, _ := store.RunChannelsKey(run) _, _ = store.RedisClient().Do(ctx, "DEL", stateKey, metaKey, chanKey) for _, key := range []string{"a", "b"} { activeKey, _ := store.RoundActiveKey(run, 1, key) _, _ = store.RedisClient().Do(ctx, "DEL", activeKey) } _, _ = store.RedisClient().Do(ctx, "SREM", store.ActiveRunsKey(), run) } if !reflect.DeepEqual(sets[0], sets[1]) { t.Fatalf("scope=%v serial=%v channel=%v", scope, sets[0], sets[1]) } if scope == nil { want := []string{operation + ":1", operation + ":13", operation + ":2", operation + ":3"} sort.Strings(want) if !reflect.DeepEqual(sets[0], want) { t.Fatalf("qualification set=%v want=%v", sets[0], want) } } } } func TestChannelSQLShapeAndParameters(t *testing.T) { for _, term := range []string{"FORCE INDEX (idx_card_chan_key)", "STRAIGHT_JOIN sys_dept", "c.chan_key = ?", "c.id > ?", "c.iccid <> ''", "c.current_package_code", "c.current_package_code <> ''", "d.deleted_at IS NULL", "c.tenant_id = d.tenant_id", "ORDER BY c.id ASC", "LIMIT ?"} { if !strings.Contains(channelPageSQL, term) { t.Fatalf("missing %q", term) } } if strings.Contains(channelPageSQL, "<=") || strings.Count(channelPageSQL, "?") != 3 { t.Fatal("SQL contains upper bound or changed bind parameter count") } }