Skip to content
Open
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
11 changes: 9 additions & 2 deletions sqlglot/expressions/temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,11 +450,18 @@ class ParseTime(Expression, Func):


class StrToDate(Expression, Func):
arg_types = {"this": True, "format": False, "safe": False}
arg_types = {"this": True, "format": False, "safe": False, "default_year": False}


class StrToTime(Expression, Func):
arg_types = {"this": True, "format": True, "zone": False, "safe": False, "target_type": False}
arg_types = {
"this": True,
"format": True,
"zone": False,
"safe": False,
"target_type": False,
"default_year": False,
}


class StrToUnix(Expression, Func):
Expand Down
45 changes: 28 additions & 17 deletions sqlglot/generators/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
rename_func,
remove_from_array_using_filter,
strposition_sql,
str_to_time_sql,
timestrtotime_sql,
unit_to_str,
)
Expand Down Expand Up @@ -2751,14 +2750,15 @@ def strtotime_sql(self, expression: exp.StrToTime) -> str:
exp.DType.TIMESTAMPTZ,
)

value, formatted_time = self._strptime_default_year(
expression, expression.this, self.format_time(expression)
)

if expression.args.get("safe"):
formatted_time = self.format_time(expression)
cast_type = exp.DType.TIMESTAMPTZ if needs_tz else exp.DType.TIMESTAMP
return self.sql(
exp.cast(self.func("TRY_STRPTIME", expression.this, formatted_time), cast_type)
)
return self.sql(exp.cast(self.func("TRY_STRPTIME", value, formatted_time), cast_type))

