Skip to content

Commit e087e71

Browse files
authored
fix: normalize NULL date options when constructing BaserowFormulaDateType (baserow#5364)
`FormulaField` allows every date option (`date_format`, `date_include_time`, `date_time_format`, `date_show_tzinfo`) to be NULL because a single row can morph between formula types. For a date-typed formula, however, only `date_force_timezone` is semantically nullable — see `BaserowFormulaDateType.nullable_option_fields`. When that invariant is violated by an existing row (NULL for a supposedly required option), `construct_type_from_formula_field` built a `BaserowFormulaDateType` carrying those NULLs. The first downstream consumer to access them — `DateFieldType.get_search_expression` building a search expression for `update_search_data` — crashed in `get_date_time_format` with `KeyError: None` (723 occurrences observed in production). Normalize at the type-construction boundary: when a non-nullable option is NULL on the row, substitute the type's documented default (ISO / False / "24" / False). Downstream code keeps its non-defensive shape, and the same fix protects every other consumer (`get_export_value`, `contains_query`, `get_alter_column_*`). Fixes baserow#5363
1 parent b66235f commit e087e71

3 files changed

Lines changed: 82 additions & 1 deletion

File tree

backend/src/baserow/contrib/database/formula/types/formula_types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,14 @@ class BaserowFormulaDateType(
875875
"date_force_timezone",
876876
]
877877
nullable_option_fields = ["date_force_timezone"]
878+
# Only `date_force_timezone` is semantically nullable; others have defaults
879+
# applied by `construct_type_from_formula_field` for legacy/corrupt rows.
880+
non_nullable_option_defaults = {
881+
"date_format": "ISO",
882+
"date_include_time": False,
883+
"date_time_format": "24",
884+
"date_show_tzinfo": False,
885+
}
878886
can_represent_date = True
879887
can_order_by_in_array = True
880888
can_group_by = True
@@ -897,6 +905,16 @@ def __init__(
897905
self.date_show_tzinfo = date_show_tzinfo
898906
self.date_force_timezone = date_force_timezone
899907

908+
@classmethod
909+
def construct_type_from_formula_field(cls, formula_field):
910+
kwargs = {}
911+
for field_name in cls.all_fields():
912+
value = getattr(formula_field, field_name)
913+
if value is None and field_name in cls.non_nullable_option_defaults:
914+
value = cls.non_nullable_option_defaults[field_name]
915+
kwargs[field_name] = value
916+
return cls(**kwargs)
917+
900918
@property
901919
def array_index_sql(self) -> str:
902920
cast = "::timestamptz" if self.date_include_time else "::date"

backend/tests/baserow/contrib/database/field/test_date_field_type.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88

99
from baserow.contrib.database.fields.field_types import DateFieldType
1010
from baserow.contrib.database.fields.handler import FieldHandler
11-
from baserow.contrib.database.fields.models import DateField, TextField
11+
from baserow.contrib.database.fields.models import DateField, FormulaField, TextField
1212
from baserow.contrib.database.fields.registries import field_type_registry
1313
from baserow.contrib.database.fields.utils import DeferredForeignKeyUpdater
14+
from baserow.contrib.database.formula.types.formula_types import (
15+
BaserowFormulaDateType,
16+
)
1417
from baserow.contrib.database.rows.handler import RowHandler
1518
from baserow.contrib.database.views.handler import ViewHandler
1619
from baserow.core.psycopg import is_psycopg3
@@ -840,3 +843,54 @@ def test_datetime_field_overflow(on_db_connection, data_fixture):
840843
assert len(out) == 1
841844

842845
assert getattr(out[0], date_field.db_column, None) is None
846+
847+
848+
def test_baserow_formula_date_type_normalizes_null_non_nullable_options():
849+
# `FormulaField` allows every date option to be NULL. When existing rows
850+
# violate invariants, `construct_type_from_formula_field` normalizes NULLs
851+
# to prevent downstream crashes.
852+
corrupt = FormulaField(
853+
formula="TODAY()",
854+
formula_type="date",
855+
date_format=None,
856+
date_include_time=None,
857+
date_time_format=None,
858+
date_show_tzinfo=None,
859+
date_force_timezone=None,
860+
version=0,
861+
requires_refresh_after_insert=False,
862+
nullable=True,
863+
)
864+
865+
formula_type = BaserowFormulaDateType.construct_type_from_formula_field(corrupt)
866+
867+
assert formula_type.date_format == "ISO"
868+
assert formula_type.date_include_time is False
869+
assert formula_type.date_time_format == "24"
870+
assert formula_type.date_show_tzinfo is False
871+
assert formula_type.date_force_timezone is None
872+
873+
874+
def test_baserow_formula_date_type_preserves_user_set_options():
875+
formula_field = FormulaField(
876+
formula="TODAY()",
877+
formula_type="date",
878+
date_format="EU",
879+
date_include_time=True,
880+
date_time_format="12",
881+
date_show_tzinfo=True,
882+
date_force_timezone="Europe/Amsterdam",
883+
version=0,
884+
requires_refresh_after_insert=False,
885+
nullable=True,
886+
)
887+
888+
formula_type = BaserowFormulaDateType.construct_type_from_formula_field(
889+
formula_field
890+
)
891+
892+
assert formula_type.date_format == "EU"
893+
assert formula_type.date_include_time is True
894+
assert formula_type.date_time_format == "12"
895+
assert formula_type.date_show_tzinfo is True
896+
assert formula_type.date_force_timezone == "Europe/Amsterdam"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fixes search index updates failing for tables with date-only formula fields",
4+
"issue_origin": "github",
5+
"issue_number": 5363,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2026-05-13"
9+
}

0 commit comments

Comments
 (0)