mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The escape QuoteIdent applies is not ours to choose — the datastore driver already spells it, as colEscape/colUnEscape in hanzo-ds/go lib/column/column.go. The test now asserts QuoteIdent writes exactly what the driver would and that the driver's own inverse recovers the name, so if that dialect ever moves this fails instead of quietly emitting an identifier the server reads differently than we wrote it. The comment claimed "order matters" between the two replacements. It does not, and the claim was the dangerous kind of wrong: it invites someone to unroll the Replacer into two sequential ReplaceAll calls and put them in the "right" order. A Replacer is SINGLE PASS over non-overlapping patterns, so every input byte is rewritten exactly once and order is irrelevant. Sequential ReplaceAll would be the broken form — escaping the backtick first and the backslash second doubles the backslashes the first call just introduced. Said plainly now, with the warning attached to the thing it protects. No behaviour change. Co-authored-by: Hanzo Dev <[email protected]>
62 lines
2.9 KiB
Go
62 lines
2.9 KiB
Go
package querybuilder
|
|
|
|
import "strings"
|
|
|
|
// A telemetry field key NAME is caller text. It arrives as JSON on
|
|
// /api/v5/query_range, and it is replayed out of stored dashboards and alert
|
|
// rules — the rule evaluator builds a QueryRangeRequest and calls the querier
|
|
// directly, so a name on that path never passes the request validator at all.
|
|
//
|
|
// It lands in two syntactic positions, and a bound parameter can occupy
|
|
// NEITHER, so quoting is the whole defence:
|
|
//
|
|
// - a backtick-quoted IDENTIFIER — the SELECT alias, the GROUP BY term, the
|
|
// ORDER BY term;
|
|
// - a single-quoted string LITERAL — the map/JSON accessors that read an
|
|
// attribute out of a column, e.g. JSONExtractString(labels, '<name>').
|
|
//
|
|
// Metrics is the position that proves this has to live here rather than in the
|
|
// validator: a metric label is free-form, so an unrecognised name falls back to
|
|
// the labels column BY DESIGN and there is no key allowlist to fail against.
|
|
//
|
|
// These two are the only way a name becomes SQL text. Nothing else quoted it —
|
|
// sqlbuilder.Escape, the one call that used to sit next to a name, replaces "$"
|
|
// with "$$" for the builder's own placeholder syntax and is not SQL escaping.
|
|
|
|
// QuoteIdent renders a name as a backtick-quoted datastore identifier.
|
|
//
|
|
// The escape is the datastore's own, and the authority for it is the driver:
|
|
// hanzo-ds/go lib/column/column.go carries the same pair as colEscape /
|
|
// colUnEscape. Inside a backtick-quoted identifier a backslash escapes the next
|
|
// character, so BOTH the backtick and the backslash have to be escaped — the
|
|
// backslash because a name ending in one would otherwise consume the closing
|
|
// backtick this function adds and leave the identifier open.
|
|
//
|
|
// It is a single-pass Replacer on purpose. The two patterns do not overlap, so
|
|
// one pass is order-independent and each input byte is rewritten exactly once.
|
|
// Two sequential ReplaceAll calls would NOT be safe — escaping the backtick
|
|
// first and the backslash second would then double the backslashes the first
|
|
// call had just introduced — so do not unroll this into ReplaceAll.
|
|
//
|
|
// A name with none of those characters — every real attribute key, which is a
|
|
// dotted identifier — renders byte for byte as before.
|
|
func QuoteIdent(name string) string {
|
|
return "`" + identEscaper.Replace(name) + "`"
|
|
}
|
|
|
|
// EscapeLiteral escapes a name for use INSIDE an existing pair of single quotes,
|
|
// e.g. fmt.Sprintf("JSONExtractString(labels, '%s')", EscapeLiteral(key.Name)).
|
|
// It does not add the quotes, because every call site already writes them as
|
|
// part of a larger expression.
|
|
//
|
|
// Same construction as QuoteIdent, and single-pass for the same reason: the
|
|
// quote and the backslash do not overlap, so each byte is rewritten once.
|
|
func EscapeLiteral(s string) string {
|
|
return literalEscaper.Replace(s)
|
|
}
|
|
|
|
var (
|
|
identEscaper = strings.NewReplacer(`\`, `\\`, "`", "\\`")
|
|
literalEscaper = strings.NewReplacer(`\`, `\\`, `'`, `\'`)
|
|
)
|