Skip to content

fix: keep backticks on names containing $ in fmt - #6322

Merged
max-sixty merged 2 commits into
mainfrom
fix/fmt-dollar-ident
Sep 15, 2026
Merged

max-sixty merged 2 commits into
mainfrom
fix/fmt-dollar-ident

Conversation

@prql-bot

@prql-bot prql-bot commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

prqlc fmt drops the backticks around any name containing $, and the result no longer lexes as the same name — $ starts a $param token in PRQL, so the formatter turns one identifier into an identifier followed by a parameter.

$ printf 'from t\nselect {`a$b`}\n' | prqlc fmt
from t
select {a$b}

$ printf 'from t\nselect {`a$b`}\n' | prqlc fmt | prqlc compile
Error:
   ╭─[ :2:9 ]

 2 │ select {a$b}
   │         ┬
   │         ╰── expected a function, but found `this.t.a`
───╯

This is the same round-trip break as #6200 and #6214, at the one remaining hole in the rule those two consolidated. valid_prql_ident() accepts $ in both leading and subsequent positions because its regex was copied from the SQL-side utils::valid_ident(), where $ genuinely is a Postgres identifier character. The PRQL lexer's ident_part() never accepts it. Dropping $ from the codegen regex is the whole fix; params are written separately as format!("${id}") and don't go through this path.

This change doesn't move the SQL: `a$b` compiles to SELECT a$b FROM t before and after it. Worth knowing when reading that claim, though — the default output path renders it as SELECT a $b FROM t, because sqlformat splits the $ the same way it does in #3188, so the SQL a user actually sees names a different identifier and isn't valid in the dialects that accept a$b. That's a pre-existing bug independent of fmt, and it currently has no open tracker: #3286 was closed as a duplicate of #3077, whose fix covered $param but not an identifier containing $.

Failure modes and verification

Three shapes, all now formatted with backticks intact:

Source Before this PR Re-parses as
select {`a$b`} select {a$b} error — a applied to $b
select {x = t.`$foo`} select {x = t.$foo} error — $foo where an ident part is expected
let `$foo` = 5 let $foo = 5 error — $foo where a declaration name is expected

The regression test is codegen::ast::test::test_quoted_dollar_in_name, covering all three. assert_is_formatted round-trips through the parser, so each case asserts the formatted output re-parses to the same source. Restoring $ to the character class while keeping the test fails it:

---- codegen::ast::test::test_quoted_dollar_in_name stdout ----
assertion failed: `(left == right)`
  left: `"from t\nselect {`a$b`}"`
 right: `"from t\nselect {a$b}"`

No snapshot moved — nothing in the repo's test corpus uses a $ in an identifier.

task prqlc:pull-request runs 763 tests; 762 pass. The one failure is queries::results::read_csv, which needs to download DuckDB's json extension and can't reach extensions.duckdb.org from this sandbox — unrelated to this change. cargo fmt --check and cargo clippy -p prqlc --all-targets --no-default-features --features=default,lsp -- -D warnings are both clean.

One related divergence I did not change: display_ident_part() in prqlc-parser has the same $-at-start hole, as the other surviving copy of the "needs backticks" rule. Since #6214 routed ExprKind::Ident through write_ident_part, that copy only backs Ident's Display impl — error messages and Debug output, which nothing re-parses — so there's no round-trip to break there. Worth folding into write_ident_part eventually, but that's a separate change with its own snapshot churn.

@prql-bot prql-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Two points, neither in the regex change itself.

No changelog entry. development.md asks for a CHANGELOG.md line on user-facing changes, and this is one — the sibling fmt fix #6311 has an entry in the same [unreleased] block. Pushing one to this branch.

The description's SQL claim only holds with --no-format. It says `a$b` "still compiles to SELECT a$b FROM t, which is correct for the dialects that allow it". The default output path splits the $:

$ printf 'from t\nfilter `a$b` == 1\n' | prqlc compile --hide-signature-comment
SELECT
  *
FROM
  t
WHERE
  a $b = 1

a $b is a different identifier from the one that was written, and it isn't valid in the dialects that accept a$b either. This is sqlformat splitting the token rather than anything in this diff — same class as #3188 — but the description asserts the opposite of what a user of the default path sees, so I'll correct it there.

Worth flagging separately, since it's adjacent and currently untracked: the parameter half of this was fixed ($sd renders intact now, via #3077), but an identifier containing $ still splits, and #3286 was closed as a duplicate of #3077 so nothing open covers it. Closing it on the PRQL side would mean dropping $ from utils::valid_ident() as well, so these names get dialect-quoted as "a$b" — that changes generated SQL for anyone using $ in identifiers on Postgres today, which is a maintainer call rather than a follow-on to this fix.

@max-sixty
max-sixty merged commit e31e910 into main Sep 15, 2026
38 checks passed
@max-sixty
max-sixty deleted the fix/fmt-dollar-ident branch September 15, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants