Repository navigation
fix(duckdb): only treat _ between two digits as a digit separator [DEEPSEEK] - #8892
Conversation
`_scan_number` consumed every underscore it met while the dialect allows underscore separators, so a number continued past characters DuckDB ends it at: `SELECT 1__2` became `SELECT 12`, `1_` became `1`, and `1e_5` became `1e5`. DuckDB only reads `_` as a separator between two digits; anywhere else the number ends and the rest starts the implicit alias. The underscore branch now requires a digit on both sides, and an exponent marker is no longer consumed when it is directly followed by an underscore (an exponent has to start with a digit). Closes tobymao#8889
| ): | ||
| # An exponent starts with a digit, so `1e_5` ends at `1` and | ||
| # `e_5` is the next token | ||
| break |
There was a problem hiding this comment.
This introduces a ClickHouse regression:
-- Original (returns 7)
SELECT 1e_5foo
FROM (SELECT 7 AS `1e_5foo`)
-- Transpiled (returns 1)
SELECT 1 AS e_5foo
FROM (SELECT 7 AS "1e_5foo")The new early break bypasses the identifiers_can_start_with_digit handling below, splitting the identifier into a number and alias. Could we preserve that setting and add a Clickhouse regression test?
There was a problem hiding this comment.
Good catch — that setting was the one path I missed. Fixed in 68a8d03.
The exponent-marker break now skips dialects where identifiers_can_start_with_digit is set, so that token falls through to the identifier branch again and stays whole:
-- before the fix
SELECT 1 AS e_5foo FROM (SELECT 7 AS "1e_5foo")
-- now
SELECT 1e_5foo FROM (SELECT 7 AS "1e_5foo")Added test_number_like_identifiers in tests/dialects/test_clickhouse.py covering that statement (plus SELECT 1e_5 and SELECT 1_000, to keep the separator rule covered for ClickHouse). With the guard removed the new case fails with exactly the split above; with it in place the file passes and the full suite is 1465 passed / 22795 subtests.
One consequence worth flagging in the same dialect: SELECT 1e_5 now tokenizes as the identifier 1e_5 rather than as the number 1e5 it used to collapse to, because _ there no longer counts as a separator. That follows from the same setting rather than from the DuckDB rule, but say the word if you'd rather keep the old reading for that shape and I'll special-case it.
…whole When identifiers may start with a digit, `1e_5foo` is a single identifier. The new exponent-marker break ran before that path and split it into `1` plus an alias, so the statement no longer round-tripped. Skip the break where identifiers_can_start_with_digit is set, which restores the previous tokenization for those dialects, and cover it with a ClickHouse case.
Fixes #8889.
Problem
_scan_numberinsqlglot/tokenizer_core.pyconsumed every_it met whileNUMBERS_CAN_BE_UNDERSCORE_SEPARATEDis set (duckdb, clickhouse), so a numeric literal kept going past the point where DuckDB ends it. DuckDB only reads_as a digit separator between two digits; anywhere else the number ends and the rest starts the implicit alias.Before, with
read="duckdb":SELECT 1__2SELECT 12SELECT 1 AS __2SELECT 1._5SELECT 1.5SELECT 1. AS _5SELECT 1_SELECT 1SELECT 1 AS _SELECT 1e_5SELECT 1e5SELECT 1 AS e_5Fix
Two conditions in
_scan_number:_charand the char after_), instead of accepting any underscore;_, because an exponent has to start with a digit. So1e_5ends the number at1rather than leaving a dangling1etoken.Numbers that use the separator normally are unchanged:
1_000->1000,1_0_0->100.Verification
Added
test_digit_separatorstotests/dialects/test_duckdb.pycovering the four cases above plus the three well-formed separator forms.test_digit_separatorsfails onSELECT 1__2('SELECT 12' != 'SELECT 1 AS __2').tests.dialects.test_duckdb64 tests OK.SKIP_INTEGRATION=1 python -m unittest: 1568 tests, OK (60 skipped), no new failures.ruff check/ruff format --checkon both touched files: formatted, and no new findings on the changed lines.Note
ClickHouse uses the same dialect flag, so it inherits the same rule. I verified the behaviour against DuckDB only, and did not check ClickHouse's own rule for these inputs.