Files
zeekayandHanzo Dev 52f2231987 security: the older builders quote the value beside the key but never the key
The v3/v4 builders are live — querier.go wires tracesV4.PrepareTracesQuery — and
they carry the same bug as the v5 statement builders, one layer down and one
position wider. Three caller values reached ClickHouse raw:

  - the ORDER BY DIRECTION. Direction is a bare string off the request body and
    every builder interpolated it beside the column, so
    "asc,(select count() from system.tables)" was an entire extra ordering term.
    It needed no quote and no backtick to get there — the cheapest of the three.
  - the ORDER BY / GROUP BY / alias COLUMN, in backticks it can close.
  - the ATTRIBUTE KEY, in the single quotes of the map and JSON accessors. That
    one lands in the WHERE clause, so it is not an ordering oracle but a
    predicate the caller writes: mapContains(attributes_string, 'a'),(select 1))
    or ('1'='1') parses and runs.

The asymmetry is stark here. On the very lines that build a filter, the VALUE
goes through utils.QuoteEscapedString and the KEY beside it goes in bare. And
the allowlist that appears to gate the column — tagLookup — is built out of the
caller's own GroupBy, so a name repeated in both fields validates itself.

utils.AddBackTickToFormatTag is the pass-through underneath, and it declined to
quote in two ways: a name with no "." and no "-" came back BARE, and a name
already wrapped in backticks came back AS IS, which let a caller supply its own
quoting. Neither branch escaped a backtick. It now returns bare only for what it
was assuming — a plain identifier — and quotes and escapes everything else.

Direction.Keyword() is the guard for the direction, on the value rather than at
each of the sixteen sites that interpolate it. It folds case to compare because
callers send both, and returns the ORIGINAL on a match, which is safe precisely
because the match succeeded: a string whose lowercase is "asc" or "desc" is
those letters and nothing else. Anything else renders as empty, which is what an
absent direction already rendered as.

Identifiers and literals go through the same querybuilder.QuoteIdent and
EscapeLiteral the v5 builders now use. A real key takes the path it always did,
so the golden SQL in every one of these packages is unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 21:49:08 -07:00

446 lines
21 KiB
Go

