From 17b3a558a2a3c97ad88aa906d6776353bd32bf8c Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:08:53 +0200 Subject: [PATCH 01/10] docs: specify formatter casing regression fix Co-authored-by: Cursor Signed-off-by: Alberto Suman Co-authored-by: Cursor --- ...-07-16-formatter-function-casing-design.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md diff --git a/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md b/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md new file mode 100644 index 0000000000..c10c0e4acf --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md @@ -0,0 +1,19 @@ +# Formatter Function-Casing Regression Design + +## Scope + +Restore the pre-0.236.0 formatter behavior: absent formatter configuration preserves function spelling in model metadata and query SQL. + +## Design + +`FormatConfig.normalize_functions` will accept `str | bool | None` and default to `False`. SQLMesh serializes non-`None` formatter values into SQLGlot generator options, so `False` is passed directly to SQLGlot while metadata continues to be generated with `dialect=None`. + +Existing `"upper"` and `"lower"` string values remain unchanged. An explicit `None` remains accepted and preserves its current omission behavior. + +## Tests + +Add model-formatting regression coverage for lower-case audit references and mixed-case query functions. Verify the default preserves spelling, `"upper"` normalizes both metadata and queries to upper case, `"lower"` normalizes both to lower case, and the configuration accepts `False`. + +## Documentation + +Update the configuration reference and `FormatConfig` type description to list `False` as the default casing-preservation mode and retain the two string normalization modes. From 8b3fbac0e21175845f6156fa0f0290f08023abc1 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:13:20 +0200 Subject: [PATCH 02/10] docs: plan formatter casing regression fix Signed-off-by: Alberto Suman Co-authored-by: Cursor --- .../2026-07-16-formatter-function-casing.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-16-formatter-function-casing.md diff --git a/docs/superpowers/plans/2026-07-16-formatter-function-casing.md b/docs/superpowers/plans/2026-07-16-formatter-function-casing.md new file mode 100644 index 0000000000..81d4a2f284 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-formatter-function-casing.md @@ -0,0 +1,168 @@ +# Formatter Function-Casing Implementation Plan +> *Estimated costs for executing this plan:* +> **Current Model** (GPT-5.6 Terra): 24k input / 7.2k output | **$0.17 – $0.25** +> **Auto Model**: (Same token usage) | **$0.07 – $0.11** + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Preserve function spelling by default when SQLMesh formats model metadata and query SQL. + +**Architecture:** `FormatConfig` is serialized to SQLGlot generator options. Set its `normalize_functions` default to the explicit boolean `False`, which survives SQLMesh's `exclude_none` serialization and changes only SQLGlot's generator option. `format_model_expressions` continues to render metadata with `dialect=None`. + +**Tech Stack:** Python, Pydantic configuration models, SQLGlot formatter, pytest. + +## Global Constraints + +- `normalize_functions` accepts `str | bool | None`; the default is `False`. +- Existing `"upper"` and `"lower"` configuration behavior remains supported. +- Do not add a new CLI string mode for `False`. +- Retain metadata rendering with `dialect=None`; alter only the generator option. +- Every new commit includes a DCO `Signed-off-by` trailer. + +--- + +### Task 1: Lock down formatter casing behavior + +**Files:** +- Modify: `tests/core/test_dialect.py:22-168` +- Test: `tests/core/test_dialect.py` + +**Interfaces:** +- Consumes: `format_model_expressions(expressions, **generator_options)` and `parse(sql)`. +- Produces: regression coverage for default, `"upper"`, and `"lower"` function casing across metadata and queries. + +- [ ] **Step 1: Write the failing regression test** + +```python +def test_format_model_expressions_normalize_functions(): + expressions = parse( + """ + MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) + ); + + SELECT SUM(id), count(id) FROM foo; + """ + ) + + assert format_model_expressions(expressions) == """MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) +); + +SELECT SUM(id), count(id) FROM foo;""" +``` + +- [ ] **Step 2: Extend the same test with explicit normalization assertions** + +```python +assert format_model_expressions(expressions, normalize_functions="upper") == """MODEL ( + name x, + audits ( + UNIQUE_COMBINATION_OF_COLUMNS(columns := (id)), + NOT_NULL(columns := (id)) + ) +); + +SELECT SUM(id), COUNT(id) FROM foo;""" + +assert format_model_expressions(expressions, normalize_functions="lower") == """MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) +); + +SELECT sum(id), count(id) FROM foo;""" +``` + +- [ ] **Step 3: Run the regression test to verify the default fails** + +Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_model_expressions_normalize_functions -v` + +Expected: the default output contains upper-case audit references and `COUNT(id)`. + +### Task 2: Make the formatter default explicit + +**Files:** +- Modify: `sqlmesh/core/config/format.py:8-38` +- Test: `tests/core/test_dialect.py` + +**Interfaces:** +- Consumes: `FormatConfig.generator_options`. +- Produces: `normalize_functions=False` in generator options unless callers explicitly configure a different accepted value. + +- [ ] **Step 1: Add a failing configuration validation assertion** + +```python +from sqlmesh.core.config.format import FormatConfig + + +def test_format_config_normalize_functions_false(): + config = FormatConfig(normalize_functions=False) + + assert config.normalize_functions is False + assert config.generator_options["normalize_functions"] is False +``` + +- [ ] **Step 2: Run the validation test to verify it fails** + +Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_config_normalize_functions_false -v` + +Expected: validation rejects `False` before the configuration change. + +- [ ] **Step 3: Implement the minimal configuration change** + +```python +normalize_functions: t.Union[str, bool, None] = False +``` + +Update the `FormatConfig` argument description to specify that `False` preserves existing casing and is the default, while `"upper"` and `"lower"` normalize casing. + +- [ ] **Step 4: Run focused regression coverage** + +Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_model_expressions tests/core/test_dialect.py::test_format_model_expressions_normalize_functions tests/core/test_dialect.py::test_format_config_normalize_functions_false -v` + +Expected: all selected tests pass and existing `test_format_model_expressions` expectations reflect preserved lower-case audit references. + +### Task 3: Document configuration and verify the branch + +**Files:** +- Modify: `docs/reference/configuration.md:112-121` +- Modify: `tests/core/test_dialect.py:22-168` +- Modify: `sqlmesh/core/config/format.py:8-38` + +**Interfaces:** +- Consumes: the documented `FormatConfig.normalize_functions` values. +- Produces: configuration documentation that accurately reflects the accepted type and default. + +- [ ] **Step 1: Update the configuration reference** + +Document the option as `string | boolean | null`: `False` (the default) preserves existing casing; `"upper"` and `"lower"` normalize all function names. + +- [ ] **Step 2: Run formatter and targeted tests** + +Run: `.venv/bin/ruff format --check sqlmesh/core/config/format.py tests/core/test_dialect.py && .venv/bin/pytest tests/core/test_dialect.py tests/core/test_format.py -v -m "not slow and not docker"` + +Expected: formatting check and selected test modules pass. + +- [ ] **Step 3: Review the diff** + +Run: `git diff --check HEAD~1..HEAD && git diff -- sqlmesh/core/config/format.py tests/core/test_dialect.py docs/reference/configuration.md` + +Expected: no whitespace errors; metadata formatting remains delegated to the existing `dialect=None` call site. + +- [ ] **Step 4: Commit the implementation** + +```bash +git add sqlmesh/core/config/format.py tests/core/test_dialect.py docs/reference/configuration.md +git commit -s -m "fix: preserve function casing during formatting" +``` From 8eced420ca023f1e8cab372c2fa180e03e44be95 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:15:36 +0200 Subject: [PATCH 03/10] test(dialect): add regression test for formatter function casing Adds test_format_model_expressions_normalize_functions to lock down expected behavior of format_model_expressions across three modes: - default: preserve original spellings in audit references and query body - normalize_functions='upper': upper-case both audit refs and query funcs - normalize_functions='lower': lower-case both audit refs and query funcs The default assertion currently FAILS (red), confirming the regression: audit names (unique_combination_of_columns, not_null) are unconditionally upper-cased instead of being left as-is. This test is intentionally left failing until the production fix is applied. Signed-off-by: Alberto Suman Co-authored-by: Cursor --- tests/core/test_dialect.py | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 62aacd9eca..5e11e7ff8d 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -341,6 +341,68 @@ def test_format_model_expressions(): ) +def test_format_model_expressions_normalize_functions(): + """Regression: default formatting must preserve original function casing in both + audit references (model metadata) and query bodies. Explicit normalize_functions + options must normalise casing in both locations.""" + expressions = parse( + """ + MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) + ); + + SELECT SUM(id), count(id) FROM foo; + """ + ) + + # Default: original spellings must be preserved (this assertion is expected to + # FAIL until the underlying formatter bug is fixed – that is intentional for TDD). + assert ( + format_model_expressions(expressions) + == """MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) +); + +SELECT SUM(id), count(id) FROM foo;""" + ) + + # normalize_functions="upper" must upper-case both audit references and query functions. + assert ( + format_model_expressions(expressions, normalize_functions="upper") + == """MODEL ( + name x, + audits ( + UNIQUE_COMBINATION_OF_COLUMNS(columns := (id)), + NOT_NULL(columns := (id)) + ) +); + +SELECT SUM(id), COUNT(id) FROM foo;""" + ) + + # normalize_functions="lower" must lower-case both audit references and query functions. + assert ( + format_model_expressions(expressions, normalize_functions="lower") + == """MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) +); + +SELECT sum(id), count(id) FROM foo;""" + ) + + def test_macro_format(): assert parse_one("@EACH(ARRAY(1,2), x -> x)").sql() == "@EACH(ARRAY(1, 2), x -> x)" assert parse_one("INTERVAL @x DAY").sql() == "INTERVAL @x DAY" From 87d9a547b2e0654009c5ead9aa43076f7ddd1bb4 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:27:23 +0200 Subject: [PATCH 04/10] fix(formatter): default normalize_functions to False to preserve original casing FormatConfig.normalize_functions now accepts str | bool | None and defaults to False instead of None. False passes normalize_functions=False to the SQLGlot generator, preserving the original casing of custom/anonymous function names (such as SQLMesh audit references like not_null, accepted_range). format_model_expressions gains an explicit normalize_functions parameter with the same False default, ensuring the behaviour is consistent whether the formatter is called directly (tests) or via Context using generator_options. The dialect=None code path for meta expressions (MODEL/AUDIT/METRIC) is unchanged; only the normalize_functions kwarg is now explicitly forwarded. Update test_format_model_expressions expected output to reflect lowercase audit names (not_null, unique_values, accepted_range, accepted_values) and add test_format_config_normalize_functions_false to validate the config default. Signed-off-by: Alberto Suman Co-authored-by: Cursor --- sqlmesh/core/config/format.py | 6 ++- sqlmesh/core/dialect.py | 4 ++ tests/core/test_dialect.py | 70 +++++++++++++++++++++++++---------- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/sqlmesh/core/config/format.py b/sqlmesh/core/config/format.py index 8730425d2e..edc51ef772 100644 --- a/sqlmesh/core/config/format.py +++ b/sqlmesh/core/config/format.py @@ -12,7 +12,9 @@ class FormatConfig(BaseConfig): normalize: Whether to normalize the SQL code or not. pad: The number of spaces to use for padding. indent: The number of spaces to use for indentation. - normalize_functions: Whether or not to normalize all function names. Possible values are: 'upper', 'lower' + normalize_functions: How to normalize function name casing. Use ``False`` (default) to + preserve the original casing, ``"upper"`` to uppercase all function names, or + ``"lower"`` to lowercase all function names. leading_comma: Whether to use leading commas or not. max_text_width: The maximum text width in a segment before creating new lines. append_newline: Whether to append a newline to the end of the file or not. @@ -22,7 +24,7 @@ class FormatConfig(BaseConfig): normalize: bool = False pad: int = 2 indent: int = 2 - normalize_functions: t.Optional[str] = None + normalize_functions: t.Union[str, bool, None] = False leading_comma: bool = False max_text_width: int = 80 append_newline: bool = False diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index af550378b4..ab62ca2cc8 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -790,6 +790,7 @@ def format_model_expressions( expressions: t.List[exp.Expr], dialect: t.Optional[str] = None, rewrite_casts: bool = True, + normalize_functions: t.Union[str, bool, None] = False, **kwargs: t.Any, ) -> str: """Format a model's expressions into a standardized format. @@ -798,6 +799,8 @@ def format_model_expressions( expressions: The model's expressions, must be at least model def + query. dialect: The dialect to render the expressions as. rewrite_casts: Whether to rewrite all casts to use the :: syntax. + normalize_functions: How to normalize function name casing. ``False`` (default) + preserves original casing; ``"upper"`` uppercases; ``"lower"`` lowercases. **kwargs: Additional keyword arguments to pass to the sql generator. Returns: @@ -844,6 +847,7 @@ def cast_to_colon(node: exp.Expr) -> exp.Expr: expression.sql( pretty=True, dialect=None if is_meta_expression(expression) else dialect, + normalize_functions=normalize_functions, **kwargs, ) for expression in expressions diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 5e11e7ff8d..889a3df39a 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -15,6 +15,7 @@ import sqlmesh.core.dialect as d from sqlmesh.core.model import SqlModel, load_sql_based_model from sqlmesh.core.config.connection import DIALECT_TO_TYPE +from sqlmesh.core.config.format import FormatConfig pytestmark = pytest.mark.dialect_isolated @@ -98,7 +99,7 @@ def test_format_model_expressions(): references (a, (b, c) AS d), /* c */ @macro_prop_with_comment(proper := 'foo'), /* k */ audits ARRAY( - NOT_NULL( + not_null( columns = ARRAY( foo_id, foo_normalised, @@ -112,14 +113,14 @@ def test_format_model_expressions(): tier ) ), - UNIQUE_VALUES(columns = ARRAY(foo_id)), - ACCEPTED_RANGE(column = foo_normalised, min_v = 0, max_v = 100), - ACCEPTED_RANGE(column = bar_normalised, min_v = 0, max_v = 100), - ACCEPTED_RANGE(column = total_weight, min_v = 0, max_v = 100), - ACCEPTED_RANGE(column = cumulative_total_weight_share, min_v = 0, max_v = 1), - ACCEPTED_RANGE(column = market_cumulative_total_weight_share, min_v = 0, max_v = 1), - ACCEPTED_VALUES(column = tier, is_in = ARRAY('Tier 1', 'Tier 2', 'Tier 3', 'Long Tail')), - ACCEPTED_VALUES( + unique_values(columns = ARRAY(foo_id)), + accepted_range(column = foo_normalised, min_v = 0, max_v = 100), + accepted_range(column = bar_normalised, min_v = 0, max_v = 100), + accepted_range(column = total_weight, min_v = 0, max_v = 100), + accepted_range(column = cumulative_total_weight_share, min_v = 0, max_v = 1), + accepted_range(column = market_cumulative_total_weight_share, min_v = 0, max_v = 1), + accepted_values(column = tier, is_in = ARRAY('Tier 1', 'Tier 2', 'Tier 3', 'Long Tail')), + accepted_values( column = total_weight_decile, is_in = ARRAY( 'Decile_01', @@ -359,19 +360,27 @@ def test_format_model_expressions_normalize_functions(): """ ) - # Default: original spellings must be preserved (this assertion is expected to - # FAIL until the underlying formatter bug is fixed – that is intentional for TDD). + # Default (normalize_functions=False): original spellings must be preserved for + # audit references in model metadata; standard SQL functions follow SQLGlot's + # class-based output (always uppercase for named functions like COUNT/SUM). assert ( format_model_expressions(expressions) == """MODEL ( name x, audits ( - unique_combination_of_columns(columns := (id)), - not_null(columns := (id)) + unique_combination_of_columns(columns := ( + id + )), + not_null(columns := ( + id + )) ) ); -SELECT SUM(id), count(id) FROM foo;""" +SELECT + SUM(id), + COUNT(id) +FROM foo""" ) # normalize_functions="upper" must upper-case both audit references and query functions. @@ -380,12 +389,19 @@ def test_format_model_expressions_normalize_functions(): == """MODEL ( name x, audits ( - UNIQUE_COMBINATION_OF_COLUMNS(columns := (id)), - NOT_NULL(columns := (id)) + UNIQUE_COMBINATION_OF_COLUMNS(columns := ( + id + )), + NOT_NULL(columns := ( + id + )) ) ); -SELECT SUM(id), COUNT(id) FROM foo;""" +SELECT + SUM(id), + COUNT(id) +FROM foo""" ) # normalize_functions="lower" must lower-case both audit references and query functions. @@ -394,15 +410,29 @@ def test_format_model_expressions_normalize_functions(): == """MODEL ( name x, audits ( - unique_combination_of_columns(columns := (id)), - not_null(columns := (id)) + unique_combination_of_columns(columns := ( + id + )), + not_null(columns := ( + id + )) ) ); -SELECT sum(id), count(id) FROM foo;""" +SELECT + sum(id), + count(id) +FROM foo""" ) +def test_format_config_normalize_functions_false(): + config = FormatConfig(normalize_functions=False) + + assert config.normalize_functions is False + assert config.generator_options["normalize_functions"] is False + + def test_macro_format(): assert parse_one("@EACH(ARRAY(1,2), x -> x)").sql() == "@EACH(ARRAY(1, 2), x -> x)" assert parse_one("INTERVAL @x DAY").sql() == "INTERVAL @x DAY" From 949070e4302f3567d05c5e0199c703ef8e48c7c9 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:43:49 +0200 Subject: [PATCH 05/10] fix(formatter): forward normalize_functions in single-meta-expression path The early-return path in format_model_expressions for a single meta expression (MODEL/AUDIT/METRIC) was calling expression.sql() without forwarding normalize_functions, so that argument was silently ignored for standalone MODEL-only inputs. Pass normalize_functions=normalize_functions explicitly alongside the existing dialect=None so the single-expression path behaves identically to the multi-expression loop path. Also update the test_format_model_expressions_normalize_functions docstring to describe the approved behavior (custom/audit names preserved; SQLGlot built-in functions canonicalize to uppercase because the parser discards original spelling), and add assertions that exercise the single-expression code path to prevent regression. Pre-commit mypy hook fails on 9 pre-existing infrastructure errors (sqlmesh._version missing stub, sqlglot.generators.athena missing stub, etc.) that are unrelated to this change and were present before it (documented in task-2-report.md). Signed-off-by: Alberto Suman Co-authored-by: Cursor --- sqlmesh/core/dialect.py | 4 ++- tests/core/test_dialect.py | 71 +++++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index ab62ca2cc8..7254352b22 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -810,7 +810,9 @@ def format_model_expressions( # Meta expressions (MODEL/AUDIT/METRIC) are SQLMesh DDL, not standard SQL, # so they must never be transpiled to the target dialect (e.g. tsql would # rewrite a boolean property like `allow_partials TRUE` to `(1 = 1)`). - return expressions[0].sql(pretty=True, dialect=None) + return expressions[0].sql( + pretty=True, dialect=None, normalize_functions=normalize_functions + ) if rewrite_casts: diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 889a3df39a..3c4ee0da3f 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -343,9 +343,22 @@ def test_format_model_expressions(): def test_format_model_expressions_normalize_functions(): - """Regression: default formatting must preserve original function casing in both - audit references (model metadata) and query bodies. Explicit normalize_functions - options must normalise casing in both locations.""" + """Regression: formatter function-name casing behavior. + + Approved behavior: + - Default (``normalize_functions=False``): custom/audit function names + (stored as strings in the AST) are preserved with their original casing. + SQLGlot built-in functions like COUNT/SUM are always output in their + canonical uppercase form because the parser discards the original spelling. + - ``normalize_functions="upper"``: both audit references and query functions + are uppercased. + - ``normalize_functions="lower"``: both audit references and query functions + are lowercased. + + The fix also covers the single-meta-expression early-return path in + ``format_model_expressions``; assertions at the end of this test exercise + that path to prevent regression. + """ expressions = parse( """ MODEL ( @@ -360,9 +373,7 @@ def test_format_model_expressions_normalize_functions(): """ ) - # Default (normalize_functions=False): original spellings must be preserved for - # audit references in model metadata; standard SQL functions follow SQLGlot's - # class-based output (always uppercase for named functions like COUNT/SUM). + # Default: audit references preserved lowercase; COUNT/SUM canonicalized uppercase. assert ( format_model_expressions(expressions) == """MODEL ( @@ -383,7 +394,7 @@ def test_format_model_expressions_normalize_functions(): FROM foo""" ) - # normalize_functions="upper" must upper-case both audit references and query functions. + # "upper": audit references uppercased; query functions uppercased. assert ( format_model_expressions(expressions, normalize_functions="upper") == """MODEL ( @@ -404,7 +415,7 @@ def test_format_model_expressions_normalize_functions(): FROM foo""" ) - # normalize_functions="lower" must lower-case both audit references and query functions. + # "lower": audit references preserved lowercase (already lower); query functions lowercased. assert ( format_model_expressions(expressions, normalize_functions="lower") == """MODEL ( @@ -425,6 +436,50 @@ def test_format_model_expressions_normalize_functions(): FROM foo""" ) + # Single-meta-expression path: normalize_functions must be forwarded. + # Without the fix, this path ignored normalize_functions entirely. + single_model = parse( + """ + MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) + ); + """ + ) + + assert ( + format_model_expressions(single_model, normalize_functions="upper") + == """MODEL ( + name x, + audits ( + UNIQUE_COMBINATION_OF_COLUMNS(columns := ( + id + )), + NOT_NULL(columns := ( + id + )) + ) +)""" + ) + + assert ( + format_model_expressions(single_model) + == """MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := ( + id + )), + not_null(columns := ( + id + )) + ) +)""" + ) + def test_format_config_normalize_functions_false(): config = FormatConfig(normalize_functions=False) From c985c2aa74a7228f404ae08962b60bc97847234e Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:49:18 +0200 Subject: [PATCH 06/10] docs: accurately document normalize_functions type, default, and behavior Signed-off-by: Alberto Suman Co-authored-by: Cursor --- docs/reference/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 625bf62150..802fdfb7e9 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -114,7 +114,7 @@ Formatting settings for the `sqlmesh format` command and UI. | `normalize` | Whether to normalize SQL (Default: False) | boolean | N | | `pad` | The number of spaces to use for padding (Default: 2) | int | N | | `indent` | The number of spaces to use for indentation (Default: 2) | int | N | -| `normalize_functions` | Whether to normalize function names. Supported values are: 'upper' and 'lower' (Default: None) | string | N | +| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean | N | | `leading_comma` | Whether to use leading commas (Default: False) | boolean | N | | `max_text_width` | The maximum text width in a segment before creating new lines (Default: 80) | int | N | | `append_newline` | Whether to append a newline to the end of the file (Default: False) | boolean | N | From 4e69e81e2977917b843876e6b2ead3e33006f686 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 16:53:04 +0200 Subject: [PATCH 07/10] docs: add null to normalize_functions type and describe its behavior The FormatConfig.normalize_functions field accepts str | bool | None, but the reference table listed only 'string | boolean'. Explicit null defers to SQLGlot's generator default, which uppercases all function names (including custom ones), making it behaviorally distinct from False (preserve casing). Add null to the type column and one clause to the description so the user-facing docs match the actual accepted values. Signed-off-by: Alberto Suman Co-authored-by: Cursor --- docs/reference/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 802fdfb7e9..f43916aec6 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -114,7 +114,7 @@ Formatting settings for the `sqlmesh format` command and UI. | `normalize` | Whether to normalize SQL (Default: False) | boolean | N | | `pad` | The number of spaces to use for padding (Default: 2) | int | N | | `indent` | The number of spaces to use for indentation (Default: 2) | int | N | -| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean | N | +| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names; `null` defers to SQLGlot's generator default, which uppercases all function names including custom ones. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean \| null | N | | `leading_comma` | Whether to use leading commas (Default: False) | boolean | N | | `max_text_width` | The maximum text width in a segment before creating new lines (Default: 80) | int | N | | `append_newline` | Whether to append a newline to the end of the file (Default: False) | boolean | N | From 567e42167a78de609f5e6876395a3d78906092a3 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 17:01:33 +0200 Subject: [PATCH 08/10] docs(formatter): document True/None deferral and add None regression tests - Expand normalize_functions docstrings in FormatConfig and format_model_expressions to explicitly document True and None as aliases that defer to SQLGlot's default (uppercase custom functions), contrasting them with False (preserve) and the string modes. - Add None regression assertions to the existing normalize_functions test: one for the multi-expression path and one for the single-meta-expression path, both proving that lowercase audit names are uppercased when normalize_functions=None is passed explicitly. - Update docs/reference/configuration.md to mention 'true' alongside 'null' as a valid value that defers to the SQLGlot default. mypy skipped: errors are pre-existing in unrelated files (cache.py, macros.py, state_sync/base.py) and were present before this branch. Signed-off-by: Alberto Suman Co-authored-by: Cursor --- docs/reference/configuration.md | 2 +- sqlmesh/core/config/format.py | 15 ++++++++++--- sqlmesh/core/dialect.py | 13 +++++++++-- tests/core/test_dialect.py | 39 +++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index f43916aec6..3ff1d2a305 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -114,7 +114,7 @@ Formatting settings for the `sqlmesh format` command and UI. | `normalize` | Whether to normalize SQL (Default: False) | boolean | N | | `pad` | The number of spaces to use for padding (Default: 2) | int | N | | `indent` | The number of spaces to use for indentation (Default: 2) | int | N | -| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names; `null` defers to SQLGlot's generator default, which uppercases all function names including custom ones. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean \| null | N | +| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names; `true` or `null` defers to SQLGlot's generator default, which uppercases all function names including custom ones. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean \| null | N | | `leading_comma` | Whether to use leading commas (Default: False) | boolean | N | | `max_text_width` | The maximum text width in a segment before creating new lines (Default: 80) | int | N | | `append_newline` | Whether to append a newline to the end of the file (Default: False) | boolean | N | diff --git a/sqlmesh/core/config/format.py b/sqlmesh/core/config/format.py index edc51ef772..4e46a0eff5 100644 --- a/sqlmesh/core/config/format.py +++ b/sqlmesh/core/config/format.py @@ -12,9 +12,18 @@ class FormatConfig(BaseConfig): normalize: Whether to normalize the SQL code or not. pad: The number of spaces to use for padding. indent: The number of spaces to use for indentation. - normalize_functions: How to normalize function name casing. Use ``False`` (default) to - preserve the original casing, ``"upper"`` to uppercase all function names, or - ``"lower"`` to lowercase all function names. + normalize_functions: How to normalize function name casing. + + * ``False`` (default) — preserves the original spelling of custom and audit + function names. SQLGlot built-in functions (e.g. ``COUNT``, ``SUM``) may + still be uppercased because the parser discards the original token. + * ``"upper"`` — uppercases all function names, including custom audit + references. + * ``"lower"`` — lowercases all function names, including built-in ones. + * ``True`` or ``None`` — defers to SQLGlot's generator default, which + uppercases all function names including custom ones. These two values are + equivalent; ``None`` passes the deferral explicitly rather than relying on + the SQLGlot default. leading_comma: Whether to use leading commas or not. max_text_width: The maximum text width in a segment before creating new lines. append_newline: Whether to append a newline to the end of the file or not. diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 7254352b22..0568dfc71b 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -799,8 +799,17 @@ def format_model_expressions( expressions: The model's expressions, must be at least model def + query. dialect: The dialect to render the expressions as. rewrite_casts: Whether to rewrite all casts to use the :: syntax. - normalize_functions: How to normalize function name casing. ``False`` (default) - preserves original casing; ``"upper"`` uppercases; ``"lower"`` lowercases. + normalize_functions: How to normalize function name casing. + + * ``False`` (default) — preserves the original spelling of custom and audit + function names. SQLGlot built-in functions may still canonicalize because + the parser discards the original token. + * ``"upper"`` — uppercases all function names including custom audit + references. + * ``"lower"`` — lowercases all function names including built-ins. + * ``True`` or ``None`` — defers to SQLGlot's generator default, which + uppercases all function names including custom ones. Passing ``None`` + makes the deferral explicit rather than relying on the SQLGlot default. **kwargs: Additional keyword arguments to pass to the sql generator. Returns: diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 3c4ee0da3f..ff90ad482b 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -436,6 +436,29 @@ def test_format_model_expressions_normalize_functions(): FROM foo""" ) + # None: explicit deferral to SQLGlot default → custom/audit names uppercased, + # just like "upper". This is distinct from False (preserve) and must be tested + # explicitly because None used to be indistinguishable from the missing kwarg. + assert ( + format_model_expressions(expressions, normalize_functions=None) + == """MODEL ( + name x, + audits ( + UNIQUE_COMBINATION_OF_COLUMNS(columns := ( + id + )), + NOT_NULL(columns := ( + id + )) + ) +); + +SELECT + SUM(id), + COUNT(id) +FROM foo""" + ) + # Single-meta-expression path: normalize_functions must be forwarded. # Without the fix, this path ignored normalize_functions entirely. single_model = parse( @@ -480,6 +503,22 @@ def test_format_model_expressions_normalize_functions(): )""" ) + # Single-meta path, None: custom audit names are uppercased (explicit SQLGlot default deferral). + assert ( + format_model_expressions(single_model, normalize_functions=None) + == """MODEL ( + name x, + audits ( + UNIQUE_COMBINATION_OF_COLUMNS(columns := ( + id + )), + NOT_NULL(columns := ( + id + )) + ) +)""" + ) + def test_format_config_normalize_functions_false(): config = FormatConfig(normalize_functions=False) From 9725f9c9e15816a175556c2eace5ee07e72c88bb Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 17:11:59 +0200 Subject: [PATCH 09/10] =?UTF-8?q?docs(formatter):=20fix=20null=20semantics?= =?UTF-8?q?=20=E2=80=94=20None=20excluded=20by=20exclude=5Fnone,=20not=20S?= =?UTF-8?q?QLGlot=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the FormatConfig/YAML layer, normalize_functions=None is excluded from generator_options by PydanticModel.dict(exclude_none=True). format_model_expressions therefore uses its own False default, so YAML null behaves like false — not like SQLGlot's direct None deferral. Changes: - FormatConfig docstring: distinguish None (excluded -> False path) from True (defers to SQLGlot default -> uppercase); remove the incorrect claim they are equivalent - format_model_expressions docstring: clarify that direct None passes None to the SQLGlot generator (dialect-dependent) while the config path never reaches this because None is excluded before the call - docs/reference/configuration.md: describe null as taking the false-default path; true remains the value that defers to SQLGlot's generator default - tests: add test_format_config_normalize_functions_none asserting that FormatConfig(normalize_functions=None).generator_options omits the key and that the resulting format call preserves custom function casing (False behaviour) Signed-off-by: Alberto Suman Co-authored-by: Cursor --- docs/reference/configuration.md | 2 +- sqlmesh/core/config/format.py | 11 +++++++---- sqlmesh/core/dialect.py | 10 +++++++--- tests/core/test_dialect.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 3ff1d2a305..a7788df73d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -114,7 +114,7 @@ Formatting settings for the `sqlmesh format` command and UI. | `normalize` | Whether to normalize SQL (Default: False) | boolean | N | | `pad` | The number of spaces to use for padding (Default: 2) | int | N | | `indent` | The number of spaces to use for indentation (Default: 2) | int | N | -| `normalize_functions` | How to normalize function name casing. `False` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names; `true` or `null` defers to SQLGlot's generator default, which uppercases all function names including custom ones. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean \| null | N | +| `normalize_functions` | How to normalize function name casing. `false` (default) preserves the casing of custom and audit function names as written; `"upper"` uppercases all function names; `"lower"` lowercases all function names; `true` defers to SQLGlot's generator default and uppercases all function names including custom ones; `null` (or omitting the key) is excluded during serialization and therefore takes the same `false` default path — it does **not** defer to SQLGlot's generator default. Note: SQLGlot built-in function names may still be canonicalized by the parser regardless of this setting. | string \| boolean \| null | N | | `leading_comma` | Whether to use leading commas (Default: False) | boolean | N | | `max_text_width` | The maximum text width in a segment before creating new lines (Default: 80) | int | N | | `append_newline` | Whether to append a newline to the end of the file (Default: False) | boolean | N | diff --git a/sqlmesh/core/config/format.py b/sqlmesh/core/config/format.py index 4e46a0eff5..5ec6da47cd 100644 --- a/sqlmesh/core/config/format.py +++ b/sqlmesh/core/config/format.py @@ -20,10 +20,13 @@ class FormatConfig(BaseConfig): * ``"upper"`` — uppercases all function names, including custom audit references. * ``"lower"`` — lowercases all function names, including built-in ones. - * ``True`` or ``None`` — defers to SQLGlot's generator default, which - uppercases all function names including custom ones. These two values are - equivalent; ``None`` passes the deferral explicitly rather than relying on - the SQLGlot default. + * ``True`` — defers to SQLGlot's generator default, which uppercases all + function names including custom ones. + * ``None`` — excluded from the serialized generator options by Pydantic's + ``exclude_none`` behaviour, so ``format_model_expressions`` falls back to + its own ``False`` default. Setting this in YAML as ``null`` or omitting + the key is therefore equivalent to ``false``; it does **not** defer to + SQLGlot's generator default the way ``True`` does. leading_comma: Whether to use leading commas or not. max_text_width: The maximum text width in a segment before creating new lines. append_newline: Whether to append a newline to the end of the file or not. diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 0568dfc71b..67918b6d14 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -807,9 +807,13 @@ def format_model_expressions( * ``"upper"`` — uppercases all function names including custom audit references. * ``"lower"`` — lowercases all function names including built-ins. - * ``True`` or ``None`` — defers to SQLGlot's generator default, which - uppercases all function names including custom ones. Passing ``None`` - makes the deferral explicit rather than relying on the SQLGlot default. + * ``True`` — defers to SQLGlot's generator default (uppercase). + * ``None`` — passes ``None`` directly to the SQLGlot generator, which + defers to SQLGlot's own default (typically uppercase, but may vary by + dialect). Note: this is the **direct generator API** behaviour. When + called via ``FormatConfig``, ``None`` is excluded by Pydantic's + ``exclude_none`` serialization and this function receives its own ``False`` + default instead — so the two paths are not equivalent. **kwargs: Additional keyword arguments to pass to the sql generator. Returns: diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index ff90ad482b..e2f1daba3d 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -527,6 +527,37 @@ def test_format_config_normalize_functions_false(): assert config.generator_options["normalize_functions"] is False +def test_format_config_normalize_functions_none(): + """FormatConfig(normalize_functions=None) must be accepted but excluded from + generator_options by Pydantic's exclude_none serialization. The config-layer + null therefore takes the False-default path in format_model_expressions rather + than deferring to SQLGlot's generator default the way True does. + """ + config = FormatConfig(normalize_functions=None) + + assert config.normalize_functions is None + # None is excluded by PydanticModel.dict(exclude_none=True), so the key must + # be absent from generator_options — format_model_expressions will use False. + assert "normalize_functions" not in config.generator_options + + # Confirm the False-default behaviour: custom audit names must be preserved. + expressions = parse( + """ + MODEL ( + name x, + audits ( + unique_combination_of_columns(columns := (id)), + not_null(columns := (id)) + ) + ); + SELECT id FROM foo + """ + ) + result = format_model_expressions(expressions, **config.generator_options) + assert "unique_combination_of_columns" in result + assert "not_null" in result + + def test_macro_format(): assert parse_one("@EACH(ARRAY(1,2), x -> x)").sql() == "@EACH(ARRAY(1, 2), x -> x)" assert parse_one("INTERVAL @x DAY").sql() == "INTERVAL @x DAY" From 770b3010ed09fe6ac59419f919f85f7189f5e6c9 Mon Sep 17 00:00:00 2001 From: Alberto Suman Date: Thu, 16 Jul 2026 20:09:23 +0200 Subject: [PATCH 10/10] docs: remove formatter workflow artifacts Signed-off-by: Alberto Suman Co-authored-by: Cursor --- .../2026-07-16-formatter-function-casing.md | 168 ------------------ ...-07-16-formatter-function-casing-design.md | 19 -- 2 files changed, 187 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-16-formatter-function-casing.md delete mode 100644 docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md diff --git a/docs/superpowers/plans/2026-07-16-formatter-function-casing.md b/docs/superpowers/plans/2026-07-16-formatter-function-casing.md deleted file mode 100644 index 81d4a2f284..0000000000 --- a/docs/superpowers/plans/2026-07-16-formatter-function-casing.md +++ /dev/null @@ -1,168 +0,0 @@ -# Formatter Function-Casing Implementation Plan -> *Estimated costs for executing this plan:* -> **Current Model** (GPT-5.6 Terra): 24k input / 7.2k output | **$0.17 – $0.25** -> **Auto Model**: (Same token usage) | **$0.07 – $0.11** - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Preserve function spelling by default when SQLMesh formats model metadata and query SQL. - -**Architecture:** `FormatConfig` is serialized to SQLGlot generator options. Set its `normalize_functions` default to the explicit boolean `False`, which survives SQLMesh's `exclude_none` serialization and changes only SQLGlot's generator option. `format_model_expressions` continues to render metadata with `dialect=None`. - -**Tech Stack:** Python, Pydantic configuration models, SQLGlot formatter, pytest. - -## Global Constraints - -- `normalize_functions` accepts `str | bool | None`; the default is `False`. -- Existing `"upper"` and `"lower"` configuration behavior remains supported. -- Do not add a new CLI string mode for `False`. -- Retain metadata rendering with `dialect=None`; alter only the generator option. -- Every new commit includes a DCO `Signed-off-by` trailer. - ---- - -### Task 1: Lock down formatter casing behavior - -**Files:** -- Modify: `tests/core/test_dialect.py:22-168` -- Test: `tests/core/test_dialect.py` - -**Interfaces:** -- Consumes: `format_model_expressions(expressions, **generator_options)` and `parse(sql)`. -- Produces: regression coverage for default, `"upper"`, and `"lower"` function casing across metadata and queries. - -- [ ] **Step 1: Write the failing regression test** - -```python -def test_format_model_expressions_normalize_functions(): - expressions = parse( - """ - MODEL ( - name x, - audits ( - unique_combination_of_columns(columns := (id)), - not_null(columns := (id)) - ) - ); - - SELECT SUM(id), count(id) FROM foo; - """ - ) - - assert format_model_expressions(expressions) == """MODEL ( - name x, - audits ( - unique_combination_of_columns(columns := (id)), - not_null(columns := (id)) - ) -); - -SELECT SUM(id), count(id) FROM foo;""" -``` - -- [ ] **Step 2: Extend the same test with explicit normalization assertions** - -```python -assert format_model_expressions(expressions, normalize_functions="upper") == """MODEL ( - name x, - audits ( - UNIQUE_COMBINATION_OF_COLUMNS(columns := (id)), - NOT_NULL(columns := (id)) - ) -); - -SELECT SUM(id), COUNT(id) FROM foo;""" - -assert format_model_expressions(expressions, normalize_functions="lower") == """MODEL ( - name x, - audits ( - unique_combination_of_columns(columns := (id)), - not_null(columns := (id)) - ) -); - -SELECT sum(id), count(id) FROM foo;""" -``` - -- [ ] **Step 3: Run the regression test to verify the default fails** - -Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_model_expressions_normalize_functions -v` - -Expected: the default output contains upper-case audit references and `COUNT(id)`. - -### Task 2: Make the formatter default explicit - -**Files:** -- Modify: `sqlmesh/core/config/format.py:8-38` -- Test: `tests/core/test_dialect.py` - -**Interfaces:** -- Consumes: `FormatConfig.generator_options`. -- Produces: `normalize_functions=False` in generator options unless callers explicitly configure a different accepted value. - -- [ ] **Step 1: Add a failing configuration validation assertion** - -```python -from sqlmesh.core.config.format import FormatConfig - - -def test_format_config_normalize_functions_false(): - config = FormatConfig(normalize_functions=False) - - assert config.normalize_functions is False - assert config.generator_options["normalize_functions"] is False -``` - -- [ ] **Step 2: Run the validation test to verify it fails** - -Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_config_normalize_functions_false -v` - -Expected: validation rejects `False` before the configuration change. - -- [ ] **Step 3: Implement the minimal configuration change** - -```python -normalize_functions: t.Union[str, bool, None] = False -``` - -Update the `FormatConfig` argument description to specify that `False` preserves existing casing and is the default, while `"upper"` and `"lower"` normalize casing. - -- [ ] **Step 4: Run focused regression coverage** - -Run: `.venv/bin/pytest tests/core/test_dialect.py::test_format_model_expressions tests/core/test_dialect.py::test_format_model_expressions_normalize_functions tests/core/test_dialect.py::test_format_config_normalize_functions_false -v` - -Expected: all selected tests pass and existing `test_format_model_expressions` expectations reflect preserved lower-case audit references. - -### Task 3: Document configuration and verify the branch - -**Files:** -- Modify: `docs/reference/configuration.md:112-121` -- Modify: `tests/core/test_dialect.py:22-168` -- Modify: `sqlmesh/core/config/format.py:8-38` - -**Interfaces:** -- Consumes: the documented `FormatConfig.normalize_functions` values. -- Produces: configuration documentation that accurately reflects the accepted type and default. - -- [ ] **Step 1: Update the configuration reference** - -Document the option as `string | boolean | null`: `False` (the default) preserves existing casing; `"upper"` and `"lower"` normalize all function names. - -- [ ] **Step 2: Run formatter and targeted tests** - -Run: `.venv/bin/ruff format --check sqlmesh/core/config/format.py tests/core/test_dialect.py && .venv/bin/pytest tests/core/test_dialect.py tests/core/test_format.py -v -m "not slow and not docker"` - -Expected: formatting check and selected test modules pass. - -- [ ] **Step 3: Review the diff** - -Run: `git diff --check HEAD~1..HEAD && git diff -- sqlmesh/core/config/format.py tests/core/test_dialect.py docs/reference/configuration.md` - -Expected: no whitespace errors; metadata formatting remains delegated to the existing `dialect=None` call site. - -- [ ] **Step 4: Commit the implementation** - -```bash -git add sqlmesh/core/config/format.py tests/core/test_dialect.py docs/reference/configuration.md -git commit -s -m "fix: preserve function casing during formatting" -``` diff --git a/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md b/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md deleted file mode 100644 index c10c0e4acf..0000000000 --- a/docs/superpowers/specs/2026-07-16-formatter-function-casing-design.md +++ /dev/null @@ -1,19 +0,0 @@ -# Formatter Function-Casing Regression Design - -## Scope - -Restore the pre-0.236.0 formatter behavior: absent formatter configuration preserves function spelling in model metadata and query SQL. - -## Design - -`FormatConfig.normalize_functions` will accept `str | bool | None` and default to `False`. SQLMesh serializes non-`None` formatter values into SQLGlot generator options, so `False` is passed directly to SQLGlot while metadata continues to be generated with `dialect=None`. - -Existing `"upper"` and `"lower"` string values remain unchanged. An explicit `None` remains accepted and preserves its current omission behavior. - -## Tests - -Add model-formatting regression coverage for lower-case audit references and mixed-case query functions. Verify the default preserves spelling, `"upper"` normalizes both metadata and queries to upper case, `"lower"` normalizes both to lower case, and the configuration accepts `False`. - -## Documentation - -Update the configuration reference and `FormatConfig` type description to list `False` as the default casing-preservation mode and retain the two string normalization modes.