Skip to content

Implement date trunc #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
64 changes: 64 additions & 0 deletions dbt/include/sqlite/macros/utils/date_trunc.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% macro sqlite__date_trunc(datepart, date) -%}
{#- Truncate a date to a specified datepart.

`sqlite__date_trunc` always returns a datetime.

Only supports the following date parts:
"year", "quarter", "month", "week", "day", "hour", "minute", "second"

Makes use of official datetime modifiers when possible.
For details of date and time functions in sqlite, refer to the official documentation:
https://www.sqlite.org/lang_datefunc.html -#}
{#- datepart can be quoted or unquoted, lower or uppercase -#}
{%- set datepart = datepart.lower().strip("'") -%}

{#- use the official modifier whenever possible -#}
{%- if datepart == "year" -%} datetime({{ date }}, 'start of year')
{%- elif datepart == "month" -%} datetime({{ date }}, 'start of month')
{%- elif datepart == "day" -%} datetime({{ date }}, 'start of day')

{%- elif datepart == "quarter" -%}
{#- truncate to start of year, then add necessary number of months -#}
{#- note that we make use of integer division to round down -#}
datetime(
{{ date }},
'start of year',
'+' || cast((strftime('%m', {{ date }}) - 1) / 3 * 3 as text) || " month"
)

{%- elif datepart == "week" -%}
{#- remove {day_no} days and truncate to start of day -#}
{#- note that week starts at Sunday, i.e. Sunday=0 -#}
datetime({{ date }}, ('-' || strftime('%w', {{ date }}) || ' day'), 'start of day')

{%- elif datepart == "hour" -%}
{#- truncate to start of day, then add back hours -#}
datetime({{ date }}, 'start of day', '+' || strftime('%H', {{ date }}) || " hour")

{%- elif datepart == "minute" -%}
{#- truncate to start of day, then add back hours and minutes -#}
datetime(
{{ date }},
'start of day',
'+' || strftime('%H', {{ date }}) || " hour",
'+' || strftime('%M', {{ date }}) || " minute"
)
{%- elif datepart == "second" -%}
{#- truncate to start of day, then add back hours, minutes, seconds -#}
datetime(
{{ date }},
'start of day',
'+' || strftime('%H', {{ date }}) || " hour",
'+' || strftime('%M', {{ date }}) || " minute",
'+' || strftime('%S', {{ date }}) || " second"
)

{%- else -%}
{#- arithmetics for micro-/nanoseconds is more complicated, skipped for now -#}
{{
exceptions.raise_compiler_error(
"Unsupported datepart for macro date_trunc in sqlite: {!r}".format(datepart)
)
}}
{%- endif -%}
{%- endmacro %}
49 changes: 45 additions & 4 deletions tests/functional/adapter/utils/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from dbt.tests.adapter.utils.base_utils import BaseUtils
from dbt.tests.adapter.utils.fixture_date_trunc import models__test_date_trunc_yml
from dbt.tests.adapter.utils.fixture_datediff import (
seeds__data_datediff_csv,
models__test_datediff_yml,
Expand All @@ -14,7 +15,6 @@
from dbt.tests.adapter.utils.test_current_timestamp import BaseCurrentTimestampNaive
from dbt.tests.adapter.utils.test_dateadd import BaseDateAdd
#from dbt.tests.adapter.utils.test_datediff import BaseDateDiff
from dbt.tests.adapter.utils.test_date_trunc import BaseDateTrunc
from dbt.tests.adapter.utils.test_escape_single_quotes import BaseEscapeSingleQuotesQuote
from dbt.tests.adapter.utils.test_escape_single_quotes import BaseEscapeSingleQuotesBackslash
from dbt.tests.adapter.utils.test_except import BaseExcept
Expand Down Expand Up @@ -129,9 +129,50 @@ class TestDateDiff(BaseDateDiff):
pass


@pytest.mark.skip("TODO: implement date_trunc")
class TestDateTrunc(BaseDateTrunc):
pass
class TestDateTrunc(BaseUtils):
seeds__data_date_trunc_csv = """date_to_trunc,year,quarter,month,week,day,hour,minute,second
1999-12-31 23:59:59.999999,1999-01-01 00:00:00,1999-10-01 00:00:00,1999-12-01 00:00:00,1999-12-26 00:00:00,1999-12-31 00:00:00,1999-12-31 23:00:00,1999-12-31 23:59:00,1999-12-31 23:59:59
1999-12-31,1999-01-01 00:00:00,1999-10-01 00:00:00,1999-12-01 00:00:00,1999-12-26 00:00:00,1999-12-31 00:00:00,1999-12-31 00:00:00,1999-12-31 00:00:00,1999-12-31 00:00:00
1999-01-01,1999-01-01 00:00:00,1999-01-01 00:00:00,1999-01-01 00:00:00,1998-12-27 00:00:00,1999-01-01 00:00:00,1999-01-01 00:00:00,1999-01-01 00:00:00,1999-01-01 00:00:00
1999-05-16 12:21,1999-01-01 00:00:00,1999-04-01 00:00:00,1999-05-01 00:00:00,1999-05-16 00:00:00,1999-05-16 00:00:00,1999-05-16 12:00:00,1999-05-16 12:21:00,1999-05-16 12:21:00
"""

models__test_date_trunc_sql = """
with data as (
select * from {{ ref('data_date_trunc') }}
)
""" + " union all ".join(
"""
select
{{{{ date_trunc("{datepart}", "date_to_trunc") }}}} as actual,
{datepart} as expected
from data""".format(
datepart=datepart
)
for datepart in [
"year",
"quarter",
"month",
"week",
"day",
"hour",
"minute",
"second",
]
)

@pytest.fixture(scope="class")
def seeds(self):
return {"data_date_trunc.csv": self.seeds__data_date_trunc_csv}

@pytest.fixture(scope="class")
def models(self):
return {
"test_date_trunc.yml": models__test_date_trunc_yml,
"test_date_trunc.sql": self.interpolate_macro_namespace(
self.models__test_date_trunc_sql, "date_trunc"
),
}


class TestEscapeSingleQuotes(BaseEscapeSingleQuotesQuote):
Expand Down