Skip to content

Commit 1233d63

Browse files
tz_issue_fix
Signed-off-by: Ajay-Satish-01 <ajay.satishpriya@gmail.com>
1 parent ee960d1 commit 1233d63

20 files changed

Lines changed: 824 additions & 52 deletions

File tree

docs/concepts/macros/macro_variables.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@ SQLMesh uses the python [datetime module](https://docs.python.org/3/library/date
5555

5656
!!! tip "Important"
5757

58-
Predefined variables with a time component always use the [UTC time zone](https://en.wikipedia.org/wiki/Coordinated_Universal_Time).
58+
Macro instants such as `@start_dt`, `@end_dt`, `@start_tstz`, and `@end_tstz` are always stored and rendered as [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) timestamps. During incremental backfill, `@start_ds` and `@end_ds` also use UTC calendar dates derived from those interval boundaries.
5959

6060
Learn more about timezones and incremental models [here](../models/model_kinds.md#timezones).
6161

62+
Relative CLI and API inputs such as `--start "2 weeks ago"` are interpreted using UTC calendar-day boundaries by default. To anchor relative start, end, and execution-time values to a specific timezone (for example, midnight in `America/Los_Angeles`), pass `--time-zone` on supported commands (`plan`, `render`, `evaluate`, `run`, `audit`, `check_intervals`) or set the project-level `time_zone` config. The CLI flag overrides the config value.
63+
64+
When a **day-or-larger** relative start or end (for example, `"1 week ago"`, `"today"`, `"yesterday"`) is parsed with a configured timezone, `@start_tstz` and `@end_tstz` reflect the correct UTC instant and `@start_ds` / `@end_ds` use that timezone's local calendar date in `render`, `evaluate`, and `audit`. Sub-day relatives such as `"2 hours ago"` ignore `--time-zone` and continue to use UTC-relative parsing. Absolute date strings and `@execution_ds` always use UTC calendar dates.
65+
6266
Prefixes:
6367

6468
* start - The inclusive starting interval of a model run

sqlmesh/cli/main.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ def init(
255255
@opt.start_time
256256
@opt.end_time
257257
@opt.execution_time
258+
@opt.time_zone
258259
@opt.expand
259260
@click.option(
260261
"--dialect",
@@ -272,6 +273,7 @@ def render(
272273
start: TimeLike,
273274
end: TimeLike,
274275
execution_time: t.Optional[TimeLike] = None,
276+
time_zone: t.Optional[str] = None,
275277
expand: t.Optional[t.Union[bool, t.Iterable[str]]] = None,
276278
dialect: t.Optional[str] = None,
277279
no_format: bool = False,
@@ -285,6 +287,7 @@ def render(
285287
start=start,
286288
end=end,
287289
execution_time=execution_time,
290+
time_zone=time_zone,
288291
expand=expand,
289292
)
290293

@@ -310,6 +313,7 @@ def render(
310313
@opt.start_time
311314
@opt.end_time
312315
@opt.execution_time
316+
@opt.time_zone
313317
@click.option(
314318
"--limit",
315319
type=int,
@@ -324,6 +328,7 @@ def evaluate(
324328
start: TimeLike,
325329
end: TimeLike,
326330
execution_time: t.Optional[TimeLike] = None,
331+
time_zone: t.Optional[str] = None,
327332
limit: t.Optional[int] = None,
328333
) -> None:
329334
"""Evaluate a model and return a dataframe with a default limit of 1000."""
@@ -332,6 +337,7 @@ def evaluate(
332337
start=start,
333338
end=end,
334339
execution_time=execution_time,
340+
time_zone=time_zone,
335341
limit=limit,
336342
)
337343
if hasattr(df, "show"):
@@ -394,6 +400,7 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None:
394400
@opt.start_time
395401
@opt.end_time
396402
@opt.execution_time
403+
@opt.time_zone
397404
@click.option(
398405
"--create-from",
399406
type=str,
@@ -574,6 +581,7 @@ def plan(
574581
@click.argument("environment", required=False)
575582
@opt.start_time
576583
@opt.end_time
584+
@opt.time_zone
577585
@click.option("--skip-janitor", is_flag=True, help="Skip the janitor task.")
578586
@click.option(
579587
"--ignore-cron",
@@ -796,6 +804,7 @@ def test(
796804
@opt.start_time
797805
@opt.end_time
798806
@opt.execution_time
807+
@opt.time_zone
799808
@click.pass_obj
800809
@error_handler
801810
@cli_analytics
@@ -805,9 +814,12 @@ def audit(
805814
start: TimeLike,
806815
end: TimeLike,
807816
execution_time: t.Optional[TimeLike] = None,
817+
time_zone: t.Optional[str] = None,
808818
) -> None:
809819
"""Run audits for the target model(s)."""
810-
if not obj.audit(models=models, start=start, end=end, execution_time=execution_time):
820+
if not obj.audit(
821+
models=models, start=start, end=end, execution_time=execution_time, time_zone=time_zone
822+
):
811823
exit(1)
812824

813825

@@ -827,6 +839,7 @@ def audit(
827839
)
828840
@opt.start_time
829841
@opt.end_time
842+
@opt.time_zone
830843
@click.pass_context
831844
@error_handler
832845
@cli_analytics
@@ -837,6 +850,7 @@ def check_intervals(
837850
select_model: t.List[str],
838851
start: TimeLike,
839852
end: TimeLike,
853+
time_zone: t.Optional[str] = None,
840854
) -> None:
841855
"""Show missing intervals in an environment, respecting signals."""
842856
context = ctx.obj
@@ -847,6 +861,7 @@ def check_intervals(
847861
select_models=select_model,
848862
start=start,
849863
end=end,
864+
time_zone=time_zone,
850865
)
851866
)
852867

sqlmesh/cli/options.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
help="The execution time (defaults to now).",
3838
)
3939

40+
time_zone = click.option(
41+
"--time-zone",
42+
help="IANA timezone for interpreting relative --start, --end, and --execution-time values (e.g. America/Los_Angeles). Defaults to UTC.",
43+
)
44+
4045
expand = click.option(
4146
"--expand",
4247
multiple=True,

sqlmesh/core/config/connection.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,10 +1806,27 @@ def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
18061806
from pyspark.conf import SparkConf
18071807
from pyspark.sql import SparkSession
18081808

1809+
from sqlmesh.utils.errors import ConfigError
1810+
from sqlmesh.utils.java import (
1811+
is_spark_java_supported,
1812+
java_major_version,
1813+
spark_java_options,
1814+
)
1815+
1816+
if not is_spark_java_supported():
1817+
raise ConfigError(
1818+
f"Spark is not supported on Java {java_major_version() or 'unknown'}. "
1819+
"Use Java 17 through 23 when running Spark locally."
1820+
)
1821+
18091822
spark_config = SparkConf()
1810-
if self.config:
1811-
for k, v in self.config.items():
1812-
spark_config.set(k, v)
1823+
config = dict(self.config or {})
1824+
java_options = spark_java_options(config.pop("spark.driver.extraJavaOptions", ""))
1825+
if java_options:
1826+
config["spark.driver.extraJavaOptions"] = java_options
1827+
1828+
for k, v in config.items():
1829+
spark_config.set(k, v)
18131830

18141831
if self.config_dir:
18151832
os.environ["SPARK_CONF_DIR"] = self.config_dir

sqlmesh/core/config/root.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from sqlmesh.core.loader import Loader, SqlMeshLoader
4747
from sqlmesh.core.notification_target import NotificationTarget
4848
from sqlmesh.core.user import User
49-
from sqlmesh.utils.date import to_timestamp, now
49+
from sqlmesh.utils.date import parse_time_zone, to_timestamp, now
5050
from sqlmesh.utils.errors import ConfigError
5151
from sqlmesh.utils.pydantic import model_validator
5252

@@ -76,16 +76,26 @@ def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re
7676
return compile_regex_mapping(value)
7777

7878

79+
def validate_time_zone(v: t.Any) -> t.Optional[str]:
80+
if not v or v == "UTC":
81+
return None
82+
v = str(v)
83+
parse_time_zone(v)
84+
return v
85+
86+
7987
if t.TYPE_CHECKING:
8088
from sqlmesh.core._typing import Self
8189

8290
NoPastTTLString = str
8391
GatewayDict = t.Dict[str, GatewayConfig]
8492
RegexKeyDict = t.Dict[re.Pattern, str]
93+
TimeZoneString = t.Optional[str]
8594
else:
8695
NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)]
8796
GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)]
8897
RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)]
98+
TimeZoneString = t.Annotated[t.Optional[str], BeforeValidator(validate_time_zone)]
8999

90100

91101
class Config(BaseConfig):
@@ -129,6 +139,8 @@ class Config(BaseConfig):
129139
before_all: SQL statements or macros to be executed at the start of the `sqlmesh plan` and `sqlmesh run` commands.
130140
after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands.
131141
cache_dir: The directory to store the SQLMesh cache. Defaults to .cache in the project folder.
142+
time_zone: IANA timezone for interpreting relative start, end, and execution-time values.
143+
Defaults to UTC when unset.
132144
"""
133145

134146
gateways: GatewayDict = {"": GatewayConfig()}
@@ -174,6 +186,7 @@ class Config(BaseConfig):
174186
linter: LinterConfig = LinterConfig()
175187
janitor: JanitorConfig = JanitorConfig()
176188
cache_dir: t.Optional[str] = None
189+
time_zone: TimeZoneString = None
177190
dbt: t.Optional[DbtConfig] = None
178191

179192
_FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = {

0 commit comments

Comments
 (0)