// 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 }