Skip to content

Commit 9725f9c

Browse files
docs(formatter): fix null semantics — None excluded by exclude_none, not SQLGlot default
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 <alberto.suman@1komma5grad.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 567e421 commit 9725f9c

4 files changed

Lines changed: 46 additions & 8 deletions

File tree

docs/reference/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Formatting settings for the `sqlmesh format` command and UI.
114114
| `normalize` | Whether to normalize SQL (Default: False) | boolean | N |
115115
| `pad` | The number of spaces to use for padding (Default: 2) | int | N |
116116
| `indent` | The number of spaces to use for indentation (Default: 2) | int | N |
117-
| `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 |
117+
| `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 |
118118
| `leading_comma` | Whether to use leading commas (Default: False) | boolean | N |
119119
| `max_text_width` | The maximum text width in a segment before creating new lines (Default: 80) | int | N |
120120
| `append_newline` | Whether to append a newline to the end of the file (Default: False) | boolean | N |

sqlmesh/core/config/format.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class FormatConfig(BaseConfig):
2020
* ``"upper"`` — uppercases all function names, including custom audit
2121
references.
2222
* ``"lower"`` — lowercases all function names, including built-in ones.
23-
* ``True`` or ``None`` — defers to SQLGlot's generator default, which
24-
uppercases all function names including custom ones. These two values are
25-
equivalent; ``None`` passes the deferral explicitly rather than relying on
26-
the SQLGlot default.
23+
* ``True`` — defers to SQLGlot's generator default, which uppercases all
24+
function names including custom ones.
25+
* ``None`` — excluded from the serialized generator options by Pydantic's
26+
``exclude_none`` behaviour, so ``format_model_expressions`` falls back to
27+
its own ``False`` default. Setting this in YAML as ``null`` or omitting
28+
the key is therefore equivalent to ``false``; it does **not** defer to
29+
SQLGlot's generator default the way ``True`` does.
2730
leading_comma: Whether to use leading commas or not.
2831
max_text_width: The maximum text width in a segment before creating new lines.
2932
append_newline: Whether to append a newline to the end of the file or not.

sqlmesh/core/dialect.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,13 @@ def format_model_expressions(
807807
* ``"upper"`` — uppercases all function names including custom audit
808808
references.
809809
* ``"lower"`` — lowercases all function names including built-ins.
810-
* ``True`` or ``None`` — defers to SQLGlot's generator default, which
811-
uppercases all function names including custom ones. Passing ``None``
812-
makes the deferral explicit rather than relying on the SQLGlot default.
810+
* ``True`` — defers to SQLGlot's generator default (uppercase).
811+
* ``None`` — passes ``None`` directly to the SQLGlot generator, which
812+
defers to SQLGlot's own default (typically uppercase, but may vary by
813+
dialect). Note: this is the **direct generator API** behaviour. When
814+
called via ``FormatConfig``, ``None`` is excluded by Pydantic's
815+
``exclude_none`` serialization and this function receives its own ``False``
816+
default instead — so the two paths are not equivalent.
813817
**kwargs: Additional keyword arguments to pass to the sql generator.
814818
815819
Returns:

tests/core/test_dialect.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,37 @@ def test_format_config_normalize_functions_false():
527527
assert config.generator_options["normalize_functions"] is False
528528

529529

530+
def test_format_config_normalize_functions_none():
531+
"""FormatConfig(normalize_functions=None) must be accepted but excluded from
532+
generator_options by Pydantic's exclude_none serialization. The config-layer
533+
null therefore takes the False-default path in format_model_expressions rather
534+
than deferring to SQLGlot's generator default the way True does.
535+
"""
536+
config = FormatConfig(normalize_functions=None)
537+
538+
assert config.normalize_functions is None
539+
# None is excluded by PydanticModel.dict(exclude_none=True), so the key must
540+
# be absent from generator_options — format_model_expressions will use False.
541+
assert "normalize_functions" not in config.generator_options
542+
543+
# Confirm the False-default behaviour: custom audit names must be preserved.
544+
expressions = parse(
545+
"""
546+
MODEL (
547+
name x,
548+
audits (
549+
unique_combination_of_columns(columns := (id)),
550+
not_null(columns := (id))
551+
)
552+
);
553+
SELECT id FROM foo
554+
"""
555+
)
556+
result = format_model_expressions(expressions, **config.generator_options)
557+
assert "unique_combination_of_columns" in result
558+
assert "not_null" in result
559+
560+
530561
def test_macro_format():
531562
assert parse_one("@EACH(ARRAY(1,2), x -> x)").sql() == "@EACH(ARRAY(1, 2), x -> x)"
532563
assert parse_one("INTERVAL @x DAY").sql() == "INTERVAL @x DAY"

0 commit comments

Comments
 (0)