summaryrefslogtreecommitdiff
path: root/internal/mapr
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-10 20:01:15 +0200
committerPaul Buetow <paul@buetow.org>2026-03-10 20:01:15 +0200
commit71e431af2e65196ad4e7bc3404e772b1726d3338 (patch)
tree935e3b1af7c5b0033caf13ea00cf9655686b6ec9 /internal/mapr
parentf6e23930da2900c43a5389a2e7d1e38d8221a76f (diff)
Introduce client runtime boundaries
Diffstat (limited to 'internal/mapr')
-rw-r--r--internal/mapr/globalgroupset.go4
-rw-r--r--internal/mapr/groupsetresult.go101
-rw-r--r--internal/mapr/groupsetresult_renderer_test.go113
-rw-r--r--internal/mapr/result_renderer.go34
4 files changed, 177 insertions, 75 deletions
diff --git a/internal/mapr/globalgroupset.go b/internal/mapr/globalgroupset.go
index 2b12898..4f1e5ba 100644
--- a/internal/mapr/globalgroupset.go
+++ b/internal/mapr/globalgroupset.go
@@ -86,8 +86,8 @@ func (g *GlobalGroupSet) WriteResult(query *Query, finalResult bool) error {
}
// Result returns the result of the mapreduce aggregation as a string.
-func (g *GlobalGroupSet) Result(query *Query, rowsLimit int) (string, int, error) {
+func (g *GlobalGroupSet) Result(query *Query, rowsLimit int, renderer ResultRenderer) (string, int, error) {
g.semaphore <- struct{}{}
defer func() { <-g.semaphore }()
- return g.GroupSet.Result(query, rowsLimit)
+ return g.GroupSet.Result(query, rowsLimit, renderer)
}
diff --git a/internal/mapr/groupsetresult.go b/internal/mapr/groupsetresult.go
index 26e4b12..c87c22f 100644
--- a/internal/mapr/groupsetresult.go
+++ b/internal/mapr/groupsetresult.go
@@ -6,15 +6,13 @@ import (
"os"
"strings"
- "github.com/mimecast/dtail/internal/color"
- "github.com/mimecast/dtail/internal/config"
"github.com/mimecast/dtail/internal/io/dlog"
"github.com/mimecast/dtail/internal/io/pool"
"github.com/mimecast/dtail/internal/protocol"
)
// Result returns a nicely formated result of the query from the group set.
-func (g *GroupSet) Result(query *Query, rowsLimit int) (string, int, error) {
+func (g *GroupSet) Result(query *Query, rowsLimit int, renderer ResultRenderer) (string, int, error) {
rows, columnWidths, err := g.result(query, true)
if err != nil {
return "", 0, err
@@ -27,97 +25,69 @@ func (g *GroupSet) Result(query *Query, rowsLimit int) (string, int, error) {
sb := pool.BuilderBuffer.Get().(*strings.Builder)
defer pool.RecycleBuilderBuffer(sb)
- g.resultWriteFormattedHeader(query, sb, lastColumn, rowsLimit, columnWidths)
- g.resultWriteFormattedHeaderRowSeparator(query, sb, lastColumn, columnWidths)
- g.resultWriteFormattedData(query, sb, lastColumn, rowsLimit, columnWidths, rows)
+ if renderer == nil {
+ renderer = PlainResultRenderer()
+ }
+
+ g.resultWriteFormattedHeader(query, renderer, sb, lastColumn, rowsLimit, columnWidths)
+ g.resultWriteFormattedHeaderRowSeparator(query, renderer, sb, lastColumn, columnWidths)
+ g.resultWriteFormattedData(query, renderer, sb, lastColumn, rowsLimit, columnWidths, rows)
return sb.String(), len(rows), nil
}
// Write a nicely formatted header for the result data.
-func (g *GroupSet) resultWriteFormattedHeader(query *Query, sb *strings.Builder,
+func (g *GroupSet) resultWriteFormattedHeader(query *Query, renderer ResultRenderer, sb *strings.Builder,
lastColumn, rowsLimit int, columnWidths []int) {
for i, sc := range query.Select {
format := fmt.Sprintf(" %%%ds ", columnWidths[i])
str := fmt.Sprintf(format, sc.FieldStorage)
- g.resultWriteFormattedHeaderEntry(query, sb, sc, str)
+ g.resultWriteFormattedHeaderEntry(query, renderer, sb, sc, str)
if i == lastColumn {
continue
}
- g.resultWriteFormattedHeaderEntrySeparator(query, sb)
+ g.resultWriteFormattedHeaderEntrySeparator(renderer, sb)
}
sb.WriteString("\n")
}
-func (g *GroupSet) resultWriteFormattedHeaderEntry(query *Query, sb *strings.Builder,
+func (g *GroupSet) resultWriteFormattedHeaderEntry(query *Query, renderer ResultRenderer, sb *strings.Builder,
sc selectCondition, str string) {
- if config.Client.TermColorsEnable {
- attrs := []color.Attribute{config.Client.TermColors.MaprTable.HeaderAttr}
- if sc.FieldStorage == query.OrderBy {
- attrs = append(attrs, config.Client.TermColors.MaprTable.HeaderSortKeyAttr)
- }
- for _, groupBy := range query.GroupBy {
- if sc.FieldStorage == groupBy {
- attrs = append(attrs, config.Client.TermColors.MaprTable.HeaderGroupKeyAttr)
- break
- }
+ isGroupKey := false
+ for _, groupBy := range query.GroupBy {
+ if sc.FieldStorage == groupBy {
+ isGroupKey = true
+ break
}
- color.PaintWithAttrs(sb, str,
- config.Client.TermColors.MaprTable.HeaderFg,
- config.Client.TermColors.MaprTable.HeaderBg,
- attrs)
-
- } else {
- sb.WriteString(str)
}
+ renderer.WriteHeaderEntry(sb, str, sc.FieldStorage == query.OrderBy, isGroupKey)
}
-func (g *GroupSet) resultWriteFormattedHeaderEntrySeparator(query *Query, sb *strings.Builder) {
- if config.Client.TermColorsEnable {
- color.PaintWithAttr(sb, protocol.FieldDelimiter,
- config.Client.TermColors.MaprTable.HeaderDelimiterFg,
- config.Client.TermColors.MaprTable.HeaderDelimiterBg,
- config.Client.TermColors.MaprTable.HeaderDelimiterAttr)
- } else {
- sb.WriteString(protocol.FieldDelimiter)
- }
+func (g *GroupSet) resultWriteFormattedHeaderEntrySeparator(renderer ResultRenderer, sb *strings.Builder) {
+ renderer.WriteHeaderDelimiter(sb, protocol.FieldDelimiter)
}
// This writes a nicely formatted line separating the header and the data.
-func (g *GroupSet) resultWriteFormattedHeaderRowSeparator(query *Query, sb *strings.Builder,
+func (g *GroupSet) resultWriteFormattedHeaderRowSeparator(query *Query, renderer ResultRenderer, sb *strings.Builder,
lastColumn int, columnWidths []int) {
for i := 0; i < len(query.Select); i++ {
str := fmt.Sprintf("-%s-", strings.Repeat("-", columnWidths[i]))
- if config.Client.TermColorsEnable {
- color.PaintWithAttr(sb, str,
- config.Client.TermColors.MaprTable.HeaderDelimiterFg,
- config.Client.TermColors.MaprTable.HeaderDelimiterBg,
- config.Client.TermColors.MaprTable.HeaderDelimiterAttr)
- } else {
- sb.WriteString(str)
- }
+ renderer.WriteHeaderDelimiter(sb, str)
if i == lastColumn {
continue
}
- if config.Client.TermColorsEnable {
- color.PaintWithAttr(sb, protocol.FieldDelimiter,
- config.Client.TermColors.MaprTable.HeaderDelimiterFg,
- config.Client.TermColors.MaprTable.HeaderDelimiterBg,
- config.Client.TermColors.MaprTable.HeaderDelimiterAttr)
- } else {
- sb.WriteString(protocol.FieldDelimiter)
- }
+ renderer.WriteHeaderDelimiter(sb, protocol.FieldDelimiter)
}
sb.WriteString("\n")
}
// Write the result data nicely formatted.
-func (g *GroupSet) resultWriteFormattedData(query *Query, sb *strings.Builder,
+func (g *GroupSet) resultWriteFormattedData(query *Query, renderer ResultRenderer, sb *strings.Builder,
lastColumn, rowsLimit int, columnWidths []int, rows []result) {
for i, r := range rows {
@@ -125,37 +95,22 @@ func (g *GroupSet) resultWriteFormattedData(query *Query, sb *strings.Builder,
break
}
for j, value := range r.values {
- g.resultWriteFormattedDataEntry(query, sb, columnWidths, j, value)
+ g.resultWriteFormattedDataEntry(renderer, sb, columnWidths, j, value)
if j == lastColumn {
continue
}
- // Now, write the data entry separator.
- if config.Client.TermColorsEnable {
- color.PaintWithAttr(sb, protocol.FieldDelimiter,
- config.Client.TermColors.MaprTable.DelimiterFg,
- config.Client.TermColors.MaprTable.DelimiterBg,
- config.Client.TermColors.MaprTable.DelimiterAttr)
- } else {
- sb.WriteString(protocol.FieldDelimiter)
- }
+ renderer.WriteDataDelimiter(sb, protocol.FieldDelimiter)
}
sb.WriteString("\n")
}
}
-func (g *GroupSet) resultWriteFormattedDataEntry(query *Query, sb *strings.Builder,
+func (g *GroupSet) resultWriteFormattedDataEntry(renderer ResultRenderer, sb *strings.Builder,
columnWidths []int, j int, value string) {
format := fmt.Sprintf(" %%%ds ", columnWidths[j])
str := fmt.Sprintf(format, value)
- if config.Client.TermColorsEnable {
- color.PaintWithAttr(sb, str,
- config.Client.TermColors.MaprTable.DataFg,
- config.Client.TermColors.MaprTable.DataBg,
- config.Client.TermColors.MaprTable.DataAttr)
- } else {
- sb.WriteString(str)
- }
+ renderer.WriteDataEntry(sb, str)
}
func (*GroupSet) writeQueryFile(query *Query) error {
diff --git a/internal/mapr/groupsetresult_renderer_test.go b/internal/mapr/groupsetresult_renderer_test.go
new file mode 100644
index 0000000..53f45d5
--- /dev/null
+++ b/internal/mapr/groupsetresult_renderer_test.go
@@ -0,0 +1,113 @@
+package mapr
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestGroupSetResultUsesProvidedRenderer(t *testing.T) {
+ query, err := NewQuery("select host,count(value) from stats group by host order by count(value)")
+ if err != nil {
+ t.Fatalf("Unable to parse query: %v", err)
+ }
+
+ groupSet := NewGroupSet()
+ set := groupSet.GetSet("host-a")
+ if err := set.Aggregate("host", Last, "host-a", false); err != nil {
+ t.Fatalf("Unable to aggregate host field: %v", err)
+ }
+ if err := set.Aggregate("count(value)", Count, "", false); err != nil {
+ t.Fatalf("Unable to aggregate count field: %v", err)
+ }
+
+ renderer := &recordingRenderer{}
+ result, numRows, err := groupSet.Result(query, 10, renderer)
+ if err != nil {
+ t.Fatalf("Unable to render result: %v", err)
+ }
+ if numRows != 1 {
+ t.Fatalf("Expected one row, got %d", numRows)
+ }
+ if len(renderer.headerCalls) != 2 {
+ t.Fatalf("Expected two header calls, got %d", len(renderer.headerCalls))
+ }
+ if renderer.headerCalls[0].isSortKey || !renderer.headerCalls[0].isGroupKey {
+ t.Fatalf("Unexpected flags for group key header: %+v", renderer.headerCalls[0])
+ }
+ if !renderer.headerCalls[1].isSortKey || renderer.headerCalls[1].isGroupKey {
+ t.Fatalf("Unexpected flags for sort key header: %+v", renderer.headerCalls[1])
+ }
+ if len(renderer.headerDelimiters) == 0 {
+ t.Fatal("Expected header delimiters to be rendered")
+ }
+ if len(renderer.dataDelimiters) == 0 {
+ t.Fatal("Expected data delimiters to be rendered")
+ }
+ if !strings.Contains(result, "host-a") || !strings.Contains(result, "1") {
+ t.Fatalf("Expected rendered output to contain row data, got %q", result)
+ }
+}
+
+func TestGroupSetResultFallsBackToPlainRenderer(t *testing.T) {
+ query, err := NewQuery("select count(value) from stats")
+ if err != nil {
+ t.Fatalf("Unable to parse query: %v", err)
+ }
+
+ groupSet := NewGroupSet()
+ set := groupSet.GetSet("")
+ if err := set.Aggregate("count(value)", Count, "", false); err != nil {
+ t.Fatalf("Unable to aggregate count field: %v", err)
+ }
+
+ result, numRows, err := groupSet.Result(query, 10, nil)
+ if err != nil {
+ t.Fatalf("Unable to render result with nil renderer: %v", err)
+ }
+ if numRows != 1 {
+ t.Fatalf("Expected one row, got %d", numRows)
+ }
+ if !strings.Contains(result, "count(value)") || !strings.Contains(result, "1") {
+ t.Fatalf("Expected plain rendered output, got %q", result)
+ }
+ if strings.Contains(result, "\x1b[") {
+ t.Fatalf("Expected plain output without ANSI escapes, got %q", result)
+ }
+}
+
+type recordingRenderer struct {
+ headerCalls []headerCall
+ headerDelimiters []string
+ dataEntries []string
+ dataDelimiters []string
+}
+
+type headerCall struct {
+ text string
+ isSortKey bool
+ isGroupKey bool
+}
+
+func (r *recordingRenderer) WriteHeaderEntry(sb *strings.Builder, text string, isSortKey, isGroupKey bool) {
+ r.headerCalls = append(r.headerCalls, headerCall{
+ text: text,
+ isSortKey: isSortKey,
+ isGroupKey: isGroupKey,
+ })
+ sb.WriteString(text)
+}
+
+func (r *recordingRenderer) WriteHeaderDelimiter(sb *strings.Builder, text string) {
+ r.headerDelimiters = append(r.headerDelimiters, text)
+ sb.WriteString(text)
+}
+
+func (r *recordingRenderer) WriteDataEntry(sb *strings.Builder, text string) {
+ r.dataEntries = append(r.dataEntries, text)
+ sb.WriteString(text)
+}
+
+func (r *recordingRenderer) WriteDataDelimiter(sb *strings.Builder, text string) {
+ r.dataDelimiters = append(r.dataDelimiters, text)
+ sb.WriteString(text)
+}
diff --git a/internal/mapr/result_renderer.go b/internal/mapr/result_renderer.go
new file mode 100644
index 0000000..e03ed9a
--- /dev/null
+++ b/internal/mapr/result_renderer.go
@@ -0,0 +1,34 @@
+package mapr
+
+import "strings"
+
+// ResultRenderer formats terminal table output for mapreduce results.
+type ResultRenderer interface {
+ WriteHeaderEntry(sb *strings.Builder, text string, isSortKey, isGroupKey bool)
+ WriteHeaderDelimiter(sb *strings.Builder, text string)
+ WriteDataEntry(sb *strings.Builder, text string)
+ WriteDataDelimiter(sb *strings.Builder, text string)
+}
+
+type plainResultRenderer struct{}
+
+// PlainResultRenderer returns a renderer that writes uncolored terminal output.
+func PlainResultRenderer() ResultRenderer {
+ return plainResultRenderer{}
+}
+
+func (plainResultRenderer) WriteHeaderEntry(sb *strings.Builder, text string, _, _ bool) {
+ sb.WriteString(text)
+}
+
+func (plainResultRenderer) WriteHeaderDelimiter(sb *strings.Builder, text string) {
+ sb.WriteString(text)
+}
+
+func (plainResultRenderer) WriteDataEntry(sb *strings.Builder, text string) {
+ sb.WriteString(text)
+}
+
+func (plainResultRenderer) WriteDataDelimiter(sb *strings.Builder, text string) {
+ sb.WriteString(text)
+}