package helpers
import (
"fmt"
"strings"
"time"
"github.com/hanzoai/o11y/pkg/query-service/constants"
v3 "github.com/hanzoai/o11y/pkg/query-service/model/v3"
"github.com/hanzoai/o11y/pkg/query-service/utils"
"github.com/hanzoai/o11y/pkg/querybuilder"
)
var (
sixHoursInMilliseconds = time.Hour.Milliseconds() * 6
oneDayInMilliseconds = time.Hour.Milliseconds() * 24
oneWeekInMilliseconds = oneDayInMilliseconds * 7
// when the query requests for almost 1 day, but not exactly 1 day, we need to add an offset to the end time
// to make sure that we are using the correct table
// this is because the start gets adjusted to the nearest step interval and uses the 5m table for 4m step interval
// leading to time series that doesn't best represent the rate of change
offsetBucket = 60 * time.Minute.Milliseconds()
)
func whichTSTableToUse(start, end int64, mq *v3.BuilderQuery) (int64, int64, string) {
// if we have a hint for the table, we need to use it
// the hint will be used to override the default table selection logic
if mq.MetricTableHints != nil {
if mq.MetricTableHints.TimeSeriesTableName != "" {
switch mq.MetricTableHints.TimeSeriesTableName {
case constants.O11Y_TIMESERIES_v4_LOCAL_TABLENAME:
// adjust the start time to nearest 1 hour
start = start - (start % (time.Hour.Milliseconds() * 1))
case constants.O11Y_TIMESERIES_v4_6HRS_LOCAL_TABLENAME:
// adjust the start time to nearest 6 hours
start = start - (start % (time.Hour.Milliseconds() * 6))
case constants.O11Y_TIMESERIES_v4_1DAY_LOCAL_TABLENAME:
// adjust the start time to nearest 1 day
start = start - (start % (time.Hour.Milliseconds() * 24))
case constants.O11Y_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME:
// adjust the start time to nearest 1 week
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
}
return start, end, mq.MetricTableHints.TimeSeriesTableName
}
}
// If time range is less than 6 hours, we need to use the `time_series_v4` table
// else if time range is less than 1 day and greater than 6 hours, we need to use the `time_series_v4_6hrs` table
// else if time range is less than 1 week and greater than 1 day, we need to use the `time_series_v4_1day` table
// else we need to use the `time_series_v4_1week` table
var tableName string
if end-start < sixHoursInMilliseconds {
// adjust the start time to nearest 1 hour
start = start - (start % (time.Hour.Milliseconds() * 1))
tableName = constants.O11Y_TIMESERIES_v4_LOCAL_TABLENAME
} else if end-start < oneDayInMilliseconds {
// adjust the start time to nearest 6 hours
start = start - (start % (time.Hour.Milliseconds() * 6))
tableName = constants.O11Y_TIMESERIES_v4_6HRS_LOCAL_TABLENAME
} else if end-start < oneWeekInMilliseconds {
// adjust the start time to nearest 1 day
start = start - (start % (time.Hour.Milliseconds() * 24))
tableName = constants.O11Y_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
} else {
// adjust the start time to nearest 1 week
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
tableName = constants.O11Y_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
}
return start, end, tableName
}
// start and end are in milliseconds
// we have three tables for samples
// 1. distributed_samples_v4
// 2. distributed_samples_v4_agg_5m - for queries with time range above or equal to 1 day and less than 1 week
// 3. distributed_samples_v4_agg_30m - for queries with time range above or equal to 1 week
// if the `timeAggregation` is `count_distinct` we can't use the aggregated tables because they don't support it
func WhichSamplesTableToUse(start, end int64, mq *v3.BuilderQuery) string {
// if we have a hint for the table, we need to use it
// the hint will be used to override the default table selection logic
if mq.MetricTableHints != nil {
if mq.MetricTableHints.SamplesTableName != "" {
return mq.MetricTableHints.SamplesTableName
}
}
// we don't have any aggregated table for sketches (yet)
if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) {
return constants.O11Y_EXP_HISTOGRAM_TABLENAME
}
// if the time aggregation is count_distinct, we need to use the distributed_samples_v4 table
// because the aggregated tables don't support count_distinct
if mq.TimeAggregation == v3.TimeAggregationCountDistinct {
return constants.O11Y_SAMPLES_V4_TABLENAME
}
if end-start < oneDayInMilliseconds+offsetBucket {
// if we are dealing with delta metrics and interval is greater than 5 minutes, we can use the 5m aggregated table
// why would interval be greater than 5 minutes?
// we allow people to configure the step interval so we can make use of this
if mq.Temporality == v3.Delta && mq.TimeAggregation == v3.TimeAggregationIncrease && mq.StepInterval >= 300 && mq.StepInterval < 1800 {
return constants.O11Y_SAMPLES_V4_AGG_5M_TABLENAME
} else if mq.Temporality == v3.Delta && mq.TimeAggregation == v3.TimeAggregationIncrease && mq.StepInterval >= 1800 {
// if we are dealing with delta metrics and interval is greater than 30 minutes, we can use the 30m aggregated table
return constants.O11Y_SAMPLES_V4_AGG_30M_TABLENAME
}
return constants.O11Y_SAMPLES_V4_TABLENAME
} else if end-start < oneWeekInMilliseconds+offsetBucket {
return constants.O11Y_SAMPLES_V4_AGG_5M_TABLENAME
} else {
return constants.O11Y_SAMPLES_V4_AGG_30M_TABLENAME
}
}
func AggregationColumnForSamplesTable(start, end int64, mq *v3.BuilderQuery) string {
tableName := WhichSamplesTableToUse(start, end, mq)
var aggregationColumn string
switch mq.Temporality {
case v3.Delta:
// for delta metrics, we only support `RATE`/`INCREASE` both of which are sum
// although it doesn't make sense to use anyLast, avg, min, max, count on delta metrics,
// we are keeping it here to make sure that query will not be invalid
switch tableName {
case constants.O11Y_SAMPLES_V4_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(value)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(value)"
case v3.TimeAggregationAvg:
aggregationColumn = "avg(value)"
case v3.TimeAggregationMin:
aggregationColumn = "min(value)"
case v3.TimeAggregationMax:
aggregationColumn = "max(value)"
case v3.TimeAggregationCount:
aggregationColumn = "count(value)"
case v3.TimeAggregationCountDistinct:
aggregationColumn = "countDistinct(value)"
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
aggregationColumn = "sum(value)"
}
case constants.O11Y_SAMPLES_V4_AGG_5M_TABLENAME, constants.O11Y_SAMPLES_V4_AGG_30M_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(last)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(sum)"
case v3.TimeAggregationAvg:
aggregationColumn = "sum(sum) / sum(count)"
case v3.TimeAggregationMin:
aggregationColumn = "min(min)"
case v3.TimeAggregationMax:
aggregationColumn = "max(max)"
case v3.TimeAggregationCount:
aggregationColumn = "sum(count)"
// count_distinct is not supported in aggregated tables
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
aggregationColumn = "sum(sum)"
}
}
case v3.Cumulative:
// for cumulative metrics, we only support `RATE`/`INCREASE`. The max value in window is
// used to calculate the sum which is then divided by the window size to get the rate
switch tableName {
case constants.O11Y_SAMPLES_V4_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(value)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(value)"
case v3.TimeAggregationAvg:
aggregationColumn = "avg(value)"
case v3.TimeAggregationMin:
aggregationColumn = "min(value)"
case v3.TimeAggregationMax:
aggregationColumn = "max(value)"
case v3.TimeAggregationCount:
aggregationColumn = "count(value)"
case v3.TimeAggregationCountDistinct:
aggregationColumn = "countDistinct(value)"
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
aggregationColumn = "max(value)"
}
case constants.O11Y_SAMPLES_V4_AGG_5M_TABLENAME, constants.O11Y_SAMPLES_V4_AGG_30M_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(last)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(sum)"
case v3.TimeAggregationAvg:
aggregationColumn = "sum(sum) / sum(count)"
case v3.TimeAggregationMin:
aggregationColumn = "min(min)"
case v3.TimeAggregationMax:
aggregationColumn = "max(max)"
case v3.TimeAggregationCount:
aggregationColumn = "sum(count)"
// count_distinct is not supported in aggregated tables
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // only these two options give meaningful results
aggregationColumn = "max(max)"
}
}
case v3.Unspecified:
switch tableName {
case constants.O11Y_SAMPLES_V4_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(value)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(value)"
case v3.TimeAggregationAvg:
aggregationColumn = "avg(value)"
case v3.TimeAggregationMin:
aggregationColumn = "min(value)"
case v3.TimeAggregationMax:
aggregationColumn = "max(value)"
case v3.TimeAggregationCount:
aggregationColumn = "count(value)"
case v3.TimeAggregationCountDistinct:
aggregationColumn = "countDistinct(value)"
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // ideally, this should never happen
aggregationColumn = "sum(value)"
}
case constants.O11Y_SAMPLES_V4_AGG_5M_TABLENAME, constants.O11Y_SAMPLES_V4_AGG_30M_TABLENAME:
switch mq.TimeAggregation {
case v3.TimeAggregationAnyLast:
aggregationColumn = "anyLast(last)"
case v3.TimeAggregationSum:
aggregationColumn = "sum(sum)"
case v3.TimeAggregationAvg:
aggregationColumn = "sum(sum) / sum(count)"
case v3.TimeAggregationMin:
aggregationColumn = "min(min)"
case v3.TimeAggregationMax:
aggregationColumn = "max(max)"
case v3.TimeAggregationCount:
aggregationColumn = "sum(count)"
// count_distinct is not supported in aggregated tables
case v3.TimeAggregationRate, v3.TimeAggregationIncrease: // ideally, this should never happen
aggregationColumn = "sum(sum)"
}
}
}
return aggregationColumn
}
// PrepareTimeseriesFilterQuery builds the sub-query to be used for filtering timeseries based on the search criteria
func PrepareTimeseriesFilterQuery(start, end int64, mq *v3.BuilderQuery) (string, error) {
var conditions []string
var fs *v3.FilterSet = mq.Filters
var groupTags []v3.AttributeKey = mq.GroupBy
conditions = append(conditions, fmt.Sprintf("metric_name IN %s", utils.DatastoreFormattedMetricNames(mq.AggregateAttribute.Key)))
conditions = append(conditions, fmt.Sprintf("temporality = '%s'", mq.Temporality))
if constants.IsDotMetricsEnabled {
conditions = append(conditions, "__normalized = false")
} else {
conditions = append(conditions, "__normalized = true")
}
start, end, tableName := whichTSTableToUse(start, end, mq)
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))
if fs != nil && len(fs.Items) != 0 {
for _, item := range fs.Items {
if item.Key.Key == "__value" {
continue
}
toFormat := item.Value
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
if op == v3.FilterOperatorContains || op == v3.FilterOperatorNotContains {
toFormat = fmt.Sprintf("%%%s%%", toFormat)
}
var fmtVal string
if op != v3.FilterOperatorExists && op != v3.FilterOperatorNotExists {
fmtVal = utils.DatastoreFormattedValue(toFormat)
}
switch op {
case v3.FilterOperatorEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') = %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') != %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') IN %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') NOT IN %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLike:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotLike:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorRegex:
conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotRegex:
conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorGreaterThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') > %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorGreaterThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') >= %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLessThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') < %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLessThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') <= %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorContains:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotContains:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorExists:
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", querybuilder.EscapeLiteral(item.Key.Key)))
case v3.FilterOperatorNotExists:
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", querybuilder.EscapeLiteral(item.Key.Key)))
case v3.FilterOperatorILike:
conditions = append(conditions, fmt.Sprintf("ilike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotILike:
conditions = append(conditions, fmt.Sprintf("notILike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
default:
return "", fmt.Errorf("unsupported filter operator")
}
}
}
whereClause := strings.Join(conditions, " AND ")
var selectLabels string
for _, tag := range groupTags {
selectLabels += fmt.Sprintf("JSONExtractString(labels, '%s') as %s, ", querybuilder.EscapeLiteral(tag.Key), utils.AddBackTickToFormatTag(tag.Key))
}
// The table JOIN key always exists
selectLabels += "fingerprint"
filterSubQuery := fmt.Sprintf(
"SELECT DISTINCT %s FROM %s.%s WHERE %s",
selectLabels,
constants.O11Y_METRIC_DBNAME,
tableName,
whereClause,
)
return filterSubQuery, nil
}
// PrepareTimeseriesFilterQuery builds the sub-query to be used for filtering timeseries based on the search criteria
func PrepareTimeseriesFilterQueryV3(start, end int64, mq *v3.BuilderQuery) (string, error) {
var conditions []string
var fs *v3.FilterSet = mq.Filters
var groupTags []v3.AttributeKey = mq.GroupBy
conditions = append(conditions, fmt.Sprintf("metric_name IN %s", utils.DatastoreFormattedMetricNames(mq.AggregateAttribute.Key)))
conditions = append(conditions, fmt.Sprintf("temporality = '%s'", mq.Temporality))
if constants.IsDotMetricsEnabled {
conditions = append(conditions, "__normalized = false")
} else {
conditions = append(conditions, "__normalized = true")
}
start, end, tableName := whichTSTableToUse(start, end, mq)
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", start, end))
if fs != nil && len(fs.Items) != 0 {
for _, item := range fs.Items {
toFormat := item.Value
op := v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
if op == v3.FilterOperatorContains || op == v3.FilterOperatorNotContains {
toFormat = fmt.Sprintf("%%%s%%", toFormat)
}
fmtVal := utils.DatastoreFormattedValue(toFormat)
switch op {
case v3.FilterOperatorEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') = %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotEqual:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') != %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') IN %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotIn:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') NOT IN %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLike:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotLike:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorRegex:
conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotRegex:
conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorGreaterThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') > %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorGreaterThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') >= %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLessThan:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') < %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorLessThanOrEq:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, '%s') <= %s", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorContains:
conditions = append(conditions, fmt.Sprintf("like(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorNotContains:
conditions = append(conditions, fmt.Sprintf("notLike(JSONExtractString(labels, '%s'), %s)", querybuilder.EscapeLiteral(item.Key.Key), fmtVal))
case v3.FilterOperatorExists:
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", querybuilder.EscapeLiteral(item.Key.Key)))
case v3.FilterOperatorNotExists:
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", querybuilder.EscapeLiteral(item.Key.Key)))
default:
return "", fmt.Errorf("unsupported filter operator")
}
}
}
whereClause := strings.Join(conditions, " AND ")
var selectLabels string
if mq.AggregateOperator == v3.AggregateOperatorNoOp || mq.AggregateOperator == v3.AggregateOperatorRate {
selectLabels += "labels, "
} else {
for _, tag := range groupTags {
selectLabels += fmt.Sprintf("JSONExtractString(labels, '%s') as %s, ", querybuilder.EscapeLiteral(tag.Key), utils.AddBackTickToFormatTag(tag.Key))
}
}
// The table JOIN key always exists
selectLabels += "fingerprint"
filterSubQuery := fmt.Sprintf(
"SELECT DISTINCT %s FROM %s.%s WHERE %s",
selectLabels,
constants.O11Y_METRIC_DBNAME,
tableName,
whereClause,
)
return filterSubQuery, nil
}
func AddFlagsFilters(samplesTableFilter string, tableName string) string {
if tableName == constants.O11Y_SAMPLES_V4_TABLENAME || tableName == constants.O11Y_SAMPLES_V4_LOCAL_TABLENAME || tableName == constants.O11Y_EXP_HISTOGRAM_TABLENAME || tableName == constants.O11Y_EXP_HISTOGRAM_LOCAL_TABLENAME {
samplesTableFilter = fmt.Sprintf("%s AND %s", samplesTableFilter, "bitAnd(flags, 1) = 0")
}
return samplesTableFilter
}