package languages import ( "bytes" "fmt" "strings" "testing" "github.com/stretchr/testify/require" "github.com/zzet/gortex/internal/graph" ) const sampleMyBatisMapper = ` INSERT INTO users (name) VALUES (#{name}) UPDATE users SET name = #{name} id = #{id} DELETE FROM users WHERE id = #{id} id, name ` func TestMyBatisExtractor_StatementNodes(t *testing.T) { res, err := NewMyBatisExtractor().Extract("UserMapper.xml", []byte(sampleMyBatisMapper)) require.NoError(t, err) var file *graph.Node stmts := map[string]*graph.Node{} for _, n := range res.Nodes { switch n.Kind { case graph.KindFile: file = n case graph.KindMethod: stmts[n.ID] = n } } require.NotNil(t, file) require.Equal(t, "com.app.UserMapper", file.Meta["mybatis_namespace"]) // One node per SELECT 1`)) require.NoError(t, err) // never a hard failure require.NotEmpty(t, res.Nodes) // at least the file node survives } func TestMyBatisExtractor_Extensions(t *testing.T) { require.Equal(t, []string{".xml"}, NewMyBatisExtractor().Extensions()) } func TestMyBatisExtractor_SignatureString(t *testing.T) { src := []byte(` id, name `) res, err := NewMyBatisExtractor().Extract("UserMapper.xml", src) require.NoError(t, err) sigs := map[string]string{} for _, n := range res.Nodes { if n.Meta != nil { if s, ok := n.Meta["signature"].(string); ok { sigs[n.Name] = s } } } require.Equal(t, "SELECT param=Long result=User", sigs["findUser"]) require.Equal(t, "SELECT result=userMap", sigs["byMap"]) // resultMap fallback require.Equal(t, "", sigs["cols"]) // The structured mybatis_* keys remain intact alongside the signature. for _, n := range res.Nodes { if n.Name == "findUser" { require.Equal(t, "select", n.Meta["mybatis_sql_kind"]) require.Equal(t, "Long", n.Meta["mybatis_parameter_type"]) require.Equal(t, "User", n.Meta["mybatis_result_type"]) } } } func TestMyBatisExtractor_LineNumbersMultiStatement(t *testing.T) { // Line numbers come from the precomputed line-start table + binary search; // assert they match the source positions exactly across several elements. src := []byte("\n" + // line 1 "\n" + // line 2 "\n" + // line 3 " id, name\n" + // line 4 " \n" + // line 5 " INSERT\n" + // line 6 "\n") res, err := NewMyBatisExtractor().Extract("M.xml", src) require.NoError(t, err) lines := map[string]int{} for _, n := range res.Nodes { if n.Kind == graph.KindMethod || n.Kind == graph.KindFunction { lines[n.Name] = n.StartLine } } require.Equal(t, 4, lines["cols"]) require.Equal(t, 5, lines["a"]) require.Equal(t, 6, lines["b"]) } // BenchmarkMyBatisLineResolution contrasts resolving every element offset to a // line via the precomputed line-start table + binary search (one O(n) scan, // then O(log n) per offset) against the previous per-element full-prefix // newline count (O(offset) per element → O(n·m) overall). The "precomputed" // sub-benchmark is asymptotically faster and allocation-free per offset. func BenchmarkMyBatisLineResolution(b *testing.B) { var sb strings.Builder for i := 0; i < 20000; i++ { fmt.Fprintf(&sb, " \n", i) } src := []byte(sb.String()) var offs []int for i, c := range src { if c == '\n' { offs = append(offs, i) } } b.Run("precomputed_binary_search", func(b *testing.B) { b.ReportAllocs() for n := 0; n < b.N; n++ { starts := lineStartOffsets(src) sum := 0 for _, off := range offs { sum += lineForOffset(starts, off) } _ = sum } }) b.Run("per_element_prefix_count", func(b *testing.B) { b.ReportAllocs() for n := 0; n < b.N; n++ { sum := 0 for _, off := range offs { sum += 1 + bytes.Count(src[:off], []byte{'\n'}) } _ = sum } }) }