Files
akvorado/console/query/column.go
Vincent Bernat e2f1df9add tests: replace godebug by go-cmp for structure diffs
go-cmp is stricter and allow to catch more problems. Moreover, the
output is a bit nicer.
2025-08-23 16:03:09 +02:00

161 lines
5.0 KiB
Go

// SPDX-FileCopyrightText: 2023 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
// Package query provides query columns and query filters. These
// types are special as they need a schema to be validated.
package query
import (
"fmt"
"strings"
"akvorado/common/helpers"
"akvorado/common/schema"
)
// Column represents a query column. It should be instantiated with NewColumn() or
// Unmarshal(), then call Validate().
type Column struct {
validated bool
name string
key schema.ColumnKey
}
// Columns is a set of query columns.
type Columns []Column
// NewColumn creates a new column. Validate() should be called before using it.
func NewColumn(name string) Column {
return Column{name: name}
}
func (qc Column) check() {
if !qc.validated {
panic("query column not validated")
}
}
func (qc Column) String() string {
return qc.name
}
// Equal returns true iff two columns have the same name.
func (qc Column) Equal(oqc Column) bool {
return qc.name == oqc.name
}
// MarshalText turns a column into a string.
func (qc Column) MarshalText() ([]byte, error) {
return []byte(qc.name), nil
}
// UnmarshalText parses a column. Validate() should be called before use.
func (qc *Column) UnmarshalText(input []byte) error {
name := string(input)
*qc = Column{name: name}
return nil
}
// Key returns the key for the column.
func (qc *Column) Key() schema.ColumnKey {
qc.check()
return qc.key
}
// Validate should be called before using the column. We need a schema component
// for that.
func (qc *Column) Validate(schema *schema.Component) error {
if column, ok := schema.LookupColumnByName(qc.name); ok && !column.ConsoleNotDimension && !column.Disabled {
qc.key = column.Key
qc.validated = true
return nil
}
return fmt.Errorf("unknown column name %s", qc.name)
}
// Reverse reverses the column direction
func (qc *Column) Reverse(schema *schema.Component) {
name := schema.ReverseColumnDirection(qc.Key()).String()
reverted := Column{name: name}
if reverted.Validate(schema) == nil {
*qc = reverted
}
// No modification otherwise
}
// Reverse reverses the direction of all columns
func (qcs Columns) Reverse(schema *schema.Component) {
for i := range qcs {
qcs[i].Reverse(schema)
}
}
// Validate call Validate on each column.
func (qcs Columns) Validate(schema *schema.Component) error {
for i := range qcs {
if err := qcs[i].Validate(schema); err != nil {
return err
}
}
return nil
}
// ToSQLSelect transforms a column into an expression to use in SELECT
func (qc Column) ToSQLSelect(sch *schema.Component) string {
var strValue string
key := qc.Key()
switch key {
// Special cases
case schema.ColumnSrcAS, schema.ColumnDstAS, schema.ColumnDst1stAS, schema.ColumnDst2ndAS, schema.ColumnDst3rdAS:
strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('%s', 'name', %s, '???'))`,
qc, schema.DictionaryASNs, qc)
case schema.ColumnInIfBoundary, schema.ColumnOutIfBoundary:
strValue = fmt.Sprintf(`toString(%s)`, qc.String())
case schema.ColumnEType:
strValue = fmt.Sprintf(`if(EType = %d, 'IPv4', if(EType = %d, 'IPv6', '???'))`,
helpers.ETypeIPv4, helpers.ETypeIPv6)
case schema.ColumnProto:
strValue = fmt.Sprintf(`dictGetOrDefault('%s', 'name', Proto, '???')`, schema.DictionaryProtocols)
case schema.ColumnMPLSLabels:
strValue = `arrayStringConcat(MPLSLabels, ' ')`
case schema.ColumnDstASPath:
strValue = `arrayStringConcat(DstASPath, ' ')`
case schema.ColumnDstCommunities:
strValue = `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')`
case schema.ColumnSrcMAC, schema.ColumnDstMAC:
strValue = fmt.Sprintf("MACNumToString(%s)", qc)
case schema.ColumnTCPFlags:
bits := []string{
"FIN",
"SYN",
"RST",
"PSH",
".ACK",
"URG",
"ECE",
"CWR",
"NS",
}
array := make([]string, len(bits))
for bit, v := range bits {
array[bit] = fmt.Sprintf("if(bitTest(%s, %d) = 1, '%s', '')", qc, bit, v[:1])
}
strValue = fmt.Sprintf("arrayStringConcat([%s], '')", strings.Join(array, ", "))
case schema.ColumnDstPort, schema.ColumnSrcPort:
strValue = fmt.Sprintf(`replaceRegexpOne(multiIf(%s==6, concat(toString(%s), '/', dictGetOrDefault('%s', 'name', %s,'')), %s==17, concat(toString(%s), '/', dictGetOrDefault('%s', 'name', %s,'')), toString(%s)), '/$', '')`,
schema.ColumnProto, qc, schema.DictionaryTCP, qc, schema.ColumnProto, qc, schema.DictionaryUDP, qc, qc)
// Generic cases
default:
strValue = qc.String()
if col, ok := sch.LookupColumnByKey(key); ok {
if strings.HasPrefix(col.ClickHouseType, "UInt") {
strValue = fmt.Sprintf(`toString(%s)`, qc)
} else if col.ClickHouseType == "IPv6" || col.ClickHouseType == "LowCardinality(IPv6)" {
strValue = fmt.Sprintf("replaceRegexpOne(IPv6NumToString(%s), '^::ffff:', '')", qc)
}
}
}
return strValue
}