Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion soda-core/src/soda_core/common/soda_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,7 +1516,8 @@ def to_jsonnable(o: Any, remove_null_values_in_dicts: bool = True) -> object:
if isinstance(o, datetime):
return convert_datetime_to_str(o)
if isinstance(o, date):
return o.strftime("%Y-%m-%d")
# Not strftime("%Y-%m-%d"): C strftime does not zero-pad years < 1000 on glibc.
return o.isoformat()
if isinstance(o, time):
return o.strftime("%H:%M:%S")
if isinstance(o, timedelta):
Expand Down
5 changes: 3 additions & 2 deletions soda-core/src/soda_core/common/sql_dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,9 @@ def literal_list(self, l: list):
return "(" + (",".join([self.literal(e) for e in l])) + ")"

def literal_date(self, date: date):
date_string = date.strftime("%Y-%m-%d")
return f"DATE '{date_string}'"
# Not strftime("%Y-%m-%d"): C strftime does not zero-pad years < 1000 on glibc,
# producing literals like DATE '200-12-17' that Spark/Databricks reject.
return f"DATE '{date.isoformat()}'"

def literal_datetime(self, datetime: datetime):
return f"'{datetime.isoformat()}'"
Expand Down
8 changes: 8 additions & 0 deletions soda-databricks/tests/unit/test_databricks_dialect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import date

import pytest
from soda_core.common.metadata_types import SodaDataTypeName
from soda_core.common.sql_dialect import FROM, RANDOM, SELECT, STAR, SamplerType
Expand Down Expand Up @@ -56,6 +58,12 @@ def test_random():
assert sql == "SELECT RAND()\nFROM `a`;"


def test_literal_date_pads_year_to_four_digits():
"""Spark SQL rejects DATE literals without a 4-digit year as INVALID_TYPED_LITERAL,
and C strftime("%Y") drops the leading zeros for years < 1000 on glibc."""
assert DatabricksSqlDialect().literal(date(200, 12, 17)) == "DATE '0200-12-17'"


class TestTimestampReverseMapping:
"""Pin the cross-source DWH dispatch invariant for Databricks.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ def _build_limit_line(self, select_elements: list) -> Optional[str]:

def literal_date(self, date: date):
"""Technically dates can be passed directly as strings, but this is more explicit."""
date_string = date.strftime("%Y-%m-%d")
return f"CAST('{date_string}' AS DATE)"
return f"CAST('{date.isoformat()}' AS DATE)"

def literal_datetime(self, datetime: datetime):
return f"'{datetime.isoformat(timespec='milliseconds')}'"
Expand Down
8 changes: 8 additions & 0 deletions soda-sqlserver/tests/unit/test_sqlserver_dialect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import date

from soda_core.common.metadata_types import SqlDataType
from soda_core.common.sql_ast import COUNT, CREATE_TABLE_COLUMN, STAR
from soda_core.common.sql_dialect import FROM, RANDOM, SELECT
Expand All @@ -10,6 +12,12 @@ def test_random():
assert sql == "SELECT ABS(CAST(CHECKSUM(NEWID()) AS FLOAT)) / 2147483648.0\nFROM [a];"


def test_literal_date_pads_year_to_four_digits():
"""C strftime("%Y") drops leading zeros for years < 1000 on glibc; the CAST
string must always carry a 4-digit, zero-padded year."""
assert SqlServerSqlDialect().literal(date(200, 12, 17)) == "CAST('0200-12-17' AS DATE)"


def test_count_renders_as_count_big():
# T-SQL COUNT returns INT and overflows above 2.1B rows; the dialect must emit COUNT_BIG.
assert SqlServerSqlDialect().build_expression_sql(COUNT(STAR())) == "COUNT_BIG(*)"
Expand Down
20 changes: 20 additions & 0 deletions soda-tests/tests/unit/test_sql_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,26 @@
)


def test_sql_ast_insert_into_pads_date_years_below_1000():
"""C strftime("%Y") drops leading zeros for years < 1000 on glibc, which used to
render DATE '200-12-17' — an INVALID_TYPED_LITERAL for Spark/Databricks. Date
literals must always carry a 4-digit, zero-padded year."""
sql_dialect: SqlDialect = SqlDialect()

assert sql_dialect.literal(date(200, 12, 17)) == "DATE '0200-12-17'"

my_insert_into_statement = sql_dialect.build_insert_into_sql(
INSERT_INTO(
fully_qualified_table_name='"customers"',
columns=[COLUMN("id"), COLUMN("birth_date")],
values=[VALUES_ROW([LITERAL(1), LITERAL(date(200, 12, 17))])],
)
)
assert my_insert_into_statement == (
'INSERT INTO "customers" ("id", "birth_date") VALUES\n' "(1, DATE '0200-12-17');"
)


def test_sql_ast_insert_into_via_select():
sql_dialect: SqlDialect = SqlDialect()
my_insert_into_statement = sql_dialect.build_insert_into_via_select_sql(
Expand Down Expand Up @@ -349,7 +369,7 @@
"""Same guard for COUNT."""
import pytest

with pytest.raises(TypeError):

Check warning on line 372 in soda-tests/tests/unit/test_sql_generation.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this exception test to have only one invocation possibly throwing an exception.

See more on https://sonarcloud.io/project/issues?id=sodadata_soda-sql&issues=AZ9q85IzSHe1d1PwTtgH&open=AZ9q85IzSHe1d1PwTtgH&pullRequest=2797
COUNT(STAR(), field_alias="x") # type: ignore[call-arg]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,11 @@ def data_type_has_parameter_datetime_precision(self, data_type_name) -> bool:
]

def literal_datetime(self, datetime: datetime):
return f"TIMESTAMP '{datetime.strftime('%Y-%m-%d %H:%M:%S.%f')}'"
return f"TIMESTAMP '{datetime.isoformat(sep=' ', timespec='microseconds')}'"

def literal_datetime_with_tz(self, datetime: datetime):
return f"TIMESTAMP '{datetime.strftime('%Y-%m-%d %H:%M:%S.%f%z')}'"
# isoformat appends the +HH:MM offset for tz-aware values.
return f"TIMESTAMP '{datetime.isoformat(sep=' ', timespec='microseconds')}'"

def literal_time(self, time: time):
formatted_time = time.strftime("%H:%M:%S.%f")
Expand Down
13 changes: 13 additions & 0 deletions soda-trino/tests/unit/test_trino_dialect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime, timezone

from soda_core.common.sql_dialect import FROM, RANDOM, SELECT
from soda_trino.common.data_sources.trino_data_source import TrinoSqlDialect

Expand All @@ -6,3 +8,14 @@ def test_random():
sql_dialect: TrinoSqlDialect = TrinoSqlDialect()
sql = sql_dialect.build_select_sql([SELECT(RANDOM()), FROM("a")])
assert sql == 'SELECT RANDOM()\nFROM "a"'


def test_literal_datetime_pads_year_to_four_digits():
"""C strftime("%Y") drops leading zeros for years < 1000 on glibc, producing
TIMESTAMP literals Trino cannot parse; the year must always be 4 digits."""
sql_dialect: TrinoSqlDialect = TrinoSqlDialect()
assert sql_dialect.literal(datetime(200, 12, 17, 1, 2, 3, 456)) == "TIMESTAMP '0200-12-17 01:02:03.000456'"
assert (
sql_dialect.literal(datetime(200, 12, 17, 1, 2, 3, 456, tzinfo=timezone.utc))
== "TIMESTAMP '0200-12-17 01:02:03.000456+00:00'"
)
Loading