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
1 change: 1 addition & 0 deletions sidemantic-rs/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub(crate) use preaggregation::{
materialization_sql as preaggregation_materialization_sql, replace_model_placeholder,
source_expression as preaggregation_source_expression,
};
pub(crate) use relative_date::bigquery_trunc_part;
pub use relative_date::RelativeDate;
pub use segment::Segment;
pub use symmetric_agg::{
Expand Down
11 changes: 11 additions & 0 deletions sidemantic-rs/src/core/relative_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
use lazy_static::lazy_static;
use regex::Regex;

/// BigQuery date part for a grain; weeks start Monday as in the other dialects.
pub(crate) fn bigquery_trunc_part(granularity: &str) -> String {
let part = granularity.to_ascii_uppercase();
// BigQuery's bare WEEK starts on Sunday.
if part == "WEEK" {
"WEEK(MONDAY)".to_string()
} else {
part
}
}

/// Relative date expression parser
pub struct RelativeDate;

Expand Down
9 changes: 8 additions & 1 deletion sidemantic-rs/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4611,7 +4611,10 @@ pub fn validate_table_formula_expression(expression: &str) -> Result<bool> {

fn relative_date_trunc(granularity: &str, dialect: &str) -> String {
if dialect.eq_ignore_ascii_case("bigquery") {
format!("DATE_TRUNC(CURRENT_DATE, {})", granularity.to_uppercase())
format!(
"DATE_TRUNC(CURRENT_DATE, {})",
crate::core::bigquery_trunc_part(granularity)
)
} else {
format!("DATE_TRUNC('{granularity}', CURRENT_DATE)")
}
Expand Down Expand Up @@ -6704,6 +6707,10 @@ type: percent_of_total
parse_relative_date("this month", "bigquery"),
Some("DATE_TRUNC(CURRENT_DATE, MONTH)".to_string())
);
assert_eq!(
parse_relative_date("this week", "bigquery"),
Some("DATE_TRUNC(CURRENT_DATE, WEEK(MONDAY))".to_string())
);

let range = relative_date_to_range("this month", "order_date", "duckdb").unwrap();
assert!(range.contains("order_date >= DATE_TRUNC('month', CURRENT_DATE)"));
Expand Down
2 changes: 1 addition & 1 deletion sidemantic-rs/src/sql/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4178,7 +4178,7 @@ impl<'a> SqlGenerator<'a> {
Ok(format!(
"DATE_TRUNC({}, {})",
column_expr,
granularity.to_ascii_uppercase()
crate::core::bigquery_trunc_part(granularity)
))
} else if matches!(self.dialect, DialectType::Spark | DialectType::Databricks) {
let grain = granularity.to_ascii_uppercase();
Expand Down
9 changes: 8 additions & 1 deletion sidemantic/core/relative_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import re


def bigquery_trunc_part(granularity: str) -> str:
"""BigQuery date part for a grain; weeks start Monday as in the other dialects."""
part = granularity.upper()
# BigQuery's bare WEEK starts on Sunday.
return "WEEK(MONDAY)" if part == "WEEK" else part


def _date_trunc(granularity: str, column_expr: str, dialect: str) -> str:
"""Generate dialect-specific DATE_TRUNC expression."""
if dialect == "bigquery":
return f"DATE_TRUNC({column_expr}, {granularity.upper()})"
return f"DATE_TRUNC({column_expr}, {bigquery_trunc_part(granularity)})"
else:
return f"DATE_TRUNC('{granularity}', {column_expr})"

Expand Down
3 changes: 2 additions & 1 deletion sidemantic/sql/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sqlglot.errors import SqlglotError

from sidemantic.core.preagg_matcher import PreAggregationMatcher
from sidemantic.core.relative_date import bigquery_trunc_part
from sidemantic.core.semantic_graph import SemanticGraph, _relationship_local_key_columns
from sidemantic.core.symmetric_aggregate import build_symmetric_aggregate_sql
from sidemantic.sql.aggregation_detection import sql_has_aggregate
Expand Down Expand Up @@ -737,7 +738,7 @@ def _date_trunc(self, granularity: str, column_expr: str) -> str:
column_expr = self._localize_to_timezone(column_expr)

if self.dialect == "bigquery":
return f"DATE_TRUNC({column_expr}, {granularity.upper()})"
return f"DATE_TRUNC({column_expr}, {bigquery_trunc_part(granularity)})"

if self.dialect in {"spark", "databricks"}:
spark_granularity = granularity.upper()
Expand Down
2 changes: 1 addition & 1 deletion tests/dates/test_relative_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def test_bigquery_dialect():
"""Test BigQuery dialect generates correct DATE_TRUNC syntax."""
# BigQuery uses DATE_TRUNC(column, MONTH) not DATE_TRUNC('month', column)
assert RelativeDateRange.parse("this month", dialect="bigquery") == "DATE_TRUNC(CURRENT_DATE, MONTH)"
assert RelativeDateRange.parse("this week", dialect="bigquery") == "DATE_TRUNC(CURRENT_DATE, WEEK)"
assert RelativeDateRange.parse("this week", dialect="bigquery") == "DATE_TRUNC(CURRENT_DATE, WEEK(MONDAY))"
assert RelativeDateRange.parse("this year", dialect="bigquery") == "DATE_TRUNC(CURRENT_DATE, YEAR)"
assert (
RelativeDateRange.parse("last month", dialect="bigquery")
Expand Down
21 changes: 21 additions & 0 deletions tests/queries/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,27 @@ def test_time_dimension_bigquery_dialect(layer):
assert "DATE_TRUNC('month'" not in sql, f"Should not have PostgreSQL syntax. Got: {sql}"


def test_time_dimension_bigquery_week_starts_monday(layer):
"""BigQuery's bare WEEK starts Sunday; week buckets must match the Monday weeks of other dialects."""
orders = Model(
name="orders",
table="orders_table",
primary_key="order_id",
dimensions=[Dimension(name="created_at", type="time", sql="created_at", granularity="day")],
metrics=[Metric(name="revenue", agg="sum", sql="amount")],
)
layer.add_model(orders)

sql = layer.compile(metrics=["orders.revenue"], dimensions=["orders.created_at__week"], dialect="bigquery")

assert "DATE_TRUNC(created_at, WEEK(MONDAY))" in sql, f"Expected Monday-start BigQuery weeks. Got: {sql}"
# Sunday 2023-12-31 belongs to the week starting Monday 2023-12-25, as in DuckDB.
bucket = sqlglot.parse_one("SELECT CAST(DATE_TRUNC(DATE '2023-12-31', WEEK(MONDAY)) AS DATE)", read="bigquery").sql(
dialect="duckdb"
)
assert duckdb.sql(bucket).fetchone()[0] == date(2023, 12, 25)


def test_time_dimension_duckdb_dialect(layer):
"""Test that time dimensions generate correct DuckDB DATE_TRUNC syntax."""
orders = Model(
Expand Down
13 changes: 9 additions & 4 deletions tests/queries/yardstick_replay_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class ReplayStatement:
has_aggregate: bool


def _drop_target(drop: exp.Drop) -> exp.Table:
# sqlglot >= 30.23 parses DROP targets into `tables`; earlier versions use `this`.
return drop.this or drop.args["tables"][0]


def split_replay_sql(sql: str, dialect: str) -> list[str]:
"""Split SQLLogicTest batches without splitting comments or quoted literals."""
tokens = sqlglot.tokenize(sql, read=dialect)
Expand Down Expand Up @@ -187,12 +192,12 @@ def execute(self, sql: str) -> list[tuple]:
self._install_models()
continue
if isinstance(parsed, exp.Drop) and str(parsed.args.get("kind", "")).upper() == "VIEW":
table = parsed.this
table = _drop_target(parsed)
name = table.name.lower()
if table.db.lower() == "main" and not table.catalog:
drop = parsed.copy()
catalog = self.layer.adapter.execute("SELECT current_database()").fetchone()[0]
drop.this.set("catalog", exp.to_identifier(catalog, quoted=True))
_drop_target(drop).set("catalog", exp.to_identifier(catalog, quoted=True))
self._execute(drop.sql(dialect=self.adapter.dialect))
else:
self._execute(statement.sql)
Expand Down Expand Up @@ -237,8 +242,8 @@ def execute(self, sql: str) -> list[tuple]:
remaining_uses = used_temporary.copy()
for remaining in statements[statement_index:]:
remaining_uses.update(self._temporary_reads(remaining, pending_temporary))
if isinstance(remaining.parsed, exp.Drop) and not remaining.parsed.this.db:
remaining_uses.add(remaining.parsed.this.name.lower())
if isinstance(remaining.parsed, exp.Drop) and not _drop_target(remaining.parsed).db:
remaining_uses.add(_drop_target(remaining.parsed).name.lower())
if pending_temporary - remaining_uses:
raise RuntimeError(
"TEMPORARY AS MEASURE views must be used in the same statement batch as AGGREGATE()"
Expand Down
Loading
Loading