base_sql = str_to_time_sql(self, expression)
base_sql = self.func("STRPTIME", value, formatted_time)
if needs_tz:
return self.sql(
exp.cast(
Expand All @@ -2769,27 +2769,38 @@ def strtotime_sql(self, expression: exp.StrToTime) -> str:
return base_sql

def strtodate_sql(self, expression: exp.StrToDate) -> str:
formatted_time = self.format_time(expression)
value, formatted_time = self._strptime_default_year(
expression, expression.this, self.format_time(expression)
)
function_name = "STRPTIME" if not expression.args.get("safe") else "TRY_STRPTIME"
return self.sql(
exp.cast(
self.func(function_name, expression.this, formatted_time),
self.func(function_name, value, formatted_time),
exp.DataType(this=exp.DType.DATE),
)
)

def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
formatted_time = self.format_time(expression)

def _strptime_default_year(
self,
expression: exp.Expr,
value: exp.ExpOrStr | None,
formatted_time: exp.ExpOrStr | None,
) -> tuple[exp.ExpOrStr | None, exp.ExpOrStr | None]:
# BigQuery initializes a missing year to 1970 while DuckDB defaults to 1900, so when
# default_year is set we prepend it to both the value and the format. DuckDB binds the
# last %Y match, so a year already present in the input overrides the prefix harmlessly.
default_year = expression.args.get("default_year")
if default_year:
year_str = exp.Literal.string(f"{default_year.name} ")
fmt_prefix = exp.Literal.string("%Y ")
value = exp.DPipe(this=year_str, expression=expression.this)
fmt = exp.DPipe(this=fmt_prefix, expression=formatted_time)
return self.func("STRPTIME", value, fmt)
value = exp.DPipe(this=exp.Literal.string(f"{default_year.name} "), expression=value)
formatted_time = exp.DPipe(this=exp.Literal.string("%Y "), expression=formatted_time)

return self.func("STRPTIME", expression.this, formatted_time)
return value, formatted_time

def parsedatetime_sql(self, expression: exp.ParseDatetime) -> str:
value, formatted_time = self._strptime_default_year(
expression, expression.this, self.format_time(expression)
)
return self.func("STRPTIME", value, formatted_time)

def parsetime_sql(self, expression: exp.ParseTime) -> str:
formatted_time = self.format_time(expression)
Expand Down
14 changes: 11 additions & 3 deletions sqlglot/parsers/bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,19 @@ def _build_levenshtein(args: list) -> exp.Levenshtein:
)


# BigQuery initializes any field left unspecified by the format string from
# 1970-01-01 00:00:00.0, so a missing year defaults to 1970 (DuckDB defaults to 1900).
# The default_year flag carries this so the relevant generators can compensate.
def _build_parse_date(args: list, dialect: Dialect) -> exp.StrToDate:
this = build_formatted_time(exp.StrToDate)([seq_get(args, 1), seq_get(args, 0)], dialect)
this.set("default_year", exp.Literal.number(1970))
return this


def _build_parse_timestamp(args: list, dialect: Dialect) -> exp.StrToTime:
this = build_formatted_time(exp.StrToTime)([seq_get(args, 1), seq_get(args, 0)], dialect)
this.set("zone", seq_get(args, 2))
this.set("default_year", exp.Literal.number(1970))
return this


Expand Down Expand Up @@ -248,9 +258,7 @@ class BigQueryParser(parser.Parser):
),
"OCTET_LENGTH": exp.ByteLength.from_arg_list,
"TO_HEX": _build_to_hex,
"PARSE_DATE": lambda args, dialect: build_formatted_time(exp.StrToDate)(
[seq_get(args, 1), seq_get(args, 0)], dialect
),
"PARSE_DATE": _build_parse_date,
"PARSE_TIME": lambda args, dialect: build_formatted_time(exp.ParseTime)(
[seq_get(args, 1), seq_get(args, 0)], dialect
),
Expand Down
22 changes: 18 additions & 4 deletions tests/dialects/test_bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def test_bigquery(self):
"PARSE_TIMESTAMP('%Y-%m-%dT%H:%M:%E6S%z', x)",
write={
"bigquery": "PARSE_TIMESTAMP('%FT%H:%M:%E6S%z', x)",
"duckdb": "STRPTIME(x, '%Y-%m-%dT%H:%M:%S.%f%z')",
"duckdb": "STRPTIME('1970 ' || x, '%Y ' || '%Y-%m-%dT%H:%M:%S.%f%z')",
},
)
self.validate_all(
Expand Down Expand Up @@ -620,7 +620,7 @@ def test_bigquery(self):
"PARSE_TIMESTAMP('%Y-%m-%dT%H:%M:%E6S%z', x)",
write={
"bigquery": "PARSE_TIMESTAMP('%FT%H:%M:%E6S%z', x)",
"duckdb": "STRPTIME(x, '%Y-%m-%dT%H:%M:%S.%f%z')",
"duckdb": "STRPTIME('1970 ' || x, '%Y ' || '%Y-%m-%dT%H:%M:%S.%f%z')",
},
)
self.validate_all(
Expand Down Expand Up @@ -1799,17 +1799,31 @@ def test_bigquery(self):
"SELECT PARSE_DATE('%A %b %e %Y', 'Thursday Dec 25 2008')",
write={
"bigquery": "SELECT PARSE_DATE('%A %b %e %Y', 'Thursday Dec 25 2008')",
"duckdb": "SELECT CAST(STRPTIME('Thursday Dec 25 2008', '%A %b %-d %Y') AS DATE)",
"duckdb": "SELECT CAST(STRPTIME('1970 ' || 'Thursday Dec 25 2008', '%Y ' || '%A %b %-d %Y') AS DATE)",
},
)
self.validate_all(
"SELECT PARSE_DATE('%Y%m%d', '20081225')",
write={
"bigquery": "SELECT PARSE_DATE('%Y%m%d', '20081225')",
"duckdb": "SELECT CAST(STRPTIME('20081225', '%Y%m%d') AS DATE)",
"duckdb": "SELECT CAST(STRPTIME('1970 ' || '20081225', '%Y ' || '%Y%m%d') AS DATE)",
"snowflake": "SELECT DATE('20081225', 'yyyymmDD')",
},
)
# BigQuery defaults a missing year to 1970 while DuckDB defaults to 1900, so a yearless
# PARSE_DATE / PARSE_TIMESTAMP must prepend 1970, matching PARSE_DATETIME.
self.validate_all(
"SELECT PARSE_DATE('%m-%d', '12-25')",
write={
"duckdb": "SELECT CAST(STRPTIME('1970 ' || '12-25', '%Y ' || '%m-%d') AS DATE)",
},
)
self.validate_all(
"SELECT PARSE_TIMESTAMP('%m-%d %H:%M:%S', '12-25 07:30:00')",
write={
"duckdb": "SELECT STRPTIME('1970 ' || '12-25 07:30:00', '%Y ' || '%m-%d %H:%M:%S')",
},
)
self.validate_all(
"SELECT ARRAY_TO_STRING(['cake', 'pie', NULL], '--') AS text",
write={
Expand Down
2 changes: 1 addition & 1 deletion tests/dialects/test_duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,7 @@ def test_time(self):
},
)
self.validate_all(
"SELECT CAST(CAST(STRPTIME('05/06/2020', '%m/%d/%Y') AS DATE) AS DATE)",
"SELECT CAST(CAST(STRPTIME('1970 ' || '05/06/2020', '%Y ' || '%m/%d/%Y') AS DATE) AS DATE)",
read={
"bigquery": "SELECT DATE(PARSE_DATE('%m/%d/%Y', '05/06/2020'))",
},
Expand Down
Loading