Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions go/mysql/decimal/decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ var testTableScientificNotation = map[string]string{
"123.456e0": "123.456",
"123.456e2": "12345.6",
"123.456e10": "1234560000000",
// MySQL accepts a written plus on the exponent.
"1e+9": "1000000000",
"1E+9": "1000000000",
"245E+3": "245000",
"123.456e+2": "12345.6",
"0e+5": "0",
}

func init() {
Expand Down Expand Up @@ -192,6 +198,52 @@ func TestNewFromString(t *testing.T) {
}
}

// TestNewFromStringLeadingPlus covers a sign the negating entries in
// testTableScientificNotation cannot: MySQL reads a leading plus as the number
// it introduces, so CAST('+1' AS DECIMAL) is 1.
func TestNewFromStringLeadingPlus(t *testing.T) {
for in, want := range map[string]string{
"+1": "1",
"+1.5": "1.5",
"+0": "0",
"+1e9": "1000000000",
"+1e+9": "1000000000",
"+123.456e-2": "1.23456",
// The number starts after any leading whitespace, and so does the sign.
" +1": "1",
" -1": "-1",
" +1.5": "1.5",
" -1e+5": "-100000",
"\t-2": "-2",
" 1.5e+3": "1500",
} {
t.Run(in, func(t *testing.T) {
d, err := NewFromString(in)
require.NoError(t, err)
require.Equal(t, want, d.String())
})
}
}

// TestNewFromStringZeroExponent pins the formatting of a zero written with a
// positive exponent. String trims the padding away, but FormatMySQL keeps the
// scale it is asked for, so a stale exponent surfaces as leading zeros.
func TestNewFromStringZeroExponent(t *testing.T) {
for _, in := range []string{"0e5", "-0e5", "+0e5", "0E+5", "0e2", "0"} {
t.Run(in, func(t *testing.T) {
d, err := NewFromString(in)
require.NoError(t, err)
require.Equal(t, "0", d.String())
require.Equal(t, "0.000000", string(d.FormatMySQL(6)))
})
}

// A zero written to a scale keeps it: only a positive exponent is dropped.
d, err := NewFromString("0.00")
require.NoError(t, err)
require.Equal(t, int32(-2), d.Exponent())
}

func TestFloat64(t *testing.T) {
t.Skipf("Float64 does not check for exact")

Expand Down
34 changes: 25 additions & 9 deletions go/mysql/decimal/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,18 @@ func NewFromString(s string) (d Decimal, err error) {
}
i++
}

// The number starts after the leading whitespace, so that is where a sign

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think #20725 is still outstanding at this head. You mentioned fixing it in this PR, but the two cases from the issue remain:

"\v+1"  -> Vitess 0, MySQL 1
"1e +2" -> Vitess 1, MySQL 100

Because evalengine drops the parse error, both become silent wrong results. Should the outer whitespace handling add '\v' and '\f', with a separate exponent start that skips only space or tab immediately after e?

Cases such as "1e\n+2", "1e+ 2", and "1 e+2" should remain partial-value errors, so the general whitespace helper probably should not be reused inside the exponent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, this was still outstanding — fixed now, and the PR closes #20725.

// is allowed and where the mantissa is read from.
start := i
next:
for i < maxLen {
switch {
case s[i] == '-':
// Negative sign is allowed at the start and at the start
// of the exponent.
if i != 0 && expPos == -1 && i != expPos+1 {
case s[i] == '-' || s[i] == '+':

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can misread fixed-width text because evalToDecimal passes raw encoded bytes into this scanner. For example, _utf16le X'2B31' is one nonnumeric U+312B character, but this head reads it as ASCII +1 and returns 1, while the base and MySQL return 0.

Should the evalBytes bridge normalise UTF-16, UTF-16LE, UCS-2 and UTF-32 to latin1 first, matching MySQL's mbminlen > 1 path?

raw := e.bytes
if col := colldata.Lookup(e.col.Collation); col != nil {
	cs := col.Charset()
	switch cs.(type) {
	case charset.Charset_utf16, charset.Charset_utf16le,
		charset.Charset_ucs2, charset.Charset_utf32:
		raw, _ = charset.Convert(nil, charset.Charset_latin1{}, raw, cs)
	}
}
dec, _ := decimal.NewFromString(hack.String(raw))

A couple of TestCompilerSingle rows should cover both sides and exercise the AST and compiled paths:

{expression: `CAST(_utf16le X'2B31' AS DECIMAL(20,6))`,
	result: "DECIMAL(0.000000)"},
{expression: `CAST(_utf16le X'2B003100' AS DECIMAL(20,6))`,
	result: "DECIMAL(1.000000)"},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this in 497c411, but the same issue exists for float and int types.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the changed numeric-cast behavior

This changes user-visible CAST(... AS DECIMAL) behavior for leading signs, additional whitespace, and fixed-width character sets, but the commit contains no release or deployment note. Add an explicit upgrade-facing callout for these observable correctness changes as required by the repository policy.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

@arthurschreiber arthurschreiber Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not adding a changelog callout for this. These are compatibility fixes that make the evalengine match MySQL in more cases — matching MySQL is the baseline contract Vitess users already expect, especially for edge cases like these, so advertising each parity fix as a behavior change adds noise rather than information. Queries that vtgate pushes down to MySQL were never affected either way. Changelog callouts remain for changes that diverge from or go beyond MySQL semantics.

// A sign is allowed at the start of the number and at the start of
// the exponent, nowhere else. With no exponent seen yet expPos is
// -1, so the second test collapses into the first.
if i != start && i != expPos+1 {
break next
}
case s[i] >= '0' && s[i] <= '9':
Expand Down Expand Up @@ -215,17 +220,21 @@ next:
var si string
switch {
case dotPos == -1 && expPos == -1:
si = s[:i]
si = s[start:i]
case expPos == -1:
si = s[:dotPos] + s[dotPos+1:i]
si = s[start:dotPos] + s[dotPos+1:i]
exp -= int64(i - dotPos - 1)
case dotPos == -1:
si = s[:expPos]
si = s[start:expPos]
default:
si = s[:dotPos] + s[dotPos+1:expPos]
si = s[start:dotPos] + s[dotPos+1:expPos]
exp -= int64(expPos - dotPos - 1)
}

// fastparse reads a leading minus but not a leading plus, which the scanner
// above accepts because MySQL does.
si = strings.TrimPrefix(si, "+")

if len(si) <= 18 {
var v int64
v, err = fastparse.ParseInt64(si, 10)
Expand All @@ -237,7 +246,7 @@ next:

var expOverflow bool
if expPos != -1 {
e, _ := fastparse.ParseInt64(s[expPos+1:i], 10)
e, _ := fastparse.ParseInt64(strings.TrimPrefix(s[expPos+1:i], "+"), 10)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think accepting the exponent + exposes another MySQL conversion boundary here:

s71 := "0." + strings.Repeat("0", 71) + "1e+73"
s72 := "0." + strings.Repeat("0", 72) + "1e+74"

This change correctly moves s71 from 0 to 10, matching MySQL, but it also moves s72 from 0 to 10 while MySQL 8.0 and 8.4 still return 0. MySQL limits the mantissa to its decimal buffer before applying the exponent, whereas this path retains the extra digit and shifts it back into significance.

There’s a similar overflow boundary: 1e+18446744073709551615 becomes the largest decimal in both MySQL and Vitess, but the next exponent becomes zero in MySQL while the signed ParseInt64 calls saturate and Vitess still returns the largest value.

Should these string conversions use a separate MySQL-compatible coercion path which applies the existing mantissa limits before the exponent and distinguishes uint64 overflow? It should probably cover both string branches in evalToDecimal, while preserving prefix behaviour such as 1e+5x returning 100000.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, but this seems to be a pre-existing issue. I opened #20742 to track it.

switch {
case e > ExponentLimit:
e = ExponentLimit
Expand All @@ -249,6 +258,13 @@ next:
exp += e
}

// Scaling zero by a positive power of ten leaves zero, but keeping the
// exponent renders it as that many leading zeros: '0e5' would format as
// 000000 where MySQL prints 0. A negative exponent is left alone, since it
// is the scale a zero is written to.
if exp > 0 && d.value.Sign() == 0 {
exp = 0
}
d.exp = int32(exp)

for i < maxLen {
Expand Down
80 changes: 80 additions & 0 deletions go/mysql/json/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ func TestParseRawNumber(t *testing.T) {
func TestParseNumberTooBigForDouble(t *testing.T) {
tooManyDigits := "1" + strings.Repeat("0", 309)

// The subnormal 2.2250738585072011e-308, written out to nearly eight
// hundred digits of fraction.
longFraction := "2.22507385850720113605740979670913197593481954635164564802342610972482222202107694551652952390813508" +
"7914149158913039621106870086438694594645527657207407820621743379988141063267329253552286881372149012" +
"9811224514518898490572223072852551331557550159143974763979834118019993239625482890171070818506906306" +
"6665599493827577257201576306269066333264756530000924588831643303777979186961204949739037782970490505" +
"1080609940730262937128958950003583799967207254304360284078895771796150945516748243471030702609144621" +
"5722898802581825451803257070188608721131280795122334262883686223215037756666225039825343359745688844" +
"2390026549819838548794829220689472168983109969836584681402285424333066033985088644580400103493397042" +
"7567186443383770486037861622771738545623065874679014086723327636718751234567890123456789012345678901" +
"e-308"

t.Run("accepted", func(t *testing.T) {
for _, doc := range []string{
"1e308",
Expand All @@ -93,6 +105,7 @@ func TestParseNumberTooBigForDouble(t *testing.T) {
"-1.7976931348623157e308",
"99999999999999999999999999999999999999999",
"1" + strings.Repeat("0", 307),
"1" + strings.Repeat("0", 308),
// Underflow keeps the document valid and reads as zero.
"1e-400",
"1e-1000",
Expand Down Expand Up @@ -121,7 +134,14 @@ func TestParseNumberTooBigForDouble(t *testing.T) {
"0.00e310",
"0.1e309",
"0.01e310",
// The largest double itself, written so that its exponent stands
// past the bound until the fraction buys the places back.
"0.017976931348623157e+310",
"0." + strings.Repeat("0", 400) + "1e700",
// A fraction contributes seventeen significant digits; the
// hundreds behind these move neither the value nor the decimal
// point, however many there are.
longFraction,
} {
t.Run(startEndString(doc), func(t *testing.T) {
var p Parser
Expand Down Expand Up @@ -155,6 +175,7 @@ func TestParseNumberTooBigForDouble(t *testing.T) {
// Within the written bound, but too big once converted.
"10e308",
"1" + strings.Repeat("0", 30) + "e279",
"0.017976931348623159e+310",
// More digits than a double has room for. The digits are read before
// the exponent is applied, so a negative exponent does not buy the
// room back however far it moves the decimal point afterwards.
Expand Down Expand Up @@ -203,6 +224,31 @@ func TestParseNumberTooBigForDouble(t *testing.T) {
}
})

// The int a negative exponent accumulates into stops taking digits once
// another could overflow it. Everything written by then already sits far
// below the smallest double, so however much further the spelling runs,
// these stay valid and read as zero. MySQL 8.0.46 reads each of them the
// same way.
t.Run("a negative exponent around the stop of the int it accumulates into", func(t *testing.T) {
for _, doc := range []string{
"1e-214748363",
"1e-214748364",
"1e-21474836311",
"1e-00011111111111",
"-1e-00011111111111",
} {
t.Run(doc, func(t *testing.T) {
var p Parser
v, err := p.Parse(doc)
require.NoError(t, err)

f, ok := v.Float64()
require.True(t, ok)
require.Zero(t, f)
})
}
})

// The significand accumulates one digit at a time, and each step rounds
// the multiplication and the addition separately, the way MySQL's builds
// run the loop. Fusing the two into one rounding — which the Go compiler
Expand Down Expand Up @@ -288,10 +334,13 @@ func TestParseNumberGrammar(t *testing.T) {
"007", "-003", "01", "00", "00.5", "01.5", "[007]",
// A decimal point missing a digit on one side.
".2", "-.2", "12.", "-12.", "1.e5", `{"a": .2}`, "[12.]",
// An exponent missing its digits.
"1e", "1e_", "1e+", "1e-", "[1e]",
// A written plus.
"+1", "+1.5", "+0", "[+1]",
// Not a number at all.
"nan", "NaN", "NAN", "[nan]", `{"a": nan}`, "-nan", ".", "-",
"inf", "-inf", "Infinity", "[inf]",
} {
t.Run(doc, func(t *testing.T) {
var p Parser
Expand All @@ -302,6 +351,37 @@ func TestParseNumberGrammar(t *testing.T) {
})
}

// TestParseNumberIntegerBoundaries walks the spellings on either side of each
// integer width a JSON number can outgrow — int32, uint32, int64, uint64 —
// until only a double can hold it. Every one of them is a number to MySQL,
// exact while an int64 or a uint64 still holds it and approximate once only a
// double does.
func TestParseNumberIntegerBoundaries(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove tests that pass without the fix

This standalone test exercises only the unchanged Parser.Parse and Value.NumberType paths: the commit modifies no production file in go/mysql/json, and the decimal change is reached through Value.Decimal, which this test never calls. Consequently, every case passes against the parent implementation as well, adding CI work without guarding this fix; remove it from this change or pair it with the production change it is intended to protect.

AGENTS.md reference: AGENTS.md:L79-L80

Useful? React with 👍 / 👎.

@arthurschreiber arthurschreiber Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The observation matches this PR's diff, but the full context is a PR split: the production change this test was written against — the RapidJSON-style number reading in go/mysql/json/parser.go — merged separately as #20722, which is now this branch's merge base. The test was authored while that parser change still lived on this branch, and the split orphaned it here.

Verified before deciding what to do with it: the merge base has only generic NumberType assertions (Signed for integers, Float for non-integers, in the round-trip tests) and pins none of these specific boundary values — no 9223372036854775808, 4294967296, or 18446744073709551616 anywhere. So this is novel regression coverage for #20722's merged behavior, not duplicate characterization, and it stays here rather than in a separate test-only PR.

One deliberate note: the 4294967296 → NumberTypeSigned row pins current behavior that #20739 will flip to Unsigned to match MySQL — it's a characterization pin that PR will consciously update, not an endorsement of the current classification.

for _, tc := range []struct {
doc string
n NumberType
}{
{"-2147483648", NumberTypeSigned},
{"-2147483649", NumberTypeSigned},
{"4294967295", NumberTypeSigned},
{"4294967296", NumberTypeSigned},
{"9223372036854775807", NumberTypeSigned},
{"9223372036854775808", NumberTypeUnsigned},
{"-9223372036854775808", NumberTypeSigned},
{"-9223372036854775809", NumberTypeFloat},
{"18446744073709551615", NumberTypeUnsigned},
{"18446744073709551616", NumberTypeFloat},
} {
t.Run(tc.doc, func(t *testing.T) {
var p Parser
v, err := p.Parse(tc.doc)
require.NoError(t, err)
require.Equal(t, TypeNumber, v.Type())
require.Equal(t, tc.n, v.NumberType())
})
}
}

// TestParseErrorAbbreviatesTheDocument covers how much of a rejected document
// its error names. Nothing bounds how long a document may be, and Parse copies
// the message it wraps, so naming the text in full hands a client its own
Expand Down
40 changes: 40 additions & 0 deletions go/vt/vtgate/evalengine/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,46 @@ func TestCompilerSingle(t *testing.T) {
}
}

// TestCastInvalidJSON pins that text MySQL refuses as a JSON document is
// refused by both evaluation paths. The differential suite cannot pin this
// direction: its comparison excuses a MySQL "Invalid JSON text" error when
// the local evaluation succeeds, so a cast that wrongly started accepting one
// of these spellings would stay green there.
func TestCastInvalidJSON(t *testing.T) {
testCases := []struct {
expression string
wantErr string
}{
{`CAST('+1' AS JSON)`, "invalid number"},
{`CAST(' +1' AS JSON)`, "invalid number"},
{`CAST('+1.5' AS JSON)`, "invalid number"},
{`CAST('1e309' AS JSON)`, "number too big to be stored in double"},
}

venv := vtenv.NewTestEnv()
for _, tc := range testCases {
t.Run(tc.expression, func(t *testing.T) {
expr, err := venv.Parser().ParseExpr(tc.expression)
require.NoError(t, err)

cfg := &evalengine.Config{
Collation: collations.CollationUtf8mb4ID,
Environment: venv,
NoConstantFolding: true,
}
converted, err := evalengine.Translate(expr, cfg)
require.NoError(t, err)

env := evalengine.EmptyExpressionEnv(venv)
_, err = env.EvaluateAST(converted)
require.ErrorContains(t, err, tc.wantErr)

_, err = env.Evaluate(converted)
require.ErrorContains(t, err, tc.wantErr)
})
}
}

func TestBindVarLiteral(t *testing.T) {
testCases := []struct {
expression string
Expand Down
23 changes: 23 additions & 0 deletions go/vt/vtgate/evalengine/testcases/cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var Cases = []TestCase{
{Run: LargeDecimals},
{Run: LargeIntegers},
{Run: DecimalClamping},
{Run: SignedExponents},
{Run: BitwiseOperatorsUnary},
{Run: BitwiseOperators},
{Run: WeightString},
Expand Down Expand Up @@ -914,6 +915,28 @@ func DecimalClamping(yield Query) {
}
}

// SignedExponents covers numeric text carrying a written sign, on the number
// itself and on its exponent. MySQL reads both, so a cast or a JSON comparison
// over one has to land on the same value.
func SignedExponents(yield Query) {
mantissas := []string{"1", "+1", "-1", "1.5", "+1.5", "-1.5", "0", "+0", "-0"}
exponents := []string{"", "e5", "e+5", "E+5", "e-5", "E-5", "e+0", "e-0"}

for _, mantissa := range mantissas {
for _, exponent := range exponents {
literal := "'" + mantissa + exponent + "'"
yield(fmt.Sprintf("CAST(%s AS DECIMAL(20, 6))", literal), nil, false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth adding an actual JSON comparison here as well? The comment and PR description mention the JSON comparison failure, but these generated cases currently only exercise decimal casts, double casts, and + 0.

For the JSON-valid mantissas, perhaps something along these lines would cover that path directly:

if !strings.HasPrefix(mantissa, "+") {
	yield(fmt.Sprintf(
		"CAST(%s AS JSON) = CAST(%s AS JSON)",
		literal, literal,
	), nil, false)
}

The leading-plus mantissas need skipping because they are not valid JSON numbers, but a case such as 1e+5 would fail before this fix and exercise the comparison path end to end.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 95b3684

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One wrinkle I missed when suggesting that the leading-plus rows be skipped: Vitess's JSON lexer currently accepts them. This decimal fix therefore changes:

CAST('+1' AS JSON) = CAST('1' AS JSON)

from a range error on the base to true at this head, while MySQL rejects the cast as invalid JSON.

Could we pin that rejection and tighten the JSON lexer at the same time? The decimal parser should continue accepting + for ordinary numeric text, but JSON's leading sign could remain minus-only:

// JSON permits an optional minus, not a plus.
if s[i] == '-' {
    i++
}

The exponent branch would still accept 1e+2. This also prevents the stacked #20723 from settling +1 as signed even though Int64() cannot parse it. Since this PR can be merged and backported independently, it seems safest for the small parser fix to land with or before it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e4f743c should make sure the case you pointed out is covered.

yield(fmt.Sprintf("CAST(%s AS DOUBLE)", literal), nil, false)
yield(literal+" + 0", nil, false)

// JSON comparison reads the number through a decimal too, so it is
// worth exercising directly. A leading plus is not a JSON number,
// so those spellings pin the cast being rejected rather than a value.
yield(fmt.Sprintf("CAST(%s AS JSON) = CAST(%s AS JSON)", literal, literal), nil, false)
Comment on lines +934 to +937

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert leading-plus JSON rejection directly

For the + mantissas, these expressions do not actually pin the claimed JSON rejection: MySQL returns the known Invalid JSON text error, and compareResult in integration/fuzz_test.go lines 326–330 accepts that remote error even when local evaluation succeeds. Consequently, a regression that makes Vitess accept CAST('+1' AS JSON) would leave these new cases green; use a direct expected-error test or a comparison mode that requires both sides to fail.

AGENTS.md reference: AGENTS.md:L79-L80

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed: knownErrors in integration/fuzz_test.go whitelists exactly Invalid JSON text in argument …, so these rows only pin the direction where Vitess is stricter than MySQL, not a regression toward accepting the document.

Addressed in cb6ab55 with a direct expected-error test, TestCastInvalidJSON in evalengine/compiler_test.go: CAST('+1' AS JSON), CAST(' +1' AS JSON), CAST('+1.5' AS JSON) and CAST('1e309' AS JSON) (which sits in the same comparison blind spot) must error on both the AST and compiled evaluation paths. The differential rows stay as they are, since they still pin the opposite direction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also carry a few representative spellings through a JSON string? The comparison here exercises JSON numbers through Value.Decimal(), but a nested cast reaches the separate JSON-string branch in evalToDecimal.

These cases fail on the parent and match MySQL with this change:

for _, jsonString := range []string{"1e+5", "+1", " -1"} {
	literal := fmt.Sprintf(`'"%s"'`, jsonString)
	yield(fmt.Sprintf(
		"CAST(CAST(%s AS JSON) AS DECIMAL(20, 6))",
		literal,
	), nil, false)
}

Keeping it to these three rows should cover that caller without repeating the full generated matrix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea — added in 4d46160, pretty much verbatim.

}
}
}

func BitwiseOperatorsUnary(yield Query) {
for _, op := range []string{"~", "BIT_COUNT"} {
for _, rhs := range inputBitwise {
Expand Down
Loading