diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml new file mode 100644 index 0000000..62e7972 --- /dev/null +++ b/.github/workflows/black.yaml @@ -0,0 +1,17 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - uses: psf/black@stable + - uses: jamescurtin/isort-action@master + with: + requirementsFiles: "requirements.txt requirements-dev.txt" + configuration: "--check-only --diff --profile black" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21d2992 --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +.idea +src/awesome_date_dimension/.venv +output/test +test.py +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..399707c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black"] + name: isort (python) +- repo: local + hooks: + - id: unittest + name: unittest + entry: python -m unittest discover -s tests + language: system + 'types': [python] + pass_filenames: false + stages: [commit] diff --git a/README.md b/README.md index 62d1d48..1768696 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + # awesome-date-dimension A few months back, I had to create a date dimension. All of the scripts I could find publicly were missing a lot of the flags and other features I needed (especially around fiscal month handling) -- so I created one myself. This is written in T-SQL, but shouldn't be _too_ hard to port to another dialect of SQL. diff --git a/awesome_date_dimension/__init__.py b/awesome_date_dimension/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/awesome_date_dimension/_internal/__init__.py b/awesome_date_dimension/_internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/awesome_date_dimension/_internal/tsql/__init__.py b/awesome_date_dimension/_internal/tsql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/awesome_date_dimension/_internal/tsql/dim_calendar_month_constraints_template.py b/awesome_date_dimension/_internal/tsql/dim_calendar_month_constraints_template.py new file mode 100644 index 0000000..3f721de --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_calendar_month_constraints_template.py @@ -0,0 +1,11 @@ +from ...config import Config + + +def dim_calendar_month_constraints_template(config: Config) -> str: + dcm_conf = config.dim_calendar_month + return f"""ALTER TABLE {dcm_conf.table_schema}.{dcm_conf.table_name} +ADD PRIMARY KEY CLUSTERED ({dcm_conf.columns.month_start_key.name}, {dcm_conf.columns.month_end_key.name} ASC); + +CREATE NONCLUSTERED INDEX IDX_NC_{dcm_conf.table_schema}_{dcm_conf.table_name}_{dcm_conf.columns.month_start_date.name} ON {dcm_conf.table_schema}.{dcm_conf.table_name} ({dcm_conf.columns.month_start_date.name}); +CREATE NONCLUSTERED INDEX IDX_NC_{dcm_conf.table_schema}_{dcm_conf.table_name}_{dcm_conf.columns.month_end_date.name} ON {dcm_conf.table_schema}.{dcm_conf.table_name} ({dcm_conf.columns.month_end_date.name}); +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_calendar_month_insert_template.py b/awesome_date_dimension/_internal/tsql/dim_calendar_month_insert_template.py new file mode 100644 index 0000000..47986b2 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_calendar_month_insert_template.py @@ -0,0 +1,148 @@ +from ...config import Config +from .tsql_columns import TSQLDimCalendarMonthColumns + + +def dim_calendar_month_insert_template( + config: Config, columns: TSQLDimCalendarMonthColumns +) -> str: + dcm_conf = config.dim_calendar_month + dcm_cols = dcm_conf.columns + dd_conf = config.dim_date + dd_cols = dd_conf.columns + h_conf = config.holidays + holiday_columndef: list[str] = [] + holiday_colselect: list[str] = [] + if h_conf.generate_holidays: + for i, t in enumerate(h_conf.holiday_types): + holiday_columndef.append( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix} = SUM({t.generated_column_prefix}{t.generated_flag_column_postfix} * 1)" + ) + holiday_colselect.append( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix}" + ) + + holiday_columndef_str = ",\n ".join(holiday_columndef) + else: + holiday_columndef_str = "" + + dcm_to_dd_colmap = { + dcm_cols.month_start_date.name: f"startdate.{dd_cols.the_date.name}", + dcm_cols.month_end_date.name: f"enddate.{dd_cols.the_date.name}", + dcm_cols.month_start_iso_date_name.name: f"startdate.{dd_cols.iso_date_name.name}", + dcm_cols.month_end_iso_date_name.name: f"enddate.{dd_cols.iso_date_name.name}", + dcm_cols.month_start_iso_week_date_name.name: f"startdate.{dd_cols.iso_week_date_name.name}", + dcm_cols.month_end_iso_week_date_name.name: f"enddate.{dd_cols.iso_week_date_name.name}", + dcm_cols.month_start_american_date_name.name: f"startdate.{dd_cols.american_date_name.name}", + dcm_cols.month_end_american_date_name.name: f"enddate.{dd_cols.american_date_name.name}", + dcm_cols.month_name.name: f"startdate.{dd_cols.month_name.name}", + dcm_cols.month_abbrev.name: f"startdate.{dd_cols.month_abbrev.name}", + dcm_cols.month_start_year_week_name.name: f"startdate.{dd_cols.year_week_name.name}", + dcm_cols.month_end_year_week_name.name: f"enddate.{dd_cols.year_week_name.name}", + dcm_cols.year_month_name.name: f"startdate.{dd_cols.year_month_name.name}", + dcm_cols.month_year_name.name: f"startdate.{dd_cols.month_year_name.name}", + dcm_cols.year_quarter_name.name: f"startdate.{dd_cols.year_quarter_name.name}", + dcm_cols.year.name: f"startdate.{dd_cols.year.name}", + dcm_cols.month_start_year_week.name: f"startdate.{dd_cols.year_week.name}", + dcm_cols.month_end_year_week.name: f"enddate.{dd_cols.year_week.name}", + dcm_cols.year_month.name: f"startdate.{dd_cols.year_month.name}", + dcm_cols.year_quarter.name: f"startdate.{dd_cols.year_quarter.name}", + dcm_cols.month_start_day_of_quarter.name: f"startdate.{dd_cols.day_of_quarter.name}", + dcm_cols.month_end_day_of_quarter.name: f"enddate.{dd_cols.day_of_quarter.name}", + dcm_cols.month_start_day_of_year.name: f"startdate.{dd_cols.day_of_year.name}", + dcm_cols.month_end_day_of_year.name: f"enddate.{dd_cols.day_of_year.name}", + dcm_cols.month_start_week_of_quarter.name: f"startdate.{dd_cols.week_of_quarter.name}", + dcm_cols.month_end_week_of_quarter.name: f"enddate.{dd_cols.week_of_quarter.name}", + dcm_cols.month_start_week_of_year.name: f"startdate.{dd_cols.week_of_year.name}", + dcm_cols.month_end_week_of_year.name: f"enddate.{dd_cols.week_of_year.name}", + dcm_cols.month_of_quarter.name: f"startdate.{dd_cols.month_of_quarter.name}", + dcm_cols.quarter.name: f"startdate.{dd_cols.quarter.name}", + dcm_cols.days_in_month.name: f"startdate.{dd_cols.days_in_month.name}", + dcm_cols.days_in_quarter.name: f"startdate.{dd_cols.days_in_quarter.name}", + dcm_cols.days_in_year.name: f"startdate.{dd_cols.days_in_year.name}", + dcm_cols.current_month_flag.name: f"startdate.{dd_cols.current_month_flag.name}", + dcm_cols.prior_month_flag.name: f"startdate.{dd_cols.prior_month_flag.name}", + dcm_cols.next_month_flag.name: f"startdate.{dd_cols.next_month_flag.name}", + dcm_cols.current_quarter_flag.name: f"startdate.{dd_cols.current_quarter_flag.name}", + dcm_cols.prior_quarter_flag.name: f"startdate.{dd_cols.prior_quarter_flag.name}", + dcm_cols.next_quarter_flag.name: f"startdate.{dd_cols.next_quarter_flag.name}", + dcm_cols.current_year_flag.name: f"startdate.{dd_cols.current_year_flag.name}", + dcm_cols.prior_year_flag.name: f"startdate.{dd_cols.prior_year_flag.name}", + dcm_cols.next_year_flag.name: f"startdate.{dd_cols.next_year_flag.name}", + dcm_cols.first_day_of_month_flag.name: f"startdate.{dd_cols.first_day_of_month_flag.name}", + dcm_cols.last_day_of_month_flag.name: f"startdate.{dd_cols.last_day_of_month_flag.name}", + dcm_cols.first_day_of_quarter_flag.name: f"startdate.{dd_cols.first_day_of_quarter_flag.name}", + dcm_cols.last_day_of_quarter_flag.name: f"startdate.{dd_cols.last_day_of_quarter_flag.name}", + dcm_cols.first_day_of_year_flag.name: f"startdate.{dd_cols.first_day_of_year_flag.name}", + dcm_cols.last_day_of_year_flag.name: f"startdate.{dd_cols.last_day_of_year_flag.name}", + dcm_cols.month_start_fraction_of_quarter.name: f"startdate.{dd_cols.fraction_of_quarter.name}", + dcm_cols.month_end_fraction_of_quarter.name: f"enddate.{dd_cols.fraction_of_quarter.name}", + dcm_cols.month_start_fraction_of_year.name: f"startdate.{dd_cols.fraction_of_year.name}", + dcm_cols.month_end_fraction_of_year.name: f"enddate.{dd_cols.fraction_of_year.name}", + dcm_cols.current_quarter_start.name: f"startdate.{dd_cols.current_quarter_start.name}", + dcm_cols.current_quarter_end.name: f"startdate.{dd_cols.current_quarter_end.name}", + dcm_cols.current_year_start.name: f"startdate.{dd_cols.current_year_start.name}", + dcm_cols.current_year_end.name: f"startdate.{dd_cols.current_year_end.name}", + dcm_cols.prior_month_start.name: f"startdate.{dd_cols.prior_month_start.name}", + dcm_cols.prior_month_end.name: f"startdate.{dd_cols.prior_month_end.name}", + dcm_cols.prior_quarter_start.name: f"startdate.{dd_cols.prior_quarter_start.name}", + dcm_cols.prior_quarter_end.name: f"startdate.{dd_cols.prior_quarter_end.name}", + dcm_cols.prior_year_start.name: f"startdate.{dd_cols.prior_year_start.name}", + dcm_cols.prior_year_end.name: f"startdate.{dd_cols.prior_year_end.name}", + dcm_cols.next_month_start.name: f"startdate.{dd_cols.next_month_start.name}", + dcm_cols.next_month_end.name: f"startdate.{dd_cols.next_month_end.name}", + dcm_cols.next_quarter_start.name: f"startdate.{dd_cols.next_quarter_start.name}", + dcm_cols.next_quarter_end.name: f"startdate.{dd_cols.next_quarter_end.name}", + dcm_cols.next_year_start.name: f"startdate.{dd_cols.next_year_start.name}", + dcm_cols.next_year_end.name: f"startdate.{dd_cols.next_year_end.name}", + dcm_cols.month_start_quarterly_burnup.name: f"startdate.{dd_cols.quarterly_burnup.name}", + dcm_cols.month_end_quarterly_burnup.name: f"enddate.{dd_cols.quarterly_burnup.name}", + dcm_cols.month_start_yearly_burnup.name: f"startdate.{dd_cols.yearly_burnup.name}", + dcm_cols.month_end_yearly_burnup.name: f"enddate.{dd_cols.yearly_burnup.name}", + } + + insert_columns_clause = ",\n ".join((c.name for c in columns)) + select_columns = [] + for col in columns: + if (dd_name := dcm_to_dd_colmap.get(col.name)) is not None: + select_columns.append(f"{col.name} = {dd_name}") + else: + select_columns.append(f"{col.name} = base.{col.name}") + + select_columns_clause = ",\n ".join(select_columns) + + return f"""WITH DistinctMonths AS ( + SELECT + {dcm_cols.month_start_key.name} = CONVERT( + int, + CONVERT( + varchar(8), + {dd_cols.current_month_start.name}, + 112 + ) + ), + {dcm_cols.month_end_key.name} = CONVERT( + int, + CONVERT( + varchar(8), + {dd_cols.current_month_end.name}, + 112 + ) + ), + {holiday_columndef_str} + FROM + {dd_conf.table_schema}.{dd_conf.table_name} + GROUP BY {dd_cols.current_month_start.name}, {dd_cols.current_month_end.name} +) + +INSERT INTO {dcm_conf.table_schema}.{dcm_conf.table_name} ( + {insert_columns_clause} +) +-- Yank the day-level stuff we need for both the start and end dates from {dd_conf.table_name} +SELECT + {select_columns_clause} +FROM + DistinctMonths AS base + INNER JOIN {dd_conf.table_schema}.{dd_conf.table_name} AS startdate + ON base.{dcm_cols.month_start_key.name} = startdate.{dd_cols.date_key.name} + INNER JOIN {dd_conf.table_schema}.{dd_conf.table_name} AS enddate + ON base.{dcm_cols.month_end_key.name} = enddate.{dd_cols.date_key.name};""" diff --git a/awesome_date_dimension/_internal/tsql/dim_calendar_month_refresh_template.py b/awesome_date_dimension/_internal/tsql/dim_calendar_month_refresh_template.py new file mode 100644 index 0000000..7830cc8 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_calendar_month_refresh_template.py @@ -0,0 +1,39 @@ +from ...config import Config +from .dim_calendar_month_insert_template import dim_calendar_month_insert_template +from .tsql_columns import TSQLDimCalendarMonthColumns + + +def dim_calendar_month_refresh_template( + config: Config, columns: TSQLDimCalendarMonthColumns +) -> str: + indentation_level = " " + insert_script = dim_calendar_month_insert_template(config, columns) + indented_script = "\n".join( + map(lambda line: indentation_level + line, insert_script.split("\n")) + ) + return f"""CREATE PROCEDURE dbo.sp_build_DimCalendarMonth AS BEGIN + SET XACT_ABORT ON; + BEGIN TRY + BEGIN TRANSACTION; + + TRUNCATE TABLE dbo.DimCalendarMonth; + +{indented_script} + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + SELECT + ERROR_NUMBER() AS ErrorNumber, + ERROR_SEVERITY() AS ErrorSeverity, + ERROR_STATE() AS ErrorState, + ERROR_LINE () AS ErrorLine, + ERROR_PROCEDURE() AS ErrorProcedure, + ERROR_MESSAGE() AS ErrorMessage; + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; +END +GO +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_date_constraints_template.py b/awesome_date_dimension/_internal/tsql/dim_date_constraints_template.py new file mode 100644 index 0000000..b3cf3ff --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_date_constraints_template.py @@ -0,0 +1,10 @@ +from ...config import Config + + +def dim_date_constraints_template(config: Config) -> str: + dd_conf = config.dim_date + return f"""ALTER TABLE {dd_conf.table_schema}.{dd_conf.table_name} +ADD PRIMARY KEY CLUSTERED ({dd_conf.columns.date_key.name} ASC); + +CREATE NONCLUSTERED INDEX IDX_NC_{dd_conf.table_schema}_{dd_conf.table_name}_{dd_conf.columns.the_date.name} ON {dd_conf.table_schema}.{dd_conf.table_name} ({dd_conf.columns.the_date.name}); +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_date_insert_template.py b/awesome_date_dimension/_internal/tsql/dim_date_insert_template.py new file mode 100644 index 0000000..127c9a9 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_date_insert_template.py @@ -0,0 +1,2094 @@ +from ...config import Config +from .tsql_columns import TSQLDimDateColumns + + +# Note: Rather than trying to be "smart" and removing or adding columns based on whether or not they should be present, +# we just remove or rename them at the very end before the INSERT statement. The performance change is so negligible for a one-time-setup +# script that it doesn't make sense to take on the complication. +def dim_date_insert_template(config: Config, columns: TSQLDimDateColumns) -> str: + holiday_join = [] + business_day_list = [] + holiday_columndef = [] + if config.holidays.generate_holidays: + for i, t in enumerate(config.holidays.holiday_types): + holiday_join.append( + f" LEFT OUTER JOIN {config.holidays.holidays_schema_name}.{config.holidays.holidays_table_name} AS h{i} -- {t.name}" + ) + holiday_join.append( + f" ON fh.DateKey = h{i}.{config.holidays.holidays_columns.date_key.name} AND h{i}.{config.holidays.holidays_columns.holiday_type_key.name} = (SELECT {config.holidays.holiday_types_columns.holiday_type_key.name} FROM {config.holidays.holiday_types_schema_name}.{config.holidays.holiday_types_table_name} WHERE {config.holidays.holiday_types_columns.holiday_type_name.name} = '{t.name}')" + ) + if t.included_in_business_day_calc: + business_day_list.append( + f"h{i}.{config.holidays.holidays_columns.date_key.name} IS NOT NULL" + ) + + holiday_columndef.append( + f" {t.generated_column_prefix}{t.generated_flag_column_postfix} = IIF(" + ) + holiday_columndef.append( + f" h{i}.{config.holidays.holidays_columns.date_key.name} IS NOT NULL," + ) + holiday_columndef.append(f" 1,") + holiday_columndef.append(f" 0") + holiday_columndef.append(f" ),") + holiday_columndef.append("") + holiday_columndef.append( + f" {t.generated_column_prefix}{t.generated_name_column_postfix} = h{i}.{config.holidays.holidays_columns.holiday_name.name}," + ) + holiday_columndef.append("") + + holiday_join = "\n".join(holiday_join) + business_day_clause = "\n".join(business_day_list) + "\n OR " + holiday_column_clause = ",\n\n" + "\n".join(holiday_columndef)[:-2] + else: + holiday_join = "" + business_day_clause = "" + holiday_column_clause = "" + + insert_column_clause = ",\n ".join(map(lambda c: c.name, columns)) + + return f"""SET DATEFIRST 7; + +DECLARE @TodayInLocal date; +DECLARE @FirstDate date; +DECLARE @NumberOfYearsToGenerate int; +DECLARE @LastDate date; +DECLARE @FiscalMonthStartDay int; +DECLARE @FiscalYearStartMonth int; +DECLARE @FiscalMonthPeriodEndMatchesCalendar bit; +DECLARE @FiscalQuarterPeriodEndMatchesCalendar bit; +DECLARE @FiscalYearPeriodEndMatchesCalendar bit; +DECLARE @ISODatekeyFormatNumber int; +DECLARE @ISO8601DatestringFormatNumber int; +DECLARE @USDatestringFormatNumber int; + +SET @FirstDate='{config.date_range.start_date.isoformat()}'; +SET @NumberOfYearsToGenerate={config.date_range.num_years}; + + +SET @FiscalMonthStartDay={config.fiscal.month_start_day}; -- Cannot be >28 or you'll blow up +SET @FiscalYearStartMonth={config.fiscal.year_start_month}; + +-- Set @FiscalPeriodEndMatchesCalendar to 1 if +-- your fiscal calendar takes on the month/quarter of +-- the fiscal period end date rather than the start date. +-- For example, if your month runs the 26th through the 25th, +-- and Dec 25-Jan 26 is considered "January", set it to 1. +-- If Dec 25-Jan 26 is considered "December", set to 0. +SET @FiscalMonthPeriodEndMatchesCalendar={1 if config.fiscal.month_end_matches_calendar else 0}; +SET @FiscalQuarterPeriodEndMatchesCalendar={1 if config.fiscal.quarter_end_matches_calendar else 0}; +SET @FiscalYearPeriodEndMatchesCalendar={1 if config.fiscal.year_end_matches_calendar else 0}; + + +-- If you need to change what timezone you want to base your relative flags on +-- be sure to make sure your target system recognizes the timezone +SET @TodayInLocal = CONVERT( + date, + -- You need to figure out what your local TZ is called in your server host's registry. + -- See https://docs.microsoft.com/en-us/sql/t-sql/queries/at-time-zone-transact-sql?view=sql-server-ver15 + -- for more info. + -- For example, on my machine, for MST, the following line would be: + -- GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE 'Mountain Standard Time' + GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE IntentionallyCrashScriptIfItTriesToRun +); + +-- No touchie these lines; adjust the above instead +SET @LastDate=DATEADD(YEAR,@NumberOfYearsToGenerate,@FirstDate); +SET @ISODatekeyFormatNumber=112; +SET @ISO8601DatestringFormatNumber=23; +SET @USDatestringFormatNumber=101; + +-- For all comments, assume: +-- The {config.dim_date.columns.date_key.name} is 20210101 +-- @TodayInLocal = 2021-07-29 +-- @FiscalMonthStartDay=26; +-- @FiscalYearStartMonth=12; +-- @FiscalMonthPeriodEndMatchesCalendar=1; +-- @FiscalQuarterPeriodEndMatchesCalendar=1; +-- @FiscalYearPeriodEndMatchesCalendar=1; +WITH Recursion AS ( + SELECT + {config.dim_date.columns.date_key.name} = CONVERT( + int, + CONVERT( + varchar(8), + @FirstDate, + @ISODatekeyFormatNumber + ) + ), + + {config.dim_date.columns.the_date.name} = @FirstDate + UNION ALL + SELECT + CONVERT( + int, + CONVERT( + varchar(8), + DATEADD(DAY, 1, {config.dim_date.columns.the_date.name}), + @ISODatekeyFormatNumber + ) + ), + DATEADD(DAY, 1, {config.dim_date.columns.the_date.name}) + FROM Recursion + WHERE {config.dim_date.columns.the_date.name} < @LastDate +), + +BaseDatesFirst AS ( + SELECT + {config.dim_date.columns.date_key.name}, + {config.dim_date.columns.the_date.name}, + CalendarYearStart = DATEFROMPARTS( + YEAR({config.dim_date.columns.the_date.name}), + 01, + 01 + ), + CalendarYearEnd = DATEFROMPARTS( + YEAR({config.dim_date.columns.the_date.name}), + 12, + 31 + ), + CalendarQuarterStart = CAST( + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}), + 0 + ) AS date + ), + CalendarQuarterEnd = CAST( + DATEADD( + day, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}) + 1, + 0 + ) + ) AS date + ), + CalendarMonthStart = DATEFROMPARTS( + YEAR({config.dim_date.columns.the_date.name}), + MONTH({config.dim_date.columns.the_date.name}), + 01 + ), + CalendarMonthEnd = EOMONTH({config.dim_date.columns.the_date.name}), + CalendarWeekStart = DATEADD( + day, + 1-DATEPART( + WEEKDAY, + {config.dim_date.columns.the_date.name} + ), + {config.dim_date.columns.the_date.name} + ), + CalendarWeekEnd = DATEADD( + day, + 7-DATEPART( + WEEKDAY, + {config.dim_date.columns.the_date.name} + ), + {config.dim_date.columns.the_date.name} + ), + + CalendarYearStartToday = DATEFROMPARTS( + YEAR(@TodayInLocal), + 01, + 01 + ), + CalendarYearEndToday = DATEFROMPARTS( + YEAR(@TodayInLocal), + 12, + 31 + ), + CalendarQuarterStartToday = CAST( + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ) AS date + ), + CalendarQuarterEndToday = CAST( + DATEADD( + day, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal) + 1, + 0 + ) + ) AS date + ), + CalendarMonthStartToday = DATEFROMPARTS( + YEAR(@TodayInLocal), + MONTH(@TodayInLocal), + 01 + ), + CalendarMonthEndToday = EOMONTH(@TodayInLocal), + CalendarWeekStartToday = DATEADD( + day, + 1-DATEPART( + WEEKDAY, + @TodayInLocal + ), + @TodayInLocal + ), + CalendarWeekEndToday = DATEADD( + day, + 7-DATEPART( + WEEKDAY, + @TodayInLocal + ), + @TodayInLocal + ), + + -- This looks insane, but it's not too complicated. Examples: + -- For: + -- {config.dim_date.columns.the_date.name} = '2021-01-01'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2020-12-26' + -- For: + -- {config.dim_date.columns.the_date.name} = '2020-12-26'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2020-12-26' + -- For: + -- {config.dim_date.columns.the_date.name} = '2020-12-25'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2019-12-26' + -- + -- You can work out implementation details yourself if you'd like. + FiscalYearStart = IIF( + DATEDIFF( + day, + DATEFROMPARTS( + DATEPART(year, {config.dim_date.columns.the_date.name}), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + {config.dim_date.columns.the_date.name} + ) >= 0, + DATEFROMPARTS( + DATEPART(year, {config.dim_date.columns.the_date.name}), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + DATEFROMPARTS( + DATEPART(year, {config.dim_date.columns.the_date.name}) - 1, + @FiscalYearStartMonth, + @FiscalMonthStartDay + ) + ), + + FiscalYearStartToday = IIF( + DATEDIFF( + day, + DATEFROMPARTS( + DATEPART(year, @TodayInLocal), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + @TodayInLocal + ) >= 0, + DATEFROMPARTS( + DATEPART(year, @TodayInLocal), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + DATEFROMPARTS( + DATEPART(year, @TodayInLocal) - 1, + @FiscalYearStartMonth, + @FiscalMonthStartDay + ) + ) + FROM Recursion +), + +-- Much easier to reuse some values calculated in +-- BaseDates than to do it in BaseDates +BaseDatesSecond AS ( + SELECT + *, + FiscalYearEnd = DATEADD( + day, + -1, + DATEADD( + year, + 1, + FiscalYearStart + ) + ), + + FiscalYearEndToday = DATEADD( + day, + -1, + DATEADD( + year, + 1, + FiscalYearStartToday + ) + ) + FROM BaseDatesFirst +), + +BaseDatesThird AS ( + SELECT + *, + + FiscalPeriodYearReference = IIF( + @FiscalYearPeriodEndMatchesCalendar = 0, + FiscalYearStart, + FiscalYearEnd + ), + + FiscalPeriodYearReferenceToday = IIF( + @FiscalYearPeriodEndMatchesCalendar = 0, + FiscalYearStartToday, + FiscalYearEndToday + ), + + -- Same here. Here are some examples: + -- For: + -- {config.dim_date.columns.the_date.name} = '2021-01-01'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2020-12-26' + -- For: + -- {config.dim_date.columns.the_date.name} = '2020-12-26'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2020-12-26' + -- For: + -- {config.dim_date.columns.the_date.name} = '2020-12-25'; + -- @FiscalYearStartMonth = 12; + -- @FiscalMonthStartDay = 26; + -- Output: + -- '2020-11-26' + FiscalMonthStart = IIF( + DATEPART( + day, + {config.dim_date.columns.the_date.name} + ) >= @FiscalMonthStartDay, + DATEFROMPARTS( + YEAR({config.dim_date.columns.the_date.name}), + MONTH({config.dim_date.columns.the_date.name}), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + -1, + {config.dim_date.columns.the_date.name} + ) + ), + MONTH( + DATEADD( + month, + -1, + {config.dim_date.columns.the_date.name} + ) + ), + @FiscalMonthStartDay + ) + ), + + FiscalMonthStartToday = IIF( + DATEPART( + day, + @TodayInLocal + ) >= @FiscalMonthStartDay, + DATEFROMPARTS( + YEAR(@TodayInLocal), + MONTH(@TodayInLocal), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + -1, + @TodayInLocal + ) + ), + MONTH( + DATEADD( + month, + -1, + @TodayInLocal + ) + ), + @FiscalMonthStartDay + ) + ) + FROM + BaseDatesSecond +), + +BaseDatesFourth AS ( + SELECT + *, + + FiscalMonthEnd = DATEADD( + day, + -1, + DATEADD( + month, + 1, + FiscalMonthStart + ) + ), + + FiscalMonthEndToday = DATEADD( + day, + -1, + DATEADD( + month, + 1, + FiscalMonthStartToday + ) + ) + FROM + BaseDatesThird +), + +BaseDatesFifth AS ( + SELECT + *, + + FiscalPeriodMonthReference = IIF( + @FiscalMonthPeriodEndMatchesCalendar = 0, + FiscalMonthStart, + FiscalMonthEnd + ), + + FiscalPeriodMonthReferenceToday = IIF( + @FiscalMonthPeriodEndMatchesCalendar = 0, + FiscalMonthStartToday, + FiscalMonthEndToday + ), + + FiscalQuarterStart = IIF( + DATEPART( + quarter, + {config.dim_date.columns.the_date.name} + ) = DATEPART( + quarter, + FiscalMonthEnd + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}), + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}), + 0 + ) + ) + ), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}) + 1, + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}) + 1, + 0 + ) + ) + ), + @FiscalMonthStartDay + ) + ), + + FiscalQuarterStartToday = IIF( + DATEPART( + quarter, + @TodayInLocal + ) = DATEPART( + quarter, + FiscalMonthEndToday + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ) + ) + ), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal) + 1, + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + IIF(@FiscalMonthStartDay = 1, 0, -1), + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal) + 1, + 0 + ) + ) + ), + @FiscalMonthStartDay + ) + ) + FROM + BaseDatesFourth +), + +BaseDatesSixth AS ( + SELECT + *, + + FiscalQuarterEnd = DATEADD( + day, + -1, + DATEADD( + quarter, + 1, + FiscalQuarterStart + ) + ), + + FiscalQuarterEndToday = DATEADD( + day, + -1, + DATEADD( + quarter, + 1, + FiscalQuarterStartToday + ) + ) + FROM + BaseDatesFifth +), + +BaseDatesSeventh AS ( + SELECT + *, + + FiscalPeriodQuarterReference = IIF( + @FiscalQuarterPeriodEndMatchesCalendar = 0, + FiscalQuarterStart, + FiscalQuarterEnd + ), + + FiscalPeriodQuarterReferenceToday = IIF( + @FiscalQuarterPeriodEndMatchesCalendar = 0, + FiscalQuarterStartToday, + FiscalQuarterEndToday + ) + FROM + BaseDatesSixth +), + +-- Generate "Prior" and "Next" dates for all above created dates +RelativeDates AS ( + SELECT + *, + PriorCalendarYearStart = DATEADD( + year, + -1, + CalendarYearStart + ), + PriorCalendarYearEnd = DATEADD( + day, + -1, + CalendarYearStart + ), + PriorCalendarQuarterStart = DATEADD( + quarter, + -1, + CalendarQuarterStart + ), + PriorCalendarQuarterEnd = DATEADD( + day, + -1, + CalendarQuarterStart + ), + PriorCalendarMonthStart = DATEADD( + month, + -1, + CalendarMonthStart + ), + PriorCalendarMonthEnd = DATEADD( + day, + -1, + CalendarMonthStart + ), + PriorCalendarWeekStart = DATEADD( + week, + -1, + CalendarWeekStart + ), + PriorCalendarWeekEnd = DATEADD( + day, + -1, + CalendarWeekStart + ), + NextCalendarYearStart = DATEADD( + day, + 1, + CalendarYearEnd + ), + NextCalendarYearEnd = DATEFROMPARTS( + YEAR(CalendarYearStart) + 1, + 12, + 31 + ), + NextCalendarQuarterStart = DATEADD( + day, + 1, + CalendarQuarterEnd + ), + NextCalendarQuarterEnd = DATEADD( + day, + -1, + DATEADD( + quarter, + 2, + CalendarQuarterStart + ) + ), + NextCalendarMonthStart = DATEADD( + day, + 1, + CalendarMonthEnd + ), + NextCalendarMonthEnd = DATEADD( + day, + -1, + DATEADD( + month, + 2, + CalendarMonthStart + ) + ), + NextCalendarWeekStart = DATEADD( + week, + 1, + CalendarWeekStart + ), + NextCalendarWeekEnd = DATEADD( + week, + 1, + CalendarWeekEnd + ), + PriorFiscalYearStart = DATEADD( + year, + -1, + FiscalYearStart + ), + PriorFiscalYearEnd = DATEADD( + day, + -1, + FiscalYearStart + ), + PriorFiscalQuarterStart = DATEADD( + quarter, + -1, + FiscalQuarterStart + ), + PriorFiscalQuarterEnd = DATEADD( + day, + -1, + FiscalQuarterStart + ), + PriorFiscalMonthStart = DATEADD( + month, + -1, + FiscalMonthStart + ), + PriorFiscalMonthEnd = DATEADD( + day, + -1, + FiscalMonthStart + ), + NextFiscalYearStart = DATEADD( + day, + 1, + FiscalYearEnd + ), + -- We can do the below because fiscal month + -- start dates aren't allowed to be >28, meaning + -- it's impossible for the year end to be on a different + -- date depending on the number of days in the month + NextFiscalYearEnd = DATEADD( + year, + 1, + FiscalYearEnd + ), + NextFiscalQuarterStart = DATEADD( + day, + 1, + FiscalQuarterEnd + ), + NextFiscalQuarterEnd = DATEADD( + day, + -1, + DATEADD( + quarter, + 2, + FiscalQuarterStart + ) + ), + NextFiscalMonthStart = DATEADD( + day, + 1, + FiscalMonthEnd + ), + NextFiscalMonthEnd = DATEADD( + day, + -1, + DATEADD( + month, + 2, + FiscalMonthStart + ) + ) + FROM + BaseDatesSeventh +), + +-- Some things are just hard to calculate and I don't want to do it +-- repeatedly +FiscalHelpers AS ( + SELECT + *, + FiscalMonthNum = IIF( + MONTH(FiscalPeriodMonthReference) - MONTH(FiscalPeriodYearReference) - @FiscalMonthPeriodEndMatchesCalendar >= 0, + 1 + MONTH(FiscalPeriodMonthReference) - MONTH(FiscalPeriodYearReference) - @FiscalMonthPeriodEndMatchesCalendar, + 13 + MONTH(FiscalPeriodMonthReference) - MONTH(FiscalPeriodYearReference) - @FiscalMonthPeriodEndMatchesCalendar + ), + FiscalQuarterNum = IIF( + DATEPART(quarter, FiscalPeriodQuarterReference) - DATEPART(quarter, FiscalPeriodYearReference) - @FiscalQuarterPeriodEndMatchesCalendar >= 0, + 1 + DATEPART(quarter, FiscalPeriodQuarterReference) - DATEPART(quarter, FiscalPeriodYearReference) - @FiscalQuarterPeriodEndMatchesCalendar, + 5 + DATEPART(quarter, FiscalPeriodQuarterReference) - DATEPART(quarter, FiscalPeriodYearReference) - @FiscalQuarterPeriodEndMatchesCalendar + ), + FiscalYearNum = YEAR(FiscalPeriodYearReference) + FROM RelativeDates +), + +Main AS ( + SELECT + fh.*, + + -- '2021-01-01' + {config.dim_date.columns.iso_date_name.name} = CONVERT( + varchar(10), + {config.dim_date.columns.the_date.name}, + @ISO8601DatestringFormatNumber + ), + + -- '2020-W53-5' + -- This is gross, but it works for + -- every value I tested for (which was a lot). + -- The "{config.dim_date.columns.year.name}" part is from https://stackoverflow.com/questions/26926271/sql-get-iso-year-for-iso-week + ISOWeekDateName = CONCAT( + DATENAME( + year, + DATEADD( + day, + 26-DATEPART( + iso_week, + {config.dim_date.columns.the_date.name} + ), + {config.dim_date.columns.the_date.name} + ) + ), + '-W', + RIGHT( + '0'+CONVERT( + varchar(2), + DATEPART( + iso_week, + {config.dim_date.columns.the_date.name} + ) + ), + 2 + ), + '-', + CONVERT( + varchar(1), + ( + DATEPART( + weekday, + {config.dim_date.columns.the_date.name} + ) + @@DATEFIRST + 6 - 1 + ) % 7 + 1 + ) + ), + + -- '01/01/2021' + {config.dim_date.columns.american_date_name.name} = CONVERT( + varchar(10), + {config.dim_date.columns.the_date.name}, + @USDatestringFormatNumber + ), + + -- 'Friday' + {config.dim_date.columns.day_of_week_name.name} = DATENAME( + weekday, + {config.dim_date.columns.the_date.name} + ), + + -- 'Fri' + {config.dim_date.columns.day_of_week_abbrev.name} = LEFT( + DATENAME( + weekday, + {config.dim_date.columns.the_date.name} + ), + 3 + ), + + -- 'January' + {config.dim_date.columns.month_name.name} = DATENAME( + month, + {config.dim_date.columns.the_date.name} + ), + + -- 'Jan' + {config.dim_date.columns.month_abbrev.name} = LEFT( + DATENAME( + month, + {config.dim_date.columns.the_date.name} + ), + 3 + ), + + -- '2021W01' + {config.dim_date.columns.year_week_name.name} = CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + 'W', + RIGHT( + '0'+DATENAME( + week, + {config.dim_date.columns.the_date.name} + ), + 2 + ) + ), + + -- '2021-01' + {config.dim_date.columns.year_month_name.name} = CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + '-', + RIGHT( + '0'+CONVERT( + varchar(2), + DATEPART( + month, + {config.dim_date.columns.the_date.name} + ) + ), + 2 + ) + ), + + -- 'Jan 2021' + {config.dim_date.columns.month_year_name.name} = CONCAT( + LEFT( + DATENAME( + month, + {config.dim_date.columns.the_date.name} + ), + 3 + ), + ' ', + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ) + ), + + -- '2021Q1' + {config.dim_date.columns.year_quarter_name.name} = CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + 'Q', + DATENAME( + quarter, + {config.dim_date.columns.the_date.name} + ) + ), + + -- 2021 + {config.dim_date.columns.year.name} = DATEPART(year, {config.dim_date.columns.the_date.name}), + + -- 202101 + {config.dim_date.columns.year_week.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + RIGHT( + '0'+DATENAME( + week, + {config.dim_date.columns.the_date.name} + ), + 2 + ) + ) + ), + + -- 202101 + {config.dim_date.columns.iso_year_week_code.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + DATEADD( + day, + 26-DATEPART( + iso_week, + {config.dim_date.columns.the_date.name} + ), + {config.dim_date.columns.the_date.name} + ) + ), + RIGHT( + '0'+CONVERT( + varchar(2), + DATEPART( + iso_week, + {config.dim_date.columns.the_date.name} + ) + ), + 2 + ) + ) + ), + + -- 202101 + {config.dim_date.columns.year_month.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + RIGHT( + '0'+CONVERT( + varchar(2), + DATEPART( + month, + {config.dim_date.columns.the_date.name} + ) + ), + 2 + ) + ) + ), + + -- 202101 + {config.dim_date.columns.year_quarter.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + {config.dim_date.columns.the_date.name} + ), + RIGHT( + '0'+DATENAME( + quarter, + {config.dim_date.columns.the_date.name} + ), + 2 + ) + ) + ), + + -- 5 + {config.dim_date.columns.day_of_week_starting_monday.name} = ( + DATEPART( + weekday, + {config.dim_date.columns.the_date.name} + ) + @@DATEFIRST + 6 - 1 + ) % 7 + 1, + + -- 6 + {config.dim_date.columns.day_of_week.name} = DATEPART( + weekday, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.day_of_month.name} = DATEPART( + day, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.day_of_quarter.name} = DATEDIFF( + day, + DATEADD( + quarter, + DATEDIFF(quarter, 0, {config.dim_date.columns.the_date.name}), + 0 + ), + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 1 + {config.dim_date.columns.day_of_year.name} = DATEPART( + dayofyear, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.week_of_quarter.name} = DATEDIFF( + week, + CalendarQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 1 + {config.dim_date.columns.week_of_year.name} = DATEPART( + week, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.iso_week_of_year.name} = DATEPART( + iso_week, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.month.name} = DATEPART( + month, + {config.dim_date.columns.the_date.name} + ), + + -- 1 + {config.dim_date.columns.month_of_quarter.name} = DATEDIFF( + month, + CalendarQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 1 + {config.dim_date.columns.quarter.name} = DATEPART( + quarter, + {config.dim_date.columns.the_date.name} + ), + + -- 31 + {config.dim_date.columns.days_in_month.name} = DATEPART( + day, + EOMONTH({config.dim_date.columns.the_date.name}) + ), + + -- 90 + {config.dim_date.columns.days_in_quarter.name} = DATEDIFF( + day, + CalendarQuarterStart, + NextCalendarQuarterStart + ), + + -- 365 + {config.dim_date.columns.days_in_year.name} = DATEDIFF( + day, + CalendarYearStart, + NextCalendarYearStart + ), + + -- -209 + {config.dim_date.columns.day_offset_from_today.name} = DATEDIFF( + day, + @TodayInLocal, + {config.dim_date.columns.the_date.name} + ), + + -- -6 + {config.dim_date.columns.month_offset_from_today.name} = DATEDIFF( + month, + @TodayInLocal, + {config.dim_date.columns.the_date.name} + ), + + -- -2 + {config.dim_date.columns.quarter_offset_from_today.name} = DATEDIFF( + quarter, + @TodayInLocal, + {config.dim_date.columns.the_date.name} + ), + + -- 0 + {config.dim_date.columns.year_offset_from_today.name} = DATEDIFF( + year, + @TodayInLocal, + {config.dim_date.columns.the_date.name} + ), + + -- 0 + {config.dim_date.columns.today_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = @TodayInLocal, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.current_week_starting_monday_flag.name} = IIF( + DATEDIFF( + week, + CONVERT(date, GETDATE()), + DATEADD(day, -1, {config.dim_date.columns.the_date.name}) + ) = 0, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.current_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 0, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.prior_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, {config.dim_date.columns.the_date.name}) = -1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.next_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.current_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 0, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.prior_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, {config.dim_date.columns.the_date.name}) = -1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.next_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.current_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 0, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.prior_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, {config.dim_date.columns.the_date.name}) = -1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.next_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 1, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.current_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 0, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.prior_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, {config.dim_date.columns.the_date.name}) = -1, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.next_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, {config.dim_date.columns.the_date.name}) = 1, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.weekday_flag.name} = IIF( + DATEPART(weekday, {config.dim_date.columns.the_date.name}) NOT IN (1,7), + 1, + 0 + ), + + {config.dim_date.columns.business_day_flag.name} = IIF( + {business_day_clause if len(business_day_list) > 0 else ''}DATEPART( + weekday, + {config.dim_date.columns.the_date.name} + ) IN (1, 7), + 0, + 1 + ), + + -- 1 + {config.dim_date.columns.first_day_of_month_flag.name} = IIF( + CalendarMonthStart = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.last_day_of_month_flag.name} = IIF( + CalendarMonthEnd = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.first_day_of_quarter_flag.name} = IIF( + CalendarQuarterStart = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.last_day_of_quarter_flag.name} = IIF( + CalendarQuarterEnd = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.first_day_of_year_flag.name} = IIF( + CalendarYearStart = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.last_day_of_year_flag.name} = IIF( + CalendarYearEnd = {config.dim_date.columns.the_date.name}, + 1, + 0 + ), + + -- 0.8571 + -- The fraction of the week, counted from Sunday to Saturday, that has passed as of + -- 2021-01-01. In this case, 6 (Friday) / 7 (the total number of days in the week) + {config.dim_date.columns.fraction_of_week.name} = CAST( + DATEPART( + weekday, + {config.dim_date.columns.the_date.name} + ) + AS decimal(8,4) + ) / 7, + + -- 0.0323 + -- The fraction of the month, counted from the first day of the calendar month to the last + -- that has passed as of 2021-01-01. + {config.dim_date.columns.fraction_of_month.name} = CAST( + DATEPART( + day, + {config.dim_date.columns.the_date.name} + ) + AS decimal(8,4) + ) / DATEPART( + day, + CalendarMonthEnd + ), + + -- 0.0111 + -- The fraction of the quarter, counted from the first day of the calendar quarter to the last + -- that has passed as of 2021-01-01. + {config.dim_date.columns.fraction_of_quarter.name} = CAST( + DATEDIFF( + day, + CalendarQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1 + AS decimal(8,4) + ) / (DATEDIFF( + day, + CalendarQuarterStart, + CalendarQuarterEnd + ) + 1), + + -- 0.0027 + -- The fraction of the year, counted from the first day of the calendar year to the last + -- that has passed as of 2021-01-01. + {config.dim_date.columns.fraction_of_year.name} = CAST( + DATEPART( + dayofyear, + {config.dim_date.columns.the_date.name} + ) + AS decimal(8,4) + ) / (DATEDIFF( + day, + CalendarYearStart, + CalendarYearEnd + ) + 1), + + -- 2020-12-31 + {config.dim_date.columns.prior_day.name} = DATEADD( + day, + -1, + {config.dim_date.columns.the_date.name} + ), + + -- 2021-01-02 + {config.dim_date.columns.next_day.name} = DATEADD( + day, + 1, + {config.dim_date.columns.the_date.name} + ), + + -- 2020-12-25 + {config.dim_date.columns.same_day_prior_week.name} = DATEADD( + week, + -1, + {config.dim_date.columns.the_date.name} + ), + + -- 2020-12-01 + {config.dim_date.columns.same_day_prior_month.name} = DATEADD( + month, + -1, + {config.dim_date.columns.the_date.name} + ), + + -- 2020-10-01 + {config.dim_date.columns.same_day_prior_quarter.name} = DATEADD( + quarter, + -1, + {config.dim_date.columns.the_date.name} + ), + + -- 2020-01-01 + {config.dim_date.columns.same_day_prior_year.name} = DATEADD( + year, + -1, + {config.dim_date.columns.the_date.name} + ), + + -- 2021-01-08 + {config.dim_date.columns.same_day_next_week.name} = DATEADD( + week, + 1, + {config.dim_date.columns.the_date.name} + ), + + -- 2021-02-01 + {config.dim_date.columns.same_day_next_month.name} = DATEADD( + month, + 1, + {config.dim_date.columns.the_date.name} + ), + + -- 2021-04-01 + {config.dim_date.columns.same_day_next_quarter.name} = DATEADD( + quarter, + 1, + {config.dim_date.columns.the_date.name} + ), + + -- 2022-01-01 + {config.dim_date.columns.same_day_next_year.name} = DATEADD( + year, + 1, + {config.dim_date.columns.the_date.name} + ), + + -- 2020-12-27 (week start is Sunday) + {config.dim_date.columns.current_week_start.name} = CalendarWeekStart, + + -- 2021-01-02 (week end is Saturday) + {config.dim_date.columns.current_week_end.name} = CalendarWeekEnd, + + -- 2021-01-01 + {config.dim_date.columns.current_month_start.name} = CalendarMonthStart, + + -- 2021-01-31 (does take into account leap years) + {config.dim_date.columns.current_month_end.name} = CalendarMonthEnd, + + -- 2021-01-01 + {config.dim_date.columns.current_quarter_start.name} = CalendarQuarterStart, + + -- 2021-03-31 + {config.dim_date.columns.current_quarter_end.name} = CalendarQuarterEnd, + + -- 2021-01-01 + {config.dim_date.columns.current_year_start.name} = CalendarYearStart, + + -- 2021-12-31 + {config.dim_date.columns.current_year_end.name} = CalendarYearEnd, + + -- 2020-12-20 + {config.dim_date.columns.prior_week_start.name} = PriorCalendarWeekStart, + + -- 2020-12-26 + {config.dim_date.columns.prior_week_end.name} = PriorCalendarWeekEnd, + + -- 2020-12-01 + {config.dim_date.columns.prior_month_start.name} = PriorCalendarMonthStart, + + -- 2020-12-31 + {config.dim_date.columns.prior_month_end.name} = PriorCalendarMonthEnd, + + -- 2020-10-01 + {config.dim_date.columns.prior_quarter_start.name} = PriorCalendarQuarterStart, + + -- 2020-12-31 + {config.dim_date.columns.prior_quarter_end.name} = PriorCalendarQuarterEnd, + + -- 2020-01-01 + {config.dim_date.columns.prior_year_start.name} = PriorCalendarYearStart, + + -- 2020-12-31 + {config.dim_date.columns.prior_year_end.name} = PriorCalendarYearEnd, + + -- 2021-01-03 + {config.dim_date.columns.next_week_start.name} = NextCalendarWeekStart, + + -- 2021-01-09 + {config.dim_date.columns.next_week_end.name} = NextCalendarWeekEnd, + + -- 2021-02-01 + {config.dim_date.columns.next_month_start.name} = NextCalendarMonthStart, + + -- 2021-02-28 (handles leap years) + {config.dim_date.columns.next_month_end.name} = NextCalendarMonthEnd, + + -- 2021-04-01 + {config.dim_date.columns.next_quarter_start.name} = NextCalendarQuarterStart, + + -- 2021-06-30 + {config.dim_date.columns.next_quarter_end.name} = NextCalendarQuarterEnd, + + -- 2022-01-01 + {config.dim_date.columns.next_year_start.name} = NextCalendarYearStart, + + -- 2022-12-31 + {config.dim_date.columns.next_year_end.name} = NextCalendarYearEnd, + + -- Hell starts here. + -- Let me use a couple of examples. With our current settings, on Jan. 1: + -- {config.dim_date.columns.year.name} = 2021 (because the end of the fiscal year falls into CY2021) + -- {config.dim_date.columns.month.name} = January (name), 01 (number) + -- (because the end of the fiscal month, 01-26, falls into January) + -- Here's the catch: + -- {config.dim_date.columns.quarter.name} and month numbers are always based off of the start of your fiscal year + -- So if your fiscal year starts July 15, July 15-August 14 will always have a month + -- number of 1 (because it's the first month in your fiscal year), but the fiscal + -- month NAME will depend on @@FiscalMonthPeriodEndMatchesCalendar. If it's set to 0, + -- the name would be July. If it's set to 1, it would be August. + + -- 'January' + {config.dim_date.columns.fiscal_month_name.name} = DATENAME( + month, + FiscalPeriodMonthReference + ), + + -- 'Jan' + {config.dim_date.columns.fiscal_month_abbrev.name} = LEFT( + DATENAME( + month, + FiscalPeriodMonthReference + ), + 3 + ), + + -- '2021W02' + {config.dim_date.columns.fiscal_year_week_name.name} = CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + 'W', + RIGHT( + '0'+ + CONVERT( + varchar(2), + DATEDIFF( + week, + FiscalYearStart, + {config.dim_date.columns.the_date.name} + ) + 1 + ), + 2 + ) + ), + + -- '2021-01' + {config.dim_date.columns.fiscal_year_month_name.name} = CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + '-', + RIGHT( + '0'+CONVERT( + varchar(2), + FiscalMonthNum + ), + 2 + ) + ), + + -- 'Jan 2021' + {config.dim_date.columns.fiscal_month_year_name.name} = CONCAT( + LEFT( + DATENAME( + month, + FiscalPeriodMonthReference + ), + 3 + ), + ' ', + DATENAME( + year, + FiscalPeriodYearReference + ) + ), + + -- '2021Q1' + {config.dim_date.columns.fiscal_year_quarter_name.name} = CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + 'Q', + CONVERT( + varchar(2), + FiscalQuarterNum + ) + ), + + -- 2021 + {config.dim_date.columns.fiscal_year.name} = FiscalYearNum, + + -- 202102 + {config.dim_date.columns.fiscal_year_week.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + RIGHT( + '0'+ + CONVERT( + varchar(2), + DATEDIFF( + week, + FiscalYearStart, + {config.dim_date.columns.the_date.name} + ) + 1 + ), + 2 + ) + ) + ), + + -- 202101 + {config.dim_date.columns.fiscal_year_month.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + RIGHT( + '0'+CONVERT( + varchar(2), + FiscalMonthNum + ), + 2 + ) + ) + ), + + -- 202101 + {config.dim_date.columns.fiscal_year_quarter.name} = CONVERT( + int, + CONCAT( + DATENAME( + year, + FiscalPeriodYearReference + ), + RIGHT( + '0'+CONVERT( + varchar(2), + FiscalQuarterNum + ), + 2 + ) + ) + ), + + -- 7 + {config.dim_date.columns.fiscal_day_of_month.name} = DATEDIFF( + day, + FiscalMonthStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 7 + {config.dim_date.columns.fiscal_day_of_quarter.name} = DATEDIFF( + day, + FiscalQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 7 + {config.dim_date.columns.fiscal_day_of_year.name} = DATEDIFF( + day, + FiscalYearStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 2 + {config.dim_date.columns.fiscal_week_of_quarter.name} = DATEDIFF( + week, + FiscalQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 2 + {config.dim_date.columns.fiscal_week_of_year.name} = DATEDIFF( + week, + FiscalYearStart, + {config.dim_date.columns.the_date.name} + ) + 1, + + -- 1 + {config.dim_date.columns.fiscal_month.name} = FiscalMonthNum, + + -- 1 + {config.dim_date.columns.fiscal_month_of_quarter.name} = DATEDIFF( + month, + FiscalQuarterStart, + {config.dim_date.columns.the_date.name} + ) + IIF( + DATEPART( + day, + {config.dim_date.columns.the_date.name} + ) >= @FiscalMonthStartDay, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.fiscal_quarter.name} = FiscalQuarterNum, + + -- 31 + {config.dim_date.columns.fiscal_days_in_month.name} = DATEDIFF( + day, + FiscalMonthStart, + FiscalMonthEnd + ) + 1, + + -- 90 + {config.dim_date.columns.fiscal_days_in_quarter.name} = DATEDIFF( + day, + FiscalQuarterStart, + FiscalQuarterEnd + ) + 1, + + -- 365 + {config.dim_date.columns.fiscal_days_in_year.name} = DATEDIFF( + day, + FiscalYearStart, + FiscalYearEnd + ) + 1, + + -- 0 + {config.dim_date.columns.fiscal_current_month_flag.name} = IIF( + @TodayInLocal BETWEEN FiscalMonthStart AND FiscalMonthEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_prior_month_flag.name} = IIF( + @TodayInLocal BETWEEN + NextFiscalMonthStart AND NextFiscalMonthEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_next_month_flag.name} = IIF( + @TodayInLocal BETWEEN + PriorFiscalMonthStart AND PriorFiscalMonthEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_current_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN FiscalQuarterStart AND FiscalQuarterEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_prior_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN + NextFiscalQuarterStart AND NextFiscalQuarterEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_next_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN + PriorFiscalQuarterStart AND PriorFiscalQuarterEnd, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.fiscal_current_year_flag.name} = IIF( + @TodayInLocal BETWEEN FiscalYearStart AND FiscalYearEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_prior_year_flag.name} = IIF( + @TodayInLocal BETWEEN + NextFiscalYearStart AND NextFiscalYearEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_next_year_flag.name} = IIF( + @TodayInLocal BETWEEN + PriorFiscalYearStart AND PriorFiscalYearEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_first_day_of_month_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalMonthStart, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_last_day_of_month_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalMonthEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_first_day_of_quarter_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalQuarterStart, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_last_day_of_quarter_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalQuarterEnd, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_first_day_of_year_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalYearStart, + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_last_day_of_year_flag.name} = IIF( + {config.dim_date.columns.the_date.name} = FiscalYearEnd, + 1, + 0 + ), + + -- 0.2258 + {config.dim_date.columns.fiscal_fraction_of_month.name} = CAST( + DATEDIFF( + day, + FiscalMonthStart, + {config.dim_date.columns.the_date.name} + ) + 1 AS decimal(8,4) + ) / (DATEDIFF( + day, + FiscalMonthStart, + FiscalMonthEnd + ) + 1), + + -- 0.0778 + {config.dim_date.columns.fiscal_fraction_of_quarter.name} = CAST( + DATEDIFF( + day, + FiscalQuarterStart, + {config.dim_date.columns.the_date.name} + ) + 1 AS decimal(8,4) + ) / (DATEDIFF( + day, + FiscalQuarterStart, + FiscalQuarterEnd + ) + 1), + + -- 0.0192 + {config.dim_date.columns.fiscal_fraction_of_year.name} = CAST( + DATEDIFF( + day, + FiscalYearStart, + {config.dim_date.columns.the_date.name} + ) + 1 AS decimal(8,4) + ) / (DATEDIFF( + day, + FiscalYearStart, + FiscalYearEnd + ) + 1), + + -- 2020-12-26 + {config.dim_date.columns.fiscal_current_month_start.name} = FiscalMonthStart, + + -- 2021-01-25 + {config.dim_date.columns.fiscal_current_month_end.name} = FiscalMonthEnd, + + -- 2020-12-26 + {config.dim_date.columns.fiscal_current_quarter_start.name} = FiscalQuarterStart, + + -- 2021-03-25 + {config.dim_date.columns.fiscal_current_quarter_end.name} = FiscalQuarterEnd, + + -- 2020-12-26 + {config.dim_date.columns.fiscal_current_year_start.name} = FiscalYearStart, + + -- 2021-12-25 + {config.dim_date.columns.fiscal_current_year_end.name} = FiscalYearEnd, + + -- 2020-11-26 + {config.dim_date.columns.fiscal_prior_month_start.name} = PriorFiscalMonthStart, + + -- 2020-12-25 + {config.dim_date.columns.fiscal_prior_month_end.name} = PriorFiscalMonthEnd, + + -- 2020-09-26 + {config.dim_date.columns.fiscal_prior_quarter_start.name} = PriorFiscalQuarterStart, + + -- 2020-12-25 + {config.dim_date.columns.fiscal_prior_quarter_end.name} = PriorFiscalQuarterEnd, + + -- 2019-12-26 + {config.dim_date.columns.fiscal_prior_year_start.name} = PriorFiscalYearStart, + + -- 2020-12-25 + {config.dim_date.columns.fiscal_prior_year_end.name} = PriorFiscalYearEnd, + + -- 2021-01-26 + {config.dim_date.columns.fiscal_next_month_start.name} = NextFiscalMonthStart, + + -- 2021-02-25 + {config.dim_date.columns.fiscal_next_month_end.name} = NextFiscalMonthEnd, + + -- 2021-03-26 + {config.dim_date.columns.fiscal_next_quarter_start.name} = NextFiscalQuarterStart, + + -- 2021-06-25 + {config.dim_date.columns.fiscal_next_quarter_end.name} = NextFiscalQuarterEnd, + + -- 2021-12-26 + {config.dim_date.columns.fiscal_next_year_start.name} = NextFiscalYearStart, + + -- 2022-12-25 + {config.dim_date.columns.fiscal_next_year_end.name} = NextFiscalYearEnd{holiday_column_clause} + FROM + FiscalHelpers AS fh +{holiday_join} +), + +Burnups AS ( + SELECT + *, + + -- 1 - This is useful for dashboards. You can check 1 and see + -- week-to-date for every week simultaneously. Same for mon/qtr/year. + {config.dim_date.columns.weekly_burnup_starting_monday.name} = IIF( + {config.dim_date.columns.day_of_week_starting_monday.name} <= ( + DATEPART( + weekday, + @TodayInLocal + ) + @@DATEFIRST + 6 - 1 + ) % 7 + 1, + 1, + 0 + ), + + {config.dim_date.columns.weekly_burnup.name} = IIF( + {config.dim_date.columns.day_of_week.name} <= DATEPART( + weekday, + @TodayInLocal + ), + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.monthly_burnup.name} = IIF( + {config.dim_date.columns.day_of_month.name} <= DATEPART( + day, + @TodayInLocal + ), + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.quarterly_burnup.name} = IIF( + {config.dim_date.columns.day_of_quarter.name} <= DATEDIFF( + day, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ), + @TodayInLocal + ) + 1, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.yearly_burnup.name} = IIF( + {config.dim_date.columns.day_of_year.name} <= DATEPART( + dayofyear, + @TodayInLocal + ), + 1, + 0 + ), + + -- 0 + {config.dim_date.columns.fiscal_monthly_burnup.name} = IIF( + {config.dim_date.columns.fiscal_day_of_month.name} <= DATEDIFF( + day, + FiscalMonthStartToday, + @TodayInLocal + ) + 1, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.fiscal_quarterly_burnup.name} = IIF( + {config.dim_date.columns.fiscal_day_of_quarter.name} <= DATEDIFF( + day, + FiscalQuarterStartToday, + @TodayInLocal + ) + 1, + 1, + 0 + ), + + -- 1 + {config.dim_date.columns.fiscal_yearly_burnup.name} = IIF( + {config.dim_date.columns.fiscal_day_of_year.name} <= DATEDIFF( + day, + FiscalYearStartToday, + @TodayInLocal + ) + 1, + 1, + 0 + ) + FROM Main +) + +INSERT INTO {config.dim_date.table_schema}.{config.dim_date.table_name} ( + {insert_column_clause} +) +SELECT + {insert_column_clause} +FROM Burnups +ORDER BY {config.dim_date.columns.date_key.name} ASC +OPTION (MAXRECURSION 0) +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_date_refresh_template.py b/awesome_date_dimension/_internal/tsql/dim_date_refresh_template.py new file mode 100644 index 0000000..fa6c484 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_date_refresh_template.py @@ -0,0 +1,501 @@ +from ...config import Config +from .tsql_columns import TSQLDimDateColumns + + +def dim_date_refresh_template(config: Config, columns: TSQLDimDateColumns) -> str: + dd_conf = config.dim_date + dd_cols = dd_conf.columns + + holiday_columndef: list[str] = [] + holiday_column_list: list[str] = [] + business_day_list: list[str] = [] + holiday_join: list[str] = [] + if config.holidays.generate_holidays: + for i, t in enumerate(config.holidays.holiday_types): + holiday_join.append( + f" LEFT OUTER JOIN {config.holidays.holidays_schema_name}.{config.holidays.holidays_table_name} AS h{i} -- {t.name}" + ) + holiday_join.append( + f" ON d.{dd_cols.date_key.name} = h{i}.{config.holidays.holidays_columns.date_key.name} AND h{i}.{config.holidays.holidays_columns.holiday_type_key.name} = (SELECT {config.holidays.holiday_types_columns.holiday_type_key.name} FROM {config.holidays.holiday_types_schema_name}.{config.holidays.holiday_types_table_name} WHERE {config.holidays.holiday_types_columns.holiday_type_name.name} = '{t.name}')" + ) + if t.included_in_business_day_calc: + business_day_list.append( + f"h{i}.{config.holidays.holidays_columns.date_key.name} IS NOT NULL" + ) + holiday_columndef.append( + f" {t.generated_column_prefix}{t.generated_flag_column_postfix} = IIF(" + ) + holiday_columndef.append( + f" h{i}.{config.holidays.holidays_columns.date_key.name} IS NOT NULL," + ) + holiday_columndef.append(f" 1,") + holiday_columndef.append(f" 0") + holiday_columndef.append(f" ),") + holiday_columndef.append( + f" {t.generated_column_prefix}{t.generated_name_column_postfix} = h{i}.{config.holidays.holidays_columns.holiday_name.name}," + ) + holiday_column_list.append( + f" d.{t.generated_column_prefix}{t.generated_flag_column_postfix} = dc.{t.generated_column_prefix}{t.generated_flag_column_postfix}," + ) + holiday_column_list.append( + f" d.{t.generated_column_prefix}{t.generated_name_column_postfix} = dc.{t.generated_column_prefix}{t.generated_name_column_postfix}," + ) + + holiday_join = "\n".join(holiday_join) + business_day_clause = "\n".join(business_day_list) + "\n OR " + holiday_column_clause = ",\n" + "\n".join(holiday_columndef)[:-1] + holiday_columns = ",\n" + "\n".join(holiday_column_list)[:-1] + else: + holiday_join = "" + business_day_clause = "" + holiday_column_clause = "" + holiday_columns = "" + + updatable_column_set = { + dd_cols.day_offset_from_today.name, + dd_cols.month_offset_from_today.name, + dd_cols.quarter_offset_from_today.name, + dd_cols.year_offset_from_today.name, + dd_cols.today_flag.name, + dd_cols.current_week_starting_monday_flag.name, + dd_cols.current_week_flag.name, + dd_cols.prior_week_flag.name, + dd_cols.next_week_flag.name, + dd_cols.current_month_flag.name, + dd_cols.prior_month_flag.name, + dd_cols.next_month_flag.name, + dd_cols.current_quarter_flag.name, + dd_cols.prior_quarter_flag.name, + dd_cols.next_quarter_flag.name, + dd_cols.current_year_flag.name, + dd_cols.prior_year_flag.name, + dd_cols.next_year_flag.name, + dd_cols.weekly_burnup_starting_monday.name, + dd_cols.weekly_burnup.name, + dd_cols.monthly_burnup.name, + dd_cols.quarterly_burnup.name, + dd_cols.yearly_burnup.name, + dd_cols.fiscal_current_month_flag.name, + dd_cols.fiscal_prior_month_flag.name, + dd_cols.fiscal_next_month_flag.name, + dd_cols.fiscal_current_quarter_flag.name, + dd_cols.fiscal_prior_quarter_flag.name, + dd_cols.fiscal_next_quarter_flag.name, + dd_cols.fiscal_current_year_flag.name, + dd_cols.fiscal_prior_year_flag.name, + dd_cols.fiscal_next_year_flag.name, + dd_cols.fiscal_monthly_burnup.name, + dd_cols.fiscal_quarterly_burnup.name, + dd_cols.fiscal_yearly_burnup.name, + } + column_update_clause = ",\n ".join( + (f"d.{c.name} = dc.{c.name}" for c in columns if c.name in updatable_column_set) + ) + return f"""CREATE PROCEDURE {dd_conf.table_schema}.sp_build_{dd_conf.table_name} AS BEGIN + DECLARE @TodayInLocal date; + DECLARE @FiscalMonthStartDay int; + DECLARE @FiscalYearStartMonth int; + + SET @FiscalMonthStartDay={config.fiscal.month_start_day}; -- Cannot be >28 or you'll blow up + SET @FiscalYearStartMonth={config.fiscal.year_start_month}; + + SET @TodayInLocal = CONVERT( + date, + -- You need to figure out what your local TZ is called in your server host's registry. + -- See https://docs.microsoft.com/en-us/sql/t-sql/queries/at-time-zone-transact-sql?view=sql-server-ver15 + -- for more info. + -- For example, on my machine, for MST, the following line would be: + -- GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE 'Mountain Standard Time' + GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE IntentionallyCrashScriptIfItTriesToRun + ); + + WITH RelativeToToday AS ( + SELECT + FiscalYearStartToday = IIF( + DATEDIFF( + day, + DATEFROMPARTS( + DATEPART(year, @TodayInLocal), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + @TodayInLocal + ) >= 0, + DATEFROMPARTS( + DATEPART(year, @TodayInLocal), + @FiscalYearStartMonth, + @FiscalMonthStartDay + ), + DATEFROMPARTS( + DATEPART(year, @TodayInLocal) - 1, + @FiscalYearStartMonth, + @FiscalMonthStartDay + ) + ), + + FiscalMonthStartToday = IIF( + DATEPART( + day, + @TodayInLocal + ) >= @FiscalMonthStartDay, + DATEFROMPARTS( + YEAR(@TodayInLocal), + MONTH(@TodayInLocal), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + -1, + @TodayInLocal + ) + ), + MONTH( + DATEADD( + month, + -1, + @TodayInLocal + ) + ), + @FiscalMonthStartDay + ) + ) + ), + + RelativeToTodayQuarter AS ( + SELECT + *, + FiscalQuarterStartToday = IIF( + DATEPART( + quarter, + @TodayInLocal + ) = DATEPART( + quarter, + DATEADD( + day, + -1, + DATEADD( + month, + 1, + FiscalMonthStartToday + ) + ) + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ) + ) + ), + @FiscalMonthStartDay + ), + DATEFROMPARTS( + YEAR( + DATEADD( + month, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal) + 1, + 0 + ) + ) + ), + MONTH( + DATEADD( + month, + -1, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal) + 1, + 0 + ) + ) + ), + @FiscalMonthStartDay + ) + ) + FROM + RelativeToToday + ), + + BurnupsAsOfToday AS ( + SELECT + DayOfWeekToday = DATEPART( + weekday, + @TodayInLocal + ), + DayOfMonthToday = DATEPART( + day, + @TodayInLocal + ), + DayOfQuarterToday = DATEDIFF( + day, + DATEADD( + quarter, + DATEDIFF(quarter, 0, @TodayInLocal), + 0 + ), + @TodayInLocal + ) + 1, + DayOfYearToday = DATEPART( + dayofyear, + @TodayInLocal + ), + FiscalDayOfMonthToday = DATEDIFF( + day, + FiscalMonthStartToday, + @TodayInLocal + ) + 1, + FiscalDayOfQuarterToday = DATEDIFF( + day, + FiscalQuarterStartToday, + @TodayInLocal + ) + 1, + FiscalDayOfYearToday = DATEDIFF( + day, + FiscalYearStartToday, + @TodayInLocal + ) + 1 + FROM + RelativeToTodayQuarter + ), + + DateCalculations AS ( + SELECT + d.{dd_cols.date_key.name}, + {dd_cols.day_offset_from_today.name} = DATEDIFF( + day, + @TodayInLocal, + d.{dd_cols.the_date.name} + ), + {dd_cols.month_offset_from_today.name} = DATEDIFF( + month, + @TodayInLocal, + d.{dd_cols.the_date.name} + ), + {dd_cols.quarter_offset_from_today.name} = DATEDIFF( + quarter, + @TodayInLocal, + d.{dd_cols.the_date.name} + ), + {dd_cols.year_offset_from_today.name} = DATEDIFF( + year, + @TodayInLocal, + d.{dd_cols.the_date.name} + ), + {dd_cols.today_flag.name} = IIF( + d.{dd_cols.the_date.name} = @TodayInLocal, + 1, + 0 + ), + {dd_cols.current_week_starting_monday_flag.name} = IIF( + DATEDIFF( + week, + CONVERT(date, GETDATE()), + DATEADD(day, -1, d.{dd_cols.the_date.name}) + ) = 0, + 1, + 0 + ), + {dd_cols.current_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, d.{dd_cols.the_date.name}) = 0, + 1, + 0 + ), + {dd_cols.prior_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, d.{dd_cols.the_date.name}) = -1, + 1, + 0 + ), + {dd_cols.next_week_flag.name} = IIF( + DATEDIFF(week, @TodayInLocal, d.{dd_cols.the_date.name}) = 1, + 1, + 0 + ), + {dd_cols.current_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, d.{dd_cols.the_date.name}) = 0, + 1, + 0 + ), + {dd_cols.prior_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, d.{dd_cols.the_date.name}) = -1, + 1, + 0 + ), + {dd_cols.next_month_flag.name} = IIF( + DATEDIFF(month, @TodayInLocal, d.{dd_cols.the_date.name}) = 1, + 1, + 0 + ), + {dd_cols.current_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, d.{dd_cols.the_date.name}) = 0, + 1, + 0 + ), + {dd_cols.prior_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, d.{dd_cols.the_date.name}) = -1, + 1, + 0 + ), + {dd_cols.next_quarter_flag.name} = IIF( + DATEDIFF(quarter, @TodayInLocal, d.{dd_cols.the_date.name}) = 1, + 1, + 0 + ), + {dd_cols.current_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, d.{dd_cols.the_date.name}) = 0, + 1, + 0 + ), + {dd_cols.prior_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, d.{dd_cols.the_date.name}) = -1, + 1, + 0 + ), + {dd_cols.next_year_flag.name} = IIF( + DATEDIFF(year, @TodayInLocal, d.{dd_cols.the_date.name}) = 1, + 1, + 0 + ), + {dd_cols.weekly_burnup_starting_monday.name} = IIF( + DayOfWeekStartingMonday <= ( + DATEPART( + weekday, + @TodayInLocal + ) + @@DATEFIRST + 6 - 1 + ) % 7 + 1, + 1, + 0 + ), + {dd_cols.weekly_burnup.name} = IIF( + d.{dd_cols.day_of_week.name} <= r.DayOfWeekToday, + 1, + 0 + ), + {dd_cols.monthly_burnup.name} = IIF( + d.{dd_cols.day_of_month.name} <= r.DayOfMonthToday, + 1, + 0 + ), + {dd_cols.quarterly_burnup.name} = IIF( + d.{dd_cols.day_of_quarter.name} <= r.DayOfQuarterToday, + 1, + 0 + ), + {dd_cols.yearly_burnup.name} = IIF( + d.{dd_cols.day_of_year.name} <= r.DayOfYearToday, + 1, + 0 + ), + {dd_cols.fiscal_current_month_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_current_month_start.name} AND d.{dd_cols.fiscal_current_month_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_prior_month_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_next_month_start.name} AND d.{dd_cols.fiscal_next_month_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_next_month_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_prior_month_start.name} AND d.{dd_cols.fiscal_prior_month_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_current_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_current_quarter_start.name} AND d.{dd_cols.fiscal_current_quarter_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_prior_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_next_quarter_start.name} AND d.{dd_cols.fiscal_next_quarter_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_next_quarter_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_prior_quarter_start.name} AND d.{dd_cols.fiscal_prior_quarter_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_current_year_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_current_year_start.name} AND d.{dd_cols.fiscal_current_year_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_prior_year_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_next_year_start.name} AND d.{dd_cols.fiscal_next_year_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_next_year_flag.name} = IIF( + @TodayInLocal BETWEEN + d.{dd_cols.fiscal_prior_year_start.name} AND d.{dd_cols.fiscal_prior_year_end.name}, + 1, + 0 + ), + {dd_cols.fiscal_monthly_burnup.name} = IIF( + d.{dd_cols.fiscal_day_of_month.name} <= r.FiscalDayOfMonthToday, + 1, + 0 + ), + {dd_cols.fiscal_quarterly_burnup.name} = IIF( + d.{dd_cols.fiscal_day_of_quarter.name} <= r.FiscalDayOfQuarterToday, + 1, + 0 + ), + {dd_cols.fiscal_yearly_burnup.name} = IIF( + d.{dd_cols.fiscal_day_of_year.name} <= r.FiscalDayOfyearToday, + 1, + 0 + ), + BusinessDayFlag = IIF( + {business_day_clause if len(business_day_list) > 0 else ''}DATEPART( + weekday, + d.{dd_cols.the_date.name} + ) IN (1, 7), + 0, + 1 + ){holiday_column_clause} + FROM + {dd_conf.table_schema}.{dd_conf.table_name} AS d + CROSS JOIN BurnupsAsOfToday AS r +{holiday_join} + ) + + UPDATE d + SET + {column_update_clause}{holiday_columns} + FROM + {dd_conf.table_schema}.{dd_conf.table_name} AS d + INNER JOIN DateCalculations AS dc + ON d.{dd_cols.date_key.name} = dc.{dd_cols.date_key.name} +END +GO +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_fiscal_month_constraints_template.py b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_constraints_template.py new file mode 100644 index 0000000..e827671 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_constraints_template.py @@ -0,0 +1,11 @@ +from ...config import Config + + +def dim_fiscal_month_constraints_template(config: Config) -> str: + dfm_conf = config.dim_fiscal_month + return f"""ALTER TABLE {dfm_conf.table_schema}.{dfm_conf.table_name} +ADD PRIMARY KEY CLUSTERED ({dfm_conf.columns.month_start_key.name}, {dfm_conf.columns.month_end_key.name} ASC); + +CREATE NONCLUSTERED INDEX IDX_NC_{dfm_conf.table_schema}_{dfm_conf.table_name}_{dfm_conf.columns.month_start_date.name} ON {dfm_conf.table_schema}.{dfm_conf.table_name} ({dfm_conf.columns.month_start_date.name}); +CREATE NONCLUSTERED INDEX IDX_NC_{dfm_conf.table_schema}_{dfm_conf.table_name}_{dfm_conf.columns.month_end_date.name} ON {dfm_conf.table_schema}.{dfm_conf.table_name} ({dfm_conf.columns.month_end_date.name}); +""" diff --git a/awesome_date_dimension/_internal/tsql/dim_fiscal_month_insert_template.py b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_insert_template.py new file mode 100644 index 0000000..51bab62 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_insert_template.py @@ -0,0 +1,148 @@ +from ...config import Config +from .tsql_columns import TSQLDimFiscalMonthColumns + + +def dim_fiscal_month_insert_template( + config: Config, columns: TSQLDimFiscalMonthColumns +) -> str: + dfm_conf = config.dim_fiscal_month + dfm_cols = dfm_conf.columns + dd_conf = config.dim_date + dd_cols = dd_conf.columns + h_conf = config.holidays + holiday_columndef: list[str] = [] + holiday_colselect: list[str] = [] + if h_conf.generate_holidays: + for i, t in enumerate(h_conf.holiday_types): + holiday_columndef.append( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix} = SUM({t.generated_column_prefix}{t.generated_flag_column_postfix} * 1)" + ) + holiday_colselect.append( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix}" + ) + + holiday_columndef_str = ",\n ".join(holiday_columndef) + else: + holiday_columndef_str = "" + + dfm_to_dd_colmap = { + dfm_cols.month_start_date.name: f"startdate.{dd_cols.the_date.name}", + dfm_cols.month_end_date.name: f"enddate.{dd_cols.the_date.name}", + dfm_cols.month_start_iso_date_name.name: f"startdate.{dd_cols.iso_date_name.name}", + dfm_cols.month_end_iso_date_name.name: f"enddate.{dd_cols.iso_date_name.name}", + dfm_cols.month_start_iso_week_date_name.name: f"startdate.{dd_cols.iso_week_date_name.name}", + dfm_cols.month_end_iso_week_date_name.name: f"enddate.{dd_cols.iso_week_date_name.name}", + dfm_cols.month_start_american_date_name.name: f"startdate.{dd_cols.american_date_name.name}", + dfm_cols.month_end_american_date_name.name: f"enddate.{dd_cols.american_date_name.name}", + dfm_cols.month_name.name: f"startdate.{dd_cols.fiscal_month_name.name}", + dfm_cols.month_abbrev.name: f"startdate.{dd_cols.fiscal_month_abbrev.name}", + dfm_cols.month_start_year_week_name.name: f"startdate.{dd_cols.fiscal_year_week_name.name}", + dfm_cols.month_end_year_week_name.name: f"enddate.{dd_cols.fiscal_year_week_name.name}", + dfm_cols.year_month_name.name: f"startdate.{dd_cols.fiscal_year_month_name.name}", + dfm_cols.month_year_name.name: f"startdate.{dd_cols.fiscal_month_year_name.name}", + dfm_cols.year_quarter_name.name: f"startdate.{dd_cols.fiscal_year_quarter_name.name}", + dfm_cols.year.name: f"startdate.{dd_cols.fiscal_year.name}", + dfm_cols.month_start_year_week.name: f"startdate.{dd_cols.fiscal_year_week.name}", + dfm_cols.month_end_year_week.name: f"enddate.{dd_cols.fiscal_year_week.name}", + dfm_cols.year_month.name: f"startdate.{dd_cols.fiscal_year_month.name}", + dfm_cols.year_quarter.name: f"startdate.{dd_cols.fiscal_year_quarter.name}", + dfm_cols.month_start_day_of_quarter.name: f"startdate.{dd_cols.fiscal_day_of_quarter.name}", + dfm_cols.month_end_day_of_quarter.name: f"enddate.{dd_cols.fiscal_day_of_quarter.name}", + dfm_cols.month_start_day_of_year.name: f"startdate.{dd_cols.fiscal_day_of_year.name}", + dfm_cols.month_end_day_of_year.name: f"enddate.{dd_cols.fiscal_day_of_year.name}", + dfm_cols.month_start_week_of_quarter.name: f"startdate.{dd_cols.fiscal_week_of_quarter.name}", + dfm_cols.month_end_week_of_quarter.name: f"enddate.{dd_cols.fiscal_week_of_quarter.name}", + dfm_cols.month_start_week_of_year.name: f"startdate.{dd_cols.fiscal_week_of_year.name}", + dfm_cols.month_end_week_of_year.name: f"enddate.{dd_cols.fiscal_week_of_year.name}", + dfm_cols.month_of_quarter.name: f"startdate.{dd_cols.fiscal_month_of_quarter.name}", + dfm_cols.quarter.name: f"startdate.{dd_cols.fiscal_quarter.name}", + dfm_cols.days_in_month.name: f"startdate.{dd_cols.fiscal_days_in_month.name}", + dfm_cols.days_in_quarter.name: f"startdate.{dd_cols.fiscal_days_in_quarter.name}", + dfm_cols.days_in_year.name: f"startdate.{dd_cols.fiscal_days_in_year.name}", + dfm_cols.current_month_flag.name: f"startdate.{dd_cols.fiscal_current_month_flag.name}", + dfm_cols.prior_month_flag.name: f"startdate.{dd_cols.fiscal_prior_month_flag.name}", + dfm_cols.next_month_flag.name: f"startdate.{dd_cols.fiscal_next_month_flag.name}", + dfm_cols.current_quarter_flag.name: f"startdate.{dd_cols.fiscal_current_quarter_flag.name}", + dfm_cols.prior_quarter_flag.name: f"startdate.{dd_cols.fiscal_prior_quarter_flag.name}", + dfm_cols.next_quarter_flag.name: f"startdate.{dd_cols.fiscal_next_quarter_flag.name}", + dfm_cols.current_year_flag.name: f"startdate.{dd_cols.fiscal_current_year_flag.name}", + dfm_cols.prior_year_flag.name: f"startdate.{dd_cols.fiscal_prior_year_flag.name}", + dfm_cols.next_year_flag.name: f"startdate.{dd_cols.fiscal_next_year_flag.name}", + dfm_cols.first_day_of_month_flag.name: f"startdate.{dd_cols.fiscal_first_day_of_month_flag.name}", + dfm_cols.last_day_of_month_flag.name: f"startdate.{dd_cols.fiscal_last_day_of_month_flag.name}", + dfm_cols.first_day_of_quarter_flag.name: f"startdate.{dd_cols.fiscal_first_day_of_quarter_flag.name}", + dfm_cols.last_day_of_quarter_flag.name: f"startdate.{dd_cols.fiscal_last_day_of_quarter_flag.name}", + dfm_cols.first_day_of_year_flag.name: f"startdate.{dd_cols.fiscal_first_day_of_year_flag.name}", + dfm_cols.last_day_of_year_flag.name: f"startdate.{dd_cols.fiscal_last_day_of_year_flag.name}", + dfm_cols.month_start_fraction_of_quarter.name: f"startdate.{dd_cols.fiscal_fraction_of_quarter.name}", + dfm_cols.month_end_fraction_of_quarter.name: f"enddate.{dd_cols.fiscal_fraction_of_quarter.name}", + dfm_cols.month_start_fraction_of_year.name: f"startdate.{dd_cols.fiscal_fraction_of_year.name}", + dfm_cols.month_end_fraction_of_year.name: f"enddate.{dd_cols.fiscal_fraction_of_year.name}", + dfm_cols.current_quarter_start.name: f"startdate.{dd_cols.fiscal_current_quarter_start.name}", + dfm_cols.current_quarter_end.name: f"startdate.{dd_cols.fiscal_current_quarter_end.name}", + dfm_cols.current_year_start.name: f"startdate.{dd_cols.fiscal_current_year_start.name}", + dfm_cols.current_year_end.name: f"startdate.{dd_cols.fiscal_current_year_end.name}", + dfm_cols.prior_month_start.name: f"startdate.{dd_cols.fiscal_prior_month_start.name}", + dfm_cols.prior_month_end.name: f"startdate.{dd_cols.fiscal_prior_month_end.name}", + dfm_cols.prior_quarter_start.name: f"startdate.{dd_cols.fiscal_prior_quarter_start.name}", + dfm_cols.prior_quarter_end.name: f"startdate.{dd_cols.fiscal_prior_quarter_end.name}", + dfm_cols.prior_year_start.name: f"startdate.{dd_cols.fiscal_prior_year_start.name}", + dfm_cols.prior_year_end.name: f"startdate.{dd_cols.fiscal_prior_year_end.name}", + dfm_cols.next_month_start.name: f"startdate.{dd_cols.fiscal_next_month_start.name}", + dfm_cols.next_month_end.name: f"startdate.{dd_cols.fiscal_next_month_end.name}", + dfm_cols.next_quarter_start.name: f"startdate.{dd_cols.fiscal_next_quarter_start.name}", + dfm_cols.next_quarter_end.name: f"startdate.{dd_cols.fiscal_next_quarter_end.name}", + dfm_cols.next_year_start.name: f"startdate.{dd_cols.fiscal_next_year_start.name}", + dfm_cols.next_year_end.name: f"startdate.{dd_cols.fiscal_next_year_end.name}", + dfm_cols.month_start_quarterly_burnup.name: f"startdate.{dd_cols.fiscal_quarterly_burnup.name}", + dfm_cols.month_end_quarterly_burnup.name: f"enddate.{dd_cols.fiscal_quarterly_burnup.name}", + dfm_cols.month_start_yearly_burnup.name: f"startdate.{dd_cols.fiscal_yearly_burnup.name}", + dfm_cols.month_end_yearly_burnup.name: f"enddate.{dd_cols.fiscal_yearly_burnup.name}", + } + + insert_columns_clause = ",\n ".join((c.name for c in columns)) + select_columns = [] + for col in columns: + if (dd_name := dfm_to_dd_colmap.get(col.name)) is not None: + select_columns.append(f"{col.name} = {dd_name}") + else: + select_columns.append(f"{col.name} = base.{col.name}") + + select_columns_clause = ",\n ".join(select_columns) + + return f"""WITH DistinctMonths AS ( + SELECT + {dfm_cols.month_start_key.name} = CONVERT( + int, + CONVERT( + varchar(8), + {dd_cols.fiscal_current_month_start.name}, + 112 + ) + ), + {dfm_cols.month_end_key.name} = CONVERT( + int, + CONVERT( + varchar(8), + {dd_cols.fiscal_current_month_end.name}, + 112 + ) + ), + {holiday_columndef_str} + FROM + {dd_conf.table_schema}.{dd_conf.table_name} + GROUP BY {dd_cols.fiscal_current_month_start.name}, {dd_cols.fiscal_current_month_end.name} +) + +INSERT INTO {dfm_conf.table_schema}.{dfm_conf.table_name} ( + {insert_columns_clause} +) +-- Yank the day-level stuff we need for both the start and end dates from {dd_conf.table_name} +SELECT + {select_columns_clause} +FROM + DistinctMonths AS base + INNER JOIN {dd_conf.table_schema}.{dd_conf.table_name} AS startdate + ON base.{dfm_cols.month_start_key.name} = startdate.{dd_cols.date_key.name} + INNER JOIN {dd_conf.table_schema}.{dd_conf.table_name} AS enddate + ON base.{dfm_cols.month_end_key.name} = enddate.{dd_cols.date_key.name};""" diff --git a/awesome_date_dimension/_internal/tsql/dim_fiscal_month_refresh_template.py b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_refresh_template.py new file mode 100644 index 0000000..9d82183 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/dim_fiscal_month_refresh_template.py @@ -0,0 +1,39 @@ +from ...config import Config +from .dim_fiscal_month_insert_template import dim_fiscal_month_insert_template +from .tsql_columns import TSQLDimFiscalMonthColumns + + +def dim_fiscal_month_refresh_template( + config: Config, columns: TSQLDimFiscalMonthColumns +) -> str: + indentation_level = " " + insert_script = dim_fiscal_month_insert_template(config, columns) + indented_script = "\n".join( + map(lambda line: indentation_level + line, insert_script.split("\n")) + ) + return f"""CREATE PROCEDURE dbo.sp_build_DimFiscalMonth AS BEGIN + SET XACT_ABORT ON; + BEGIN TRY + BEGIN TRANSACTION; + + TRUNCATE TABLE dbo.DimFiscalMonth; + +{indented_script} + + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + SELECT + ERROR_NUMBER() AS ErrorNumber, + ERROR_SEVERITY() AS ErrorSeverity, + ERROR_STATE() AS ErrorState, + ERROR_LINE () AS ErrorLine, + ERROR_PROCEDURE() AS ErrorProcedure, + ERROR_MESSAGE() AS ErrorMessage; + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH; +END +GO +""" diff --git a/awesome_date_dimension/_internal/tsql/holiday_types_constraints_template.py b/awesome_date_dimension/_internal/tsql/holiday_types_constraints_template.py new file mode 100644 index 0000000..bffab40 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/holiday_types_constraints_template.py @@ -0,0 +1,8 @@ +from ...config import Config + + +def holiday_types_constraints_template(config: Config) -> str: + h_conf = config.holidays + return f"""ALTER TABLE {h_conf.holiday_types_schema_name}.{h_conf.holiday_types_table_name} +ADD PRIMARY KEY CLUSTERED ({h_conf.holiday_types_columns.holiday_type_key.name} ASC); +""" diff --git a/awesome_date_dimension/_internal/tsql/holiday_types_insert_template.py b/awesome_date_dimension/_internal/tsql/holiday_types_insert_template.py new file mode 100644 index 0000000..6abaec4 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/holiday_types_insert_template.py @@ -0,0 +1,12 @@ +from ...config import Config + + +def holiday_types_insert_template(config: Config) -> str: + table_schema = config.holidays.holiday_types_schema_name + table_name = config.holidays.holiday_types_table_name + ht_column_name = config.holidays.holiday_types_columns.holiday_type_name.name + holiday_types = config.holidays.holiday_types + formatted_types = map(lambda t: f"('{t.name}')", holiday_types) + join_str = ",\n " + return f"""INSERT INTO {table_schema}.{table_name} ({ht_column_name}) VALUES + {join_str.join(formatted_types)};""" diff --git a/awesome_date_dimension/_internal/tsql/holidays_constraints_template.py b/awesome_date_dimension/_internal/tsql/holidays_constraints_template.py new file mode 100644 index 0000000..276b043 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/holidays_constraints_template.py @@ -0,0 +1,14 @@ +from ...config import Config + + +def holidays_constraints_template(config: Config) -> str: + h_conf = config.holidays + return f"""ALTER TABLE {h_conf.holidays_schema_name}.{h_conf.holidays_table_name} +ADD PRIMARY KEY CLUSTERED ({h_conf.holidays_columns.date_key.name} ASC, {h_conf.holidays_columns.holiday_type_key.name} ASC); + +ALTER TABLE {h_conf.holidays_schema_name}.{h_conf.holidays_table_name} +ADD CONSTRAINT FK_{h_conf.holidays_table_name}_{h_conf.holidays_columns.holiday_type_key.name} FOREIGN KEY ({h_conf.holidays_columns.holiday_type_key.name}) + REFERENCES {h_conf.holiday_types_schema_name}.{h_conf.holiday_types_table_name} ({h_conf.holiday_types_columns.holiday_type_key.name}) + ON DELETE CASCADE + ON UPDATE CASCADE; +""" diff --git a/awesome_date_dimension/_internal/tsql/holidays_insert_template.py b/awesome_date_dimension/_internal/tsql/holidays_insert_template.py new file mode 100644 index 0000000..e83ef98 --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/holidays_insert_template.py @@ -0,0 +1,23 @@ +from ...config import Config + + +def holidays_insert_template(config: Config): + holiday_config = config.holidays + join_str = ",\n " + holidays = [] + for cal in holiday_config.holiday_calendars: + for holiday in cal.holidays: + date_key = f"{str(holiday.holiday_date.year).zfill(4)}{str(holiday.holiday_date.month).zfill(2)}{str(holiday.holiday_date.day).zfill(2)}" + holidays.append( + f"({date_key}, '{holiday.holiday_name}', '{cal.holiday_type.name}')" + ) + return f"""INSERT INTO {holiday_config.holidays_schema_name}.{holiday_config.holidays_table_name} ({holiday_config.holidays_columns.date_key.name}, {holiday_config.holidays_columns.holiday_name.name}, {holiday_config.holidays_columns.holiday_type_key.name}) + SELECT h.{holiday_config.holidays_columns.date_key.name}, h.{holiday_config.holidays_columns.holiday_name.name}, ht.{holiday_config.holiday_types_columns.holiday_type_key.name} + FROM ( + VALUES + {join_str.join(holidays)} + ) AS h ({holiday_config.holidays_columns.date_key.name}, {holiday_config.holidays_columns.holiday_name.name}, {holiday_config.holiday_types_columns.holiday_type_name.name}) + -- Note the inner join -- if you haven't correctly specified your HolidayTypes, they'll be thrown out. + INNER JOIN {holiday_config.holiday_types_schema_name}.{holiday_config.holiday_types_table_name} AS ht + ON h.{holiday_config.holiday_types_columns.holiday_type_name.name} = ht.{holiday_config.holiday_types_columns.holiday_type_name.name} +""" diff --git a/awesome_date_dimension/_internal/tsql/table_setup_template.py b/awesome_date_dimension/_internal/tsql/table_setup_template.py new file mode 100644 index 0000000..42085fd --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/table_setup_template.py @@ -0,0 +1,8 @@ +def table_setup_template( + table_schema: str, table_name: str, column_def: list[str] +) -> str: + join_str = ",\n " + return f"""CREATE TABLE {table_schema}.{table_name} ( + {join_str.join(column_def)} +); +""" diff --git a/awesome_date_dimension/_internal/tsql/tsql_columns.py b/awesome_date_dimension/_internal/tsql/tsql_columns.py new file mode 100644 index 0000000..3cebc6a --- /dev/null +++ b/awesome_date_dimension/_internal/tsql/tsql_columns.py @@ -0,0 +1,545 @@ +from dataclasses import asdict, dataclass + +from ...config import ( + Column, + Config, + DimCalendarMonthColumns, + DimDateColumns, + DimFiscalMonthColumns, + HolidayConfig, +) + + +@dataclass +class TSQLColumn: + name: str + include: bool + sort_index: int + sql_datatype: str + nullable: bool + constraint: str = None + + @classmethod + def from_column( + cls, sql_datatype: str, nullable: bool, column: Column, constraint: str = None + ): + return cls( + sql_datatype=sql_datatype, + nullable=nullable, + constraint=constraint, + **asdict(column), + ) + + +class TSQLDimDateColumns: + def __init__(self, columns: DimDateColumns): + self._columns: list[TSQLColumn] = [ + TSQLColumn.from_column("int", False, columns.date_key), + TSQLColumn.from_column("date", False, columns.the_date), + TSQLColumn.from_column("varchar(10)", False, columns.iso_date_name), + TSQLColumn.from_column("varchar(10)", False, columns.iso_week_date_name), + TSQLColumn.from_column("varchar(10)", False, columns.american_date_name), + TSQLColumn.from_column("varchar(9)", False, columns.day_of_week_name), + TSQLColumn.from_column("varchar(3)", False, columns.day_of_week_abbrev), + TSQLColumn.from_column("varchar(9)", False, columns.month_name), + TSQLColumn.from_column("varchar(3)", False, columns.month_abbrev), + TSQLColumn.from_column("varchar(8)", False, columns.year_week_name), + TSQLColumn.from_column("varchar(7)", False, columns.year_month_name), + TSQLColumn.from_column("varchar(8)", False, columns.month_year_name), + TSQLColumn.from_column("varchar(6)", False, columns.year_quarter_name), + TSQLColumn.from_column("int", False, columns.year), + TSQLColumn.from_column("int", False, columns.year_week), + TSQLColumn.from_column("int", False, columns.iso_year_week_code), + TSQLColumn.from_column("int", False, columns.year_month), + TSQLColumn.from_column("int", False, columns.year_quarter), + TSQLColumn.from_column("int", False, columns.day_of_week_starting_monday), + TSQLColumn.from_column("int", False, columns.day_of_week), + TSQLColumn.from_column("int", False, columns.day_of_month), + TSQLColumn.from_column("int", False, columns.day_of_quarter), + TSQLColumn.from_column("int", False, columns.day_of_year), + TSQLColumn.from_column("int", False, columns.week_of_quarter), + TSQLColumn.from_column("int", False, columns.week_of_year), + TSQLColumn.from_column("int", False, columns.iso_week_of_year), + TSQLColumn.from_column("int", False, columns.month), + TSQLColumn.from_column("int", False, columns.month_of_quarter), + TSQLColumn.from_column("int", False, columns.quarter), + TSQLColumn.from_column("int", False, columns.days_in_month), + TSQLColumn.from_column("int", False, columns.days_in_quarter), + TSQLColumn.from_column("int", False, columns.days_in_year), + TSQLColumn.from_column("int", False, columns.day_offset_from_today), + TSQLColumn.from_column("int", False, columns.month_offset_from_today), + TSQLColumn.from_column("int", False, columns.quarter_offset_from_today), + TSQLColumn.from_column("int", False, columns.year_offset_from_today), + TSQLColumn.from_column("bit", False, columns.today_flag), + TSQLColumn.from_column( + "bit", False, columns.current_week_starting_monday_flag + ), + TSQLColumn.from_column("bit", False, columns.current_week_flag), + TSQLColumn.from_column("bit", False, columns.prior_week_flag), + TSQLColumn.from_column("bit", False, columns.next_week_flag), + TSQLColumn.from_column("bit", False, columns.current_month_flag), + TSQLColumn.from_column("bit", False, columns.prior_month_flag), + TSQLColumn.from_column("bit", False, columns.next_month_flag), + TSQLColumn.from_column("bit", False, columns.current_quarter_flag), + TSQLColumn.from_column("bit", False, columns.prior_quarter_flag), + TSQLColumn.from_column("bit", False, columns.next_quarter_flag), + TSQLColumn.from_column("bit", False, columns.current_year_flag), + TSQLColumn.from_column("bit", False, columns.prior_year_flag), + TSQLColumn.from_column("bit", False, columns.next_year_flag), + TSQLColumn.from_column("bit", False, columns.weekday_flag), + TSQLColumn.from_column("bit", False, columns.business_day_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_year_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_year_flag), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fraction_of_week, + f"chk_DimDate_{columns.fraction_of_week.name} CHECK ({columns.fraction_of_week.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fraction_of_month, + f"chk_DimDate_{columns.fraction_of_month.name} CHECK ({columns.fraction_of_month.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fraction_of_quarter, + f"chk_DimDate_{columns.fraction_of_quarter.name} CHECK ({columns.fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fraction_of_year, + f"chk_DimDate_{columns.fraction_of_year.name} CHECK ({columns.fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column("date", False, columns.prior_day), + TSQLColumn.from_column("date", False, columns.next_day), + TSQLColumn.from_column("date", False, columns.same_day_prior_week), + TSQLColumn.from_column("date", False, columns.same_day_prior_month), + TSQLColumn.from_column("date", False, columns.same_day_prior_quarter), + TSQLColumn.from_column("date", False, columns.same_day_prior_year), + TSQLColumn.from_column("date", False, columns.same_day_next_week), + TSQLColumn.from_column("date", False, columns.same_day_next_month), + TSQLColumn.from_column("date", False, columns.same_day_next_quarter), + TSQLColumn.from_column("date", False, columns.same_day_next_year), + TSQLColumn.from_column("date", False, columns.current_week_start), + TSQLColumn.from_column("date", False, columns.current_week_end), + TSQLColumn.from_column("date", False, columns.current_month_start), + TSQLColumn.from_column("date", False, columns.current_month_end), + TSQLColumn.from_column("date", False, columns.current_quarter_start), + TSQLColumn.from_column("date", False, columns.current_quarter_end), + TSQLColumn.from_column("date", False, columns.current_year_start), + TSQLColumn.from_column("date", False, columns.current_year_end), + TSQLColumn.from_column("date", False, columns.prior_week_start), + TSQLColumn.from_column("date", False, columns.prior_week_end), + TSQLColumn.from_column("date", False, columns.prior_month_start), + TSQLColumn.from_column("date", False, columns.prior_month_end), + TSQLColumn.from_column("date", False, columns.prior_quarter_start), + TSQLColumn.from_column("date", False, columns.prior_quarter_end), + TSQLColumn.from_column("date", False, columns.prior_year_start), + TSQLColumn.from_column("date", False, columns.prior_year_end), + TSQLColumn.from_column("date", False, columns.next_week_start), + TSQLColumn.from_column("date", False, columns.next_week_end), + TSQLColumn.from_column("date", False, columns.next_month_start), + TSQLColumn.from_column("date", False, columns.next_month_end), + TSQLColumn.from_column("date", False, columns.next_quarter_start), + TSQLColumn.from_column("date", False, columns.next_quarter_end), + TSQLColumn.from_column("date", False, columns.next_year_start), + TSQLColumn.from_column("date", False, columns.next_year_end), + TSQLColumn.from_column("bit", False, columns.weekly_burnup_starting_monday), + TSQLColumn.from_column("bit", False, columns.weekly_burnup), + TSQLColumn.from_column("bit", False, columns.monthly_burnup), + TSQLColumn.from_column("bit", False, columns.quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.yearly_burnup), + TSQLColumn.from_column("varchar(9)", False, columns.fiscal_month_name), + TSQLColumn.from_column("varchar(3)", False, columns.fiscal_month_abbrev), + TSQLColumn.from_column("varchar(8)", False, columns.fiscal_year_week_name), + TSQLColumn.from_column("varchar(7)", False, columns.fiscal_year_month_name), + TSQLColumn.from_column("varchar(8)", False, columns.fiscal_month_year_name), + TSQLColumn.from_column( + "varchar(6)", False, columns.fiscal_year_quarter_name + ), + TSQLColumn.from_column("int", False, columns.fiscal_year), + TSQLColumn.from_column("int", False, columns.fiscal_year_week), + TSQLColumn.from_column("int", False, columns.fiscal_year_month), + TSQLColumn.from_column("int", False, columns.fiscal_year_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_day_of_month), + TSQLColumn.from_column("int", False, columns.fiscal_day_of_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_day_of_year), + TSQLColumn.from_column("int", False, columns.fiscal_week_of_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_week_of_year), + TSQLColumn.from_column("int", False, columns.fiscal_month), + TSQLColumn.from_column("int", False, columns.fiscal_month_of_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_days_in_month), + TSQLColumn.from_column("int", False, columns.fiscal_days_in_quarter), + TSQLColumn.from_column("int", False, columns.fiscal_days_in_year), + TSQLColumn.from_column("bit", False, columns.fiscal_current_month_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_prior_month_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_next_month_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_current_quarter_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_prior_quarter_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_next_quarter_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_current_year_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_prior_year_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_next_year_flag), + TSQLColumn.from_column( + "bit", False, columns.fiscal_first_day_of_month_flag + ), + TSQLColumn.from_column("bit", False, columns.fiscal_last_day_of_month_flag), + TSQLColumn.from_column( + "bit", False, columns.fiscal_first_day_of_quarter_flag + ), + TSQLColumn.from_column( + "bit", False, columns.fiscal_last_day_of_quarter_flag + ), + TSQLColumn.from_column("bit", False, columns.fiscal_first_day_of_year_flag), + TSQLColumn.from_column("bit", False, columns.fiscal_last_day_of_year_flag), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fiscal_fraction_of_month, + f"chk_DimDate_{columns.fiscal_fraction_of_month.name} CHECK ({columns.fiscal_fraction_of_month.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fiscal_fraction_of_quarter, + f"chk_DimDate_{columns.fiscal_fraction_of_quarter.name} CHECK ({columns.fiscal_fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.fiscal_fraction_of_year, + f"chk_DimDate_{columns.fiscal_fraction_of_year.name} CHECK ({columns.fiscal_fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column("date", False, columns.fiscal_current_month_start), + TSQLColumn.from_column("date", False, columns.fiscal_current_month_end), + TSQLColumn.from_column("date", False, columns.fiscal_current_quarter_start), + TSQLColumn.from_column("date", False, columns.fiscal_current_quarter_end), + TSQLColumn.from_column("date", False, columns.fiscal_current_year_start), + TSQLColumn.from_column("date", False, columns.fiscal_current_year_end), + TSQLColumn.from_column("date", False, columns.fiscal_prior_month_start), + TSQLColumn.from_column("date", False, columns.fiscal_prior_month_end), + TSQLColumn.from_column("date", False, columns.fiscal_prior_quarter_start), + TSQLColumn.from_column("date", False, columns.fiscal_prior_quarter_end), + TSQLColumn.from_column("date", False, columns.fiscal_prior_year_start), + TSQLColumn.from_column("date", False, columns.fiscal_prior_year_end), + TSQLColumn.from_column("date", False, columns.fiscal_next_month_start), + TSQLColumn.from_column("date", False, columns.fiscal_next_month_end), + TSQLColumn.from_column("date", False, columns.fiscal_next_quarter_start), + TSQLColumn.from_column("date", False, columns.fiscal_next_quarter_end), + TSQLColumn.from_column("date", False, columns.fiscal_next_year_start), + TSQLColumn.from_column("date", False, columns.fiscal_next_year_end), + TSQLColumn.from_column("bit", False, columns.fiscal_monthly_burnup), + TSQLColumn.from_column("bit", False, columns.fiscal_quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.fiscal_yearly_burnup), + ] + self._columns = list(filter(lambda c: c.include, self._columns)) + self._columns.sort(key=lambda c: c.sort_index) + + def __iter__(self): + return iter(self._columns) + + def add_holiday_columns(self, holiday_config: HolidayConfig): + idx = self._columns[-1].sort_index + 1 + if holiday_config.generate_holidays: + for t in holiday_config.holiday_types: + self._columns.append( + TSQLColumn( + f"{t.generated_column_prefix}{t.generated_flag_column_postfix}", + True, + idx, + "bit", + False, + ) + ) + idx += 1 + self._columns.append( + TSQLColumn( + f"{t.generated_column_prefix}{t.generated_name_column_postfix}", + True, + idx, + "varchar(255)", + True, + ) + ) + idx += 1 + + +class TSQLDimFiscalMonthColumns: + def __init__(self, columns: DimFiscalMonthColumns): + self._columns: list[TSQLColumn] = [ + TSQLColumn.from_column("int", False, columns.month_start_key), + TSQLColumn.from_column("int", False, columns.month_end_key), + TSQLColumn.from_column("date", False, columns.month_start_date), + TSQLColumn.from_column("date", False, columns.month_end_date), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_iso_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_iso_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_iso_week_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_iso_week_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_american_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_american_date_name + ), + TSQLColumn.from_column("varchar(9)", False, columns.month_name), + TSQLColumn.from_column("varchar(3)", False, columns.month_abbrev), + TSQLColumn.from_column( + "varchar(8)", False, columns.month_start_year_week_name + ), + TSQLColumn.from_column( + "varchar(8)", False, columns.month_end_year_week_name + ), + TSQLColumn.from_column("varchar(7)", False, columns.year_month_name), + TSQLColumn.from_column("varchar(8)", False, columns.month_year_name), + TSQLColumn.from_column("varchar(6)", False, columns.year_quarter_name), + TSQLColumn.from_column("int", False, columns.year), + TSQLColumn.from_column("int", False, columns.month_start_year_week), + TSQLColumn.from_column("int", False, columns.month_end_year_week), + TSQLColumn.from_column("int", False, columns.year_month), + TSQLColumn.from_column("int", False, columns.year_quarter), + TSQLColumn.from_column("int", False, columns.month_start_day_of_quarter), + TSQLColumn.from_column("int", False, columns.month_end_day_of_quarter), + TSQLColumn.from_column("int", False, columns.month_start_day_of_year), + TSQLColumn.from_column("int", False, columns.month_end_day_of_year), + TSQLColumn.from_column("int", False, columns.month_start_week_of_quarter), + TSQLColumn.from_column("int", False, columns.month_end_week_of_quarter), + TSQLColumn.from_column("int", False, columns.month_start_week_of_year), + TSQLColumn.from_column("int", False, columns.month_end_week_of_year), + TSQLColumn.from_column("int", False, columns.month_of_quarter), + TSQLColumn.from_column("int", False, columns.quarter), + TSQLColumn.from_column("int", False, columns.days_in_month), + TSQLColumn.from_column("int", False, columns.days_in_quarter), + TSQLColumn.from_column("int", False, columns.days_in_year), + TSQLColumn.from_column("bit", False, columns.current_month_flag), + TSQLColumn.from_column("bit", False, columns.prior_month_flag), + TSQLColumn.from_column("bit", False, columns.next_month_flag), + TSQLColumn.from_column("bit", False, columns.current_quarter_flag), + TSQLColumn.from_column("bit", False, columns.prior_quarter_flag), + TSQLColumn.from_column("bit", False, columns.next_quarter_flag), + TSQLColumn.from_column("bit", False, columns.current_year_flag), + TSQLColumn.from_column("bit", False, columns.prior_year_flag), + TSQLColumn.from_column("bit", False, columns.next_year_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_year_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_year_flag), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_start_fraction_of_quarter, + f"chk_DimFiscalMonth_{columns.month_start_fraction_of_quarter.name} CHECK ({columns.month_start_fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_end_fraction_of_quarter, + f"chk_DimFiscalMonth_{columns.month_end_fraction_of_quarter.name} CHECK ({columns.month_end_fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_start_fraction_of_year, + f"chk_DimFiscalMonth_{columns.month_start_fraction_of_year.name} CHECK ({columns.month_start_fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_end_fraction_of_year, + f"chk_DimFiscalMonth_{columns.month_end_fraction_of_year.name} CHECK ({columns.month_end_fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column("date", False, columns.current_quarter_start), + TSQLColumn.from_column("date", False, columns.current_quarter_end), + TSQLColumn.from_column("date", False, columns.current_year_start), + TSQLColumn.from_column("date", False, columns.current_year_end), + TSQLColumn.from_column("date", False, columns.prior_month_start), + TSQLColumn.from_column("date", False, columns.prior_month_end), + TSQLColumn.from_column("date", False, columns.prior_quarter_start), + TSQLColumn.from_column("date", False, columns.prior_quarter_end), + TSQLColumn.from_column("date", False, columns.prior_year_start), + TSQLColumn.from_column("date", False, columns.prior_year_end), + TSQLColumn.from_column("date", False, columns.next_month_start), + TSQLColumn.from_column("date", False, columns.next_month_end), + TSQLColumn.from_column("date", False, columns.next_quarter_start), + TSQLColumn.from_column("date", False, columns.next_quarter_end), + TSQLColumn.from_column("date", False, columns.next_year_start), + TSQLColumn.from_column("date", False, columns.next_year_end), + TSQLColumn.from_column("bit", False, columns.month_start_quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.month_end_quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.month_start_yearly_burnup), + TSQLColumn.from_column("bit", False, columns.month_end_yearly_burnup), + ] + self._columns = list(filter(lambda c: c.include, self._columns)) + self._columns.sort(key=lambda c: c.sort_index) + + def __iter__(self): + return iter(self._columns) + + def add_holiday_columns(self, holiday_config: HolidayConfig): + idx = self._columns[-1].sort_index + 1 + if holiday_config.generate_holidays: + for t in holiday_config.holiday_types: + self._columns.append( + TSQLColumn( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix}", + True, + idx, + "int", + False, + ) + ) + idx += 1 + + +class TSQLDimCalendarMonthColumns: + def __init__(self, columns: DimCalendarMonthColumns): + self._columns: list[TSQLColumn] = [ + TSQLColumn.from_column("int", False, columns.month_start_key), + TSQLColumn.from_column("int", False, columns.month_end_key), + TSQLColumn.from_column("date", False, columns.month_start_date), + TSQLColumn.from_column("date", False, columns.month_end_date), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_iso_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_iso_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_iso_week_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_iso_week_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_start_american_date_name + ), + TSQLColumn.from_column( + "varchar(10)", False, columns.month_end_american_date_name + ), + TSQLColumn.from_column("varchar(9)", False, columns.month_name), + TSQLColumn.from_column("varchar(3)", False, columns.month_abbrev), + TSQLColumn.from_column( + "varchar(8)", False, columns.month_start_year_week_name + ), + TSQLColumn.from_column( + "varchar(8)", False, columns.month_end_year_week_name + ), + TSQLColumn.from_column("varchar(7)", False, columns.year_month_name), + TSQLColumn.from_column("varchar(8)", False, columns.month_year_name), + TSQLColumn.from_column("varchar(6)", False, columns.year_quarter_name), + TSQLColumn.from_column("int", False, columns.year), + TSQLColumn.from_column("int", False, columns.month_start_year_week), + TSQLColumn.from_column("int", False, columns.month_end_year_week), + TSQLColumn.from_column("int", False, columns.year_month), + TSQLColumn.from_column("int", False, columns.year_quarter), + TSQLColumn.from_column("int", False, columns.month_start_day_of_quarter), + TSQLColumn.from_column("int", False, columns.month_end_day_of_quarter), + TSQLColumn.from_column("int", False, columns.month_start_day_of_year), + TSQLColumn.from_column("int", False, columns.month_end_day_of_year), + TSQLColumn.from_column("int", False, columns.month_start_week_of_quarter), + TSQLColumn.from_column("int", False, columns.month_end_week_of_quarter), + TSQLColumn.from_column("int", False, columns.month_start_week_of_year), + TSQLColumn.from_column("int", False, columns.month_end_week_of_year), + TSQLColumn.from_column("int", False, columns.month_of_quarter), + TSQLColumn.from_column("int", False, columns.quarter), + TSQLColumn.from_column("int", False, columns.days_in_month), + TSQLColumn.from_column("int", False, columns.days_in_quarter), + TSQLColumn.from_column("int", False, columns.days_in_year), + TSQLColumn.from_column("bit", False, columns.current_month_flag), + TSQLColumn.from_column("bit", False, columns.prior_month_flag), + TSQLColumn.from_column("bit", False, columns.next_month_flag), + TSQLColumn.from_column("bit", False, columns.current_quarter_flag), + TSQLColumn.from_column("bit", False, columns.prior_quarter_flag), + TSQLColumn.from_column("bit", False, columns.next_quarter_flag), + TSQLColumn.from_column("bit", False, columns.current_year_flag), + TSQLColumn.from_column("bit", False, columns.prior_year_flag), + TSQLColumn.from_column("bit", False, columns.next_year_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_month_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_quarter_flag), + TSQLColumn.from_column("bit", False, columns.first_day_of_year_flag), + TSQLColumn.from_column("bit", False, columns.last_day_of_year_flag), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_start_fraction_of_quarter, + f"chk_DimCalendarMonth_{columns.month_start_fraction_of_quarter.name} CHECK ({columns.month_start_fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_end_fraction_of_quarter, + f"chk_DimCalendarMonth_{columns.month_end_fraction_of_quarter.name} CHECK ({columns.month_end_fraction_of_quarter.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_start_fraction_of_year, + f"chk_DimCalendarMonth_{columns.month_start_fraction_of_year.name} CHECK ({columns.month_start_fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column( + "decimal(5,4)", + False, + columns.month_end_fraction_of_year, + f"chk_DimCalendarMonth_{columns.month_end_fraction_of_year.name} CHECK ({columns.month_end_fraction_of_year.name} BETWEEN 0 AND 1)", + ), + TSQLColumn.from_column("date", False, columns.current_quarter_start), + TSQLColumn.from_column("date", False, columns.current_quarter_end), + TSQLColumn.from_column("date", False, columns.current_year_start), + TSQLColumn.from_column("date", False, columns.current_year_end), + TSQLColumn.from_column("date", False, columns.prior_month_start), + TSQLColumn.from_column("date", False, columns.prior_month_end), + TSQLColumn.from_column("date", False, columns.prior_quarter_start), + TSQLColumn.from_column("date", False, columns.prior_quarter_end), + TSQLColumn.from_column("date", False, columns.prior_year_start), + TSQLColumn.from_column("date", False, columns.prior_year_end), + TSQLColumn.from_column("date", False, columns.next_month_start), + TSQLColumn.from_column("date", False, columns.next_month_end), + TSQLColumn.from_column("date", False, columns.next_quarter_start), + TSQLColumn.from_column("date", False, columns.next_quarter_end), + TSQLColumn.from_column("date", False, columns.next_year_start), + TSQLColumn.from_column("date", False, columns.next_year_end), + TSQLColumn.from_column("bit", False, columns.month_start_quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.month_end_quarterly_burnup), + TSQLColumn.from_column("bit", False, columns.month_start_yearly_burnup), + TSQLColumn.from_column("bit", False, columns.month_end_yearly_burnup), + ] + self._columns = list(filter(lambda c: c.include, self._columns)) + self._columns.sort(key=lambda c: c.sort_index) + + def __iter__(self): + return iter(self._columns) + + def add_holiday_columns(self, holiday_config: HolidayConfig): + idx = self._columns[-1].sort_index + 1 + if holiday_config.generate_holidays: + for t in holiday_config.holiday_types: + self._columns.append( + TSQLColumn( + f"{t.generated_column_prefix}{t.generated_monthly_count_column_postfix}", + True, + idx, + "int", + False, + ) + ) + idx += 1 diff --git a/awesome_date_dimension/config.py b/awesome_date_dimension/config.py new file mode 100644 index 0000000..db028e3 --- /dev/null +++ b/awesome_date_dimension/config.py @@ -0,0 +1,1524 @@ +from dataclasses import asdict, dataclass, field, fields +from datetime import date, datetime, tzinfo +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Iterable, Union, ValuesView + +from pytz import timezone + + +@dataclass(frozen=True) +class DateRange: + start_date: date = field( + default_factory=lambda: datetime.fromisoformat("2000-01-01").date() + ) + num_years: int = 100 + + def __post_init__(self): + assert self.num_years > 0, "num_years must be greater than 0." + + +@dataclass(frozen=True) +class FiscalConfig: + month_start_day: int = 1 + year_start_month: int = 1 + month_end_matches_calendar: bool = True + quarter_end_matches_calendar: bool = True + year_end_matches_calendar: bool = True + + def __post_init__(self): + assert ( + 1 <= self.month_start_day <= 28 + ), "fiscal_month_start_day must be between 1 and 28." + assert ( + 1 <= self.year_start_month <= 12 + ), "fiscal_year_start_month must be between 1 and 12." + + +@dataclass(frozen=True) +class HolidayType: + name: str + generated_column_prefix: str + generated_flag_column_postfix: str = "HolidayFlag" + generated_name_column_postfix: str = "HolidayName" + generated_monthly_count_column_postfix: str = "HolidaysInMonth" + included_in_business_day_calc: bool = False + + +@dataclass(frozen=True) +class Holiday: + holiday_name: str + holiday_date: date + + +@dataclass(frozen=True) +class HolidayCalendar: + holiday_type: HolidayType + holidays: list[Holiday] + + def __post_init__(self): + assert len(self.holidays) == len( + set(self.holidays) + ), f"detected duplicate holidays in the HolidayCalendar for {self.holiday_type}. This is not allowed." + + dates = [h.holiday_date for h in self.holidays] + assert len(dates) == len( + set(dates) + ), f"detected holidays with duplicate dates in the HolidayCalendar for {self.holiday_type}. This is not allowed." + + +def default_company_holidays() -> HolidayCalendar: + return HolidayCalendar( + HolidayType("Company Holiday", "Company", included_in_business_day_calc=True), + [ + Holiday("New Year's Day", datetime.fromisoformat("2012-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2012-01-16") + ), + Holiday("Memorial Day", datetime.fromisoformat("2012-05-28")), + Holiday("Independence Day", datetime.fromisoformat("2012-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2012-09-03")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2012-11-22")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2012-11-23")), + Holiday("Christmas Eve", datetime.fromisoformat("2012-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2012-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2013-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2013-01-21") + ), + Holiday("Memorial Day", datetime.fromisoformat("2013-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2013-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2013-09-02")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2013-11-28")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2013-11-29")), + Holiday("Christmas Eve", datetime.fromisoformat("2013-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2013-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2014-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2014-01-20") + ), + Holiday("Memorial Day", datetime.fromisoformat("2014-05-26")), + Holiday("Independence Day", datetime.fromisoformat("2014-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2014-09-01")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2014-11-28")), + Holiday("Christmas Eve", datetime.fromisoformat("2014-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2014-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2015-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2015-01-19") + ), + Holiday("Memorial Day", datetime.fromisoformat("2015-05-25")), + Holiday("Independence Day", datetime.fromisoformat("2015-07-03")), + Holiday("Labor Day", datetime.fromisoformat("2015-09-07")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2015-11-26")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2015-11-27")), + Holiday("Christmas Eve", datetime.fromisoformat("2015-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2015-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2016-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2016-01-18") + ), + Holiday("Memorial Day", datetime.fromisoformat("2016-05-30")), + Holiday("Independence Day", datetime.fromisoformat("2016-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2016-09-05")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2016-11-24")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2016-11-25")), + Holiday("Christmas Eve", datetime.fromisoformat("2016-12-23")), + Holiday("Christmas Day", datetime.fromisoformat("2016-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2017-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2017-01-16") + ), + Holiday("Memorial Day", datetime.fromisoformat("2017-05-29")), + Holiday("Independence Day", datetime.fromisoformat("2017-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2017-09-04")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2017-11-23")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2017-11-24")), + Holiday("Christmas Eve", datetime.fromisoformat("2017-12-25")), + Holiday("Christmas Day", datetime.fromisoformat("2017-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2018-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2018-01-15") + ), + Holiday("Memorial Day", datetime.fromisoformat("2018-05-28")), + Holiday("Independence Day", datetime.fromisoformat("2018-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2018-09-03")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2018-11-22")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2018-11-23")), + Holiday("Christmas Eve", datetime.fromisoformat("2018-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2018-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2019-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2019-01-21") + ), + Holiday("Memorial Day", datetime.fromisoformat("2019-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2019-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2019-09-02")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2019-11-28")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2019-11-29")), + Holiday("Christmas Eve", datetime.fromisoformat("2019-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2019-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2020-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2020-01-20") + ), + Holiday("Memorial Day", datetime.fromisoformat("2020-05-25")), + Holiday("Independence Day", datetime.fromisoformat("2020-07-03")), + Holiday("Labor Day", datetime.fromisoformat("2020-09-07")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2020-11-26")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2020-11-27")), + Holiday("Christmas Eve", datetime.fromisoformat("2020-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2020-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2021-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2021-01-18") + ), + Holiday("Memorial Day", datetime.fromisoformat("2021-05-31")), + Holiday("Independence Day", datetime.fromisoformat("2021-07-05")), + Holiday("Labor Day", datetime.fromisoformat("2021-09-06")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2021-11-26")), + Holiday("Christmas Eve", datetime.fromisoformat("2021-12-23")), + Holiday("Christmas Day", datetime.fromisoformat("2021-12-24")), + Holiday("New Year's Day", datetime.fromisoformat("2021-12-31")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2022-01-17") + ), + Holiday("Memorial Day", datetime.fromisoformat("2022-05-30")), + Holiday("Independence Day", datetime.fromisoformat("2022-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2022-09-05")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2022-11-24")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2022-11-25")), + Holiday("Christmas Eve", datetime.fromisoformat("2022-12-23")), + Holiday("Christmas Day", datetime.fromisoformat("2022-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2023-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2023-01-16") + ), + Holiday("Memorial Day", datetime.fromisoformat("2023-05-29")), + Holiday("Independence Day", datetime.fromisoformat("2023-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2023-09-04")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2023-11-23")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2023-11-24")), + Holiday("Christmas Eve", datetime.fromisoformat("2023-12-25")), + Holiday("Christmas Day", datetime.fromisoformat("2023-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2024-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2024-01-15") + ), + Holiday("Memorial Day", datetime.fromisoformat("2024-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2024-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2024-09-02")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2024-11-28")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2024-11-29")), + Holiday("Christmas Eve", datetime.fromisoformat("2024-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2024-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2025-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2025-01-20") + ), + Holiday("Memorial Day", datetime.fromisoformat("2025-05-26")), + Holiday("Independence Day", datetime.fromisoformat("2025-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2025-09-01")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2025-11-27")), + Holiday("Friday After Thanksgiving", datetime.fromisoformat("2025-11-28")), + Holiday("Christmas Eve", datetime.fromisoformat("2025-12-24")), + Holiday("Christmas Day", datetime.fromisoformat("2025-12-25")), + ], + ) + + +def default_us_public_holidays() -> HolidayCalendar: + return HolidayCalendar( + HolidayType("US Public Holiday", "USPublic"), + [ + Holiday("New Year's Day", datetime.fromisoformat("2012-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2012-01-16") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2012-02-20")), + Holiday("Memorial Day", datetime.fromisoformat("2012-05-28")), + Holiday("Independence Day", datetime.fromisoformat("2012-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2012-09-03")), + Holiday("Columbus Day", datetime.fromisoformat("2012-10-08")), + Holiday("Veterans Day", datetime.fromisoformat("2012-11-12")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2012-11-22")), + Holiday("Christmas Day", datetime.fromisoformat("2012-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2013-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2013-01-21") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2013-02-18")), + Holiday("Memorial Day", datetime.fromisoformat("2013-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2013-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2013-09-02")), + Holiday("Columbus Day", datetime.fromisoformat("2013-10-14")), + Holiday("Veterans Day", datetime.fromisoformat("2013-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2013-11-28")), + Holiday("Christmas Day", datetime.fromisoformat("2013-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2014-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2014-01-20") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2014-02-17")), + Holiday("Memorial Day", datetime.fromisoformat("2014-05-26")), + Holiday("Independence Day", datetime.fromisoformat("2014-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2014-09-01")), + Holiday("Columbus Day", datetime.fromisoformat("2014-10-13")), + Holiday("Veterans Day", datetime.fromisoformat("2014-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2014-11-27")), + Holiday("Christmas Day", datetime.fromisoformat("2014-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2015-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2015-01-19") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2015-02-16")), + Holiday("Memorial Day", datetime.fromisoformat("2015-05-25")), + Holiday("Independence Day", datetime.fromisoformat("2015-07-03")), + Holiday("Labor Day", datetime.fromisoformat("2015-09-07")), + Holiday("Columbus Day", datetime.fromisoformat("2015-10-12")), + Holiday("Veterans Day", datetime.fromisoformat("2015-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2015-11-26")), + Holiday("Christmas Day", datetime.fromisoformat("2015-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2016-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2016-01-18") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2016-02-15")), + Holiday("Memorial Day", datetime.fromisoformat("2016-05-30")), + Holiday("Independence Day", datetime.fromisoformat("2016-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2016-09-05")), + Holiday("Columbus Day", datetime.fromisoformat("2016-10-10")), + Holiday("Veterans Day", datetime.fromisoformat("2016-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2016-11-24")), + Holiday("Christmas Day", datetime.fromisoformat("2016-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2017-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2017-01-16") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2017-02-20")), + Holiday("Memorial Day", datetime.fromisoformat("2017-05-29")), + Holiday("Independence Day", datetime.fromisoformat("2017-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2017-09-04")), + Holiday("Columbus Day", datetime.fromisoformat("2017-10-09")), + Holiday("Veterans Day", datetime.fromisoformat("2017-11-10")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2017-11-23")), + Holiday("Christmas Day", datetime.fromisoformat("2017-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2018-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2018-01-15") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2018-02-19")), + Holiday("Memorial Day", datetime.fromisoformat("2018-05-28")), + Holiday("Independence Day", datetime.fromisoformat("2018-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2018-09-03")), + Holiday("Columbus Day", datetime.fromisoformat("2018-10-08")), + Holiday("Veterans Day", datetime.fromisoformat("2018-11-12")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2018-11-22")), + Holiday("Christmas Day", datetime.fromisoformat("2018-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2019-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2019-01-21") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2019-02-18")), + Holiday("Memorial Day", datetime.fromisoformat("2019-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2019-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2019-09-02")), + Holiday("Columbus Day", datetime.fromisoformat("2019-10-14")), + Holiday("Veterans Day", datetime.fromisoformat("2019-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2019-11-28")), + Holiday("Christmas Day", datetime.fromisoformat("2019-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2020-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2020-01-20") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2020-02-17")), + Holiday("Memorial Day", datetime.fromisoformat("2020-05-25")), + Holiday("Independence Day", datetime.fromisoformat("2020-07-03")), + Holiday("Labor Day", datetime.fromisoformat("2020-09-07")), + Holiday("Columbus Day", datetime.fromisoformat("2020-10-12")), + Holiday("Veterans Day", datetime.fromisoformat("2020-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2020-11-26")), + Holiday("Christmas Day", datetime.fromisoformat("2020-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2021-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2021-01-18") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2021-02-15")), + Holiday("Memorial Day", datetime.fromisoformat("2021-05-31")), + Holiday("Independence Day", datetime.fromisoformat("2021-07-05")), + Holiday("Labor Day", datetime.fromisoformat("2021-09-06")), + Holiday("Columbus Day", datetime.fromisoformat("2021-10-11")), + Holiday("Veterans Day", datetime.fromisoformat("2021-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2021-11-25")), + Holiday("Christmas Day", datetime.fromisoformat("2021-12-24")), + Holiday("New Year's Day", datetime.fromisoformat("2021-12-31")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2022-01-17") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2022-02-21")), + Holiday("Memorial Day", datetime.fromisoformat("2022-05-30")), + Holiday("Independence Day", datetime.fromisoformat("2022-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2022-09-05")), + Holiday("Columbus Day", datetime.fromisoformat("2022-10-10")), + Holiday("Veterans Day", datetime.fromisoformat("2022-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2022-11-24")), + Holiday("Christmas Day", datetime.fromisoformat("2022-12-26")), + Holiday("New Year's Day", datetime.fromisoformat("2023-01-02")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2023-01-16") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2023-02-20")), + Holiday("Memorial Day", datetime.fromisoformat("2023-05-29")), + Holiday("Independence Day", datetime.fromisoformat("2023-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2023-09-04")), + Holiday("Columbus Day", datetime.fromisoformat("2023-10-09")), + Holiday("Veterans Day", datetime.fromisoformat("2023-11-10")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2023-11-23")), + Holiday("Christmas Day", datetime.fromisoformat("2023-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2024-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2024-01-15") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2024-02-19")), + Holiday("Memorial Day", datetime.fromisoformat("2024-05-27")), + Holiday("Independence Day", datetime.fromisoformat("2024-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2024-09-02")), + Holiday("Columbus Day", datetime.fromisoformat("2024-10-14")), + Holiday("Veterans Day", datetime.fromisoformat("2024-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2024-11-28")), + Holiday("Christmas Day", datetime.fromisoformat("2024-12-25")), + Holiday("New Year's Day", datetime.fromisoformat("2025-01-01")), + Holiday( + "Martin Luther King, Jr. Day", datetime.fromisoformat("2025-01-20") + ), + Holiday("Presidents' Day", datetime.fromisoformat("2025-02-17")), + Holiday("Memorial Day", datetime.fromisoformat("2025-05-26")), + Holiday("Independence Day", datetime.fromisoformat("2025-07-04")), + Holiday("Labor Day", datetime.fromisoformat("2025-09-01")), + Holiday("Columbus Day", datetime.fromisoformat("2025-10-13")), + Holiday("Veterans Day", datetime.fromisoformat("2025-11-11")), + Holiday("Thanksgiving Day", datetime.fromisoformat("2025-11-27")), + Holiday("Christmas Day", datetime.fromisoformat("2025-12-25")), + ], + ) + + +@dataclass(frozen=True) +class Column: + name: str + include: bool + sort_index: int + + +def _assert_no_duplicate_colnames(collection_name: str, cols: Iterable[Column]): + colnames = [c.name for c in cols] + assert len(colnames) == len( + set(colnames) + ), f"detected duplicate column names in {collection_name}. This is not allowed." + + +def _assert_no_duplicate_col_sortkeys(collection_name: str, cols: Iterable[Column]): + indices = [c.sort_index for c in cols] + assert len(indices) == len( + set(indices) + ), f"detected duplicate sort_indices in {collection_name}. This is not allowed." + + +@dataclass(frozen=True) +class HolidayTypesColumns: + holiday_type_key: Column = field( + default_factory=lambda: Column("HolidayTypeKey", True, 1000) + ) + holiday_type_name: Column = field( + default_factory=lambda: Column("HolidayTypeName", True, 2000) + ) + + def __post_init__(self): + assert ( + self.holiday_type_key.include and self.holiday_type_name.include + ), "all HolidayTypes columns must be included." + col_list = [self.holiday_type_key, self.holiday_type_name] + _assert_no_duplicate_colnames("HolidayTypesColumns", col_list) + _assert_no_duplicate_col_sortkeys("HolidayTypesColumns", col_list) + + +@dataclass(frozen=True) +class HolidaysColumns: + date_key: Column = field(default_factory=lambda: Column("DateKey", True, 0)) + holiday_name: Column = field(default_factory=lambda: Column("HolidayName", True, 1)) + holiday_type_key: Column = field( + default_factory=lambda: Column("HolidayTypeKey", True, 2) + ) + + def __post_init__(self): + assert ( + self.date_key.include + and self.holiday_name.include + and self.holiday_type_key.include + ), "all Holidays columns must be included." + col_list = [self.date_key, self.holiday_name, self.holiday_type_key] + _assert_no_duplicate_colnames("HolidayColumns", col_list) + _assert_no_duplicate_col_sortkeys("HolidayColumns", col_list) + + +@dataclass(frozen=True) +class HolidayConfig: + generate_holidays: bool = True + holiday_types_schema_name: str = "integration" + holiday_types_table_name: str = "manual_HolidayTypes" + holiday_types_columns: HolidayTypesColumns = field( + default_factory=lambda: HolidayTypesColumns() + ) + holidays_schema_name: str = "integration" + holidays_table_name: str = "manual_Holidays" + holidays_columns: HolidaysColumns = field(default_factory=lambda: HolidaysColumns()) + holiday_calendars: list[HolidayCalendar] = field( + default_factory=lambda: [ + default_company_holidays(), + default_us_public_holidays(), + ] + ) + holiday_types: list[HolidayType] = field(init=False, default_factory=list) + + def __post_init__(self): + if self.generate_holidays: + assert ( + self.holiday_types_schema_name + self.holiday_types_table_name + != self.holidays_schema_name + self.holidays_table_name + ), "holidays table name and holiday types table name are the same. This is not allowed." + holiday_types = [cal.holiday_type for cal in self.holiday_calendars] + holiday_type_names = [t.name for t in holiday_types] + holiday_type_prefixes = [t.generated_column_prefix for t in holiday_types] + assert len(holiday_type_names) == len( + set(holiday_type_names) + ), "detected a duplicate HolidayType name in HolidayConfig. This would create multiple columns with the same name, which is not allowed." + assert len(holiday_type_prefixes) == len( + set(holiday_type_prefixes) + ), "detected a duplicate HolidayTypePrefix in HolidayConfig. This would create multiple columns with the same name, which is not allowed." + object.__setattr__(self, "holiday_types", holiday_types) + + +@dataclass(frozen=True) +class DimDateColumns: + date_key: Column = field(default_factory=lambda: Column("DateKey", True, 1000)) + the_date: Column = field(default_factory=lambda: Column("TheDate", True, 2000)) + iso_date_name: Column = field( + default_factory=lambda: Column("ISODateName", True, 3000) + ) + iso_week_date_name: Column = field( + default_factory=lambda: Column("ISOWeekDateName", True, 4000) + ) + american_date_name: Column = field( + default_factory=lambda: Column("AmericanDateName", True, 5000) + ) + day_of_week_name: Column = field( + default_factory=lambda: Column("DayOfWeekName", True, 6000) + ) + day_of_week_abbrev: Column = field( + default_factory=lambda: Column("DayOfWeekAbbrev", True, 7000) + ) + month_name: Column = field(default_factory=lambda: Column("MonthName", True, 8000)) + month_abbrev: Column = field( + default_factory=lambda: Column("MonthAbbrev", True, 9000) + ) + year_week_name: Column = field( + default_factory=lambda: Column("YearWeekName", True, 10000) + ) + year_month_name: Column = field( + default_factory=lambda: Column("YearMonthName", True, 11000) + ) + month_year_name: Column = field( + default_factory=lambda: Column("MonthYearName", True, 12000) + ) + year_quarter_name: Column = field( + default_factory=lambda: Column("YearQuarterName", True, 13000) + ) + year: Column = field(default_factory=lambda: Column("Year", True, 14000)) + year_week: Column = field(default_factory=lambda: Column("YearWeek", True, 15000)) + iso_year_week_code: Column = field( + default_factory=lambda: Column("ISOYearWeekCode", True, 16000) + ) + year_month: Column = field(default_factory=lambda: Column("YearMonth", True, 17000)) + year_quarter: Column = field( + default_factory=lambda: Column("YearQuarter", True, 18000) + ) + day_of_week_starting_monday: Column = field( + default_factory=lambda: Column("DayOfWeekStartingMonday", True, 19000) + ) + day_of_week: Column = field( + default_factory=lambda: Column("DayOfWeek", True, 20000) + ) + day_of_month: Column = field( + default_factory=lambda: Column("DayOfMonth", True, 21000) + ) + day_of_quarter: Column = field( + default_factory=lambda: Column("DayOfQuarter", True, 22000) + ) + day_of_year: Column = field( + default_factory=lambda: Column("DayOfYear", True, 23000) + ) + week_of_quarter: Column = field( + default_factory=lambda: Column("WeekOfQuarter", True, 24000) + ) + week_of_year: Column = field( + default_factory=lambda: Column("WeekOfYear", True, 25000) + ) + iso_week_of_year: Column = field( + default_factory=lambda: Column("ISOWeekOfYear", True, 26000) + ) + month: Column = field(default_factory=lambda: Column("Month", True, 27000)) + month_of_quarter: Column = field( + default_factory=lambda: Column("MonthOfQuarter", True, 28000) + ) + quarter: Column = field(default_factory=lambda: Column("Quarter", True, 29000)) + days_in_month: Column = field( + default_factory=lambda: Column("DaysInMonth", True, 30000) + ) + days_in_quarter: Column = field( + default_factory=lambda: Column("DaysInQuarter", True, 31000) + ) + days_in_year: Column = field( + default_factory=lambda: Column("DaysInYear", True, 32000) + ) + day_offset_from_today: Column = field( + default_factory=lambda: Column("DayOffsetFromToday", True, 33000) + ) + month_offset_from_today: Column = field( + default_factory=lambda: Column("MonthOffsetFromToday", True, 34000) + ) + quarter_offset_from_today: Column = field( + default_factory=lambda: Column("QuarterOffsetFromToday", True, 35000) + ) + year_offset_from_today: Column = field( + default_factory=lambda: Column("YearOffsetFromToday", True, 36000) + ) + today_flag: Column = field(default_factory=lambda: Column("TodayFlag", True, 37000)) + current_week_starting_monday_flag: Column = field( + default_factory=lambda: Column("CurrentWeekStartingMondayFlag", True, 38000) + ) + current_week_flag: Column = field( + default_factory=lambda: Column("CurrentWeekFlag", True, 39000) + ) + prior_week_flag: Column = field( + default_factory=lambda: Column("PriorWeekFlag", True, 40000) + ) + next_week_flag: Column = field( + default_factory=lambda: Column("NextWeekFlag", True, 41000) + ) + current_month_flag: Column = field( + default_factory=lambda: Column("CurrentMonthFlag", True, 42000) + ) + prior_month_flag: Column = field( + default_factory=lambda: Column("PriorMonthFlag", True, 43000) + ) + next_month_flag: Column = field( + default_factory=lambda: Column("NextMonthFlag", True, 44000) + ) + current_quarter_flag: Column = field( + default_factory=lambda: Column("CurrentQuarterFlag", True, 45000) + ) + prior_quarter_flag: Column = field( + default_factory=lambda: Column("PriorQuarterFlag", True, 46000) + ) + next_quarter_flag: Column = field( + default_factory=lambda: Column("NextQuarterFlag", True, 47000) + ) + current_year_flag: Column = field( + default_factory=lambda: Column("CurrentYearFlag", True, 48000) + ) + prior_year_flag: Column = field( + default_factory=lambda: Column("PriorYearFlag", True, 49000) + ) + next_year_flag: Column = field( + default_factory=lambda: Column("NextYearFlag", True, 50000) + ) + weekday_flag: Column = field( + default_factory=lambda: Column("WeekdayFlag", True, 51000) + ) + business_day_flag: Column = field( + default_factory=lambda: Column("BusinessDayFlag", True, 52000) + ) + first_day_of_month_flag: Column = field( + default_factory=lambda: Column("FirstDayOfMonthFlag", True, 53000) + ) + last_day_of_month_flag: Column = field( + default_factory=lambda: Column("LastDayOfMonthFlag", True, 54000) + ) + first_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("FirstDayOfQuarterFlag", True, 55000) + ) + last_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("LastDayOfQuarterFlag", True, 56000) + ) + first_day_of_year_flag: Column = field( + default_factory=lambda: Column("FirstDayOfYearFlag", True, 57000) + ) + last_day_of_year_flag: Column = field( + default_factory=lambda: Column("LastDayOfYearFlag", True, 58000) + ) + fraction_of_week: Column = field( + default_factory=lambda: Column("FractionOfWeek", True, 59000) + ) + fraction_of_month: Column = field( + default_factory=lambda: Column("FractionOfMonth", True, 60000) + ) + fraction_of_quarter: Column = field( + default_factory=lambda: Column("FractionOfQuarter", True, 61000) + ) + fraction_of_year: Column = field( + default_factory=lambda: Column("FractionOfYear", True, 62000) + ) + prior_day: Column = field(default_factory=lambda: Column("PriorDay", True, 63000)) + next_day: Column = field(default_factory=lambda: Column("NextDay", True, 64000)) + same_day_prior_week: Column = field( + default_factory=lambda: Column("SameDayPriorWeek", True, 65000) + ) + same_day_prior_month: Column = field( + default_factory=lambda: Column("SameDayPriorMonth", True, 66000) + ) + same_day_prior_quarter: Column = field( + default_factory=lambda: Column("SameDayPriorQuarter", True, 67000) + ) + same_day_prior_year: Column = field( + default_factory=lambda: Column("SameDayPriorYear", True, 68000) + ) + same_day_next_week: Column = field( + default_factory=lambda: Column("SameDayNextWeek", True, 69000) + ) + same_day_next_month: Column = field( + default_factory=lambda: Column("SameDayNextMonth", True, 70000) + ) + same_day_next_quarter: Column = field( + default_factory=lambda: Column("SameDayNextQuarter", True, 71000) + ) + same_day_next_year: Column = field( + default_factory=lambda: Column("SameDayNextYear", True, 72000) + ) + current_week_start: Column = field( + default_factory=lambda: Column("CurrentWeekStart", True, 73000) + ) + current_week_end: Column = field( + default_factory=lambda: Column("CurrentWeekEnd", True, 74000) + ) + current_month_start: Column = field( + default_factory=lambda: Column("CurrentMonthStart", True, 75000) + ) + current_month_end: Column = field( + default_factory=lambda: Column("CurrentMonthEnd", True, 76000) + ) + current_quarter_start: Column = field( + default_factory=lambda: Column("CurrentQuarterStart", True, 77000) + ) + current_quarter_end: Column = field( + default_factory=lambda: Column("CurrentQuarterEnd", True, 78000) + ) + current_year_start: Column = field( + default_factory=lambda: Column("CurrentYearStart", True, 79000) + ) + current_year_end: Column = field( + default_factory=lambda: Column("CurrentYearEnd", True, 80000) + ) + prior_week_start: Column = field( + default_factory=lambda: Column("PriorWeekStart", True, 81000) + ) + prior_week_end: Column = field( + default_factory=lambda: Column("PriorWeekEnd", True, 82000) + ) + prior_month_start: Column = field( + default_factory=lambda: Column("PriorMonthStart", True, 83000) + ) + prior_month_end: Column = field( + default_factory=lambda: Column("PriorMonthEnd", True, 84000) + ) + prior_quarter_start: Column = field( + default_factory=lambda: Column("PriorQuarterStart", True, 85000) + ) + prior_quarter_end: Column = field( + default_factory=lambda: Column("PriorQuarterEnd", True, 86000) + ) + prior_year_start: Column = field( + default_factory=lambda: Column("PriorYearStart", True, 87000) + ) + prior_year_end: Column = field( + default_factory=lambda: Column("PriorYearEnd", True, 88000) + ) + next_week_start: Column = field( + default_factory=lambda: Column("NextWeekStart", True, 89000) + ) + next_week_end: Column = field( + default_factory=lambda: Column("NextWeekEnd", True, 90000) + ) + next_month_start: Column = field( + default_factory=lambda: Column("NextMonthStart", True, 91000) + ) + next_month_end: Column = field( + default_factory=lambda: Column("NextMonthEnd", True, 92000) + ) + next_quarter_start: Column = field( + default_factory=lambda: Column("NextQuarterStart", True, 93000) + ) + next_quarter_end: Column = field( + default_factory=lambda: Column("NextQuarterEnd", True, 94000) + ) + next_year_start: Column = field( + default_factory=lambda: Column("NextYearStart", True, 95000) + ) + next_year_end: Column = field( + default_factory=lambda: Column("NextYearEnd", True, 96000) + ) + weekly_burnup_starting_monday: Column = field( + default_factory=lambda: Column("WeeklyBurnupStartingMonday", True, 97000) + ) + weekly_burnup: Column = field( + default_factory=lambda: Column("WeeklyBurnup", True, 98000) + ) + monthly_burnup: Column = field( + default_factory=lambda: Column("MonthlyBurnup", True, 99000) + ) + quarterly_burnup: Column = field( + default_factory=lambda: Column("QuarterlyBurnup", True, 100000) + ) + yearly_burnup: Column = field( + default_factory=lambda: Column("YearlyBurnup", True, 101000) + ) + fiscal_month_name: Column = field( + default_factory=lambda: Column("FiscalMonthName", True, 102000) + ) + fiscal_month_abbrev: Column = field( + default_factory=lambda: Column("FiscalMonthAbbrev", True, 103000) + ) + fiscal_year_week_name: Column = field( + default_factory=lambda: Column("FiscalYearWeekName", True, 104000) + ) + fiscal_year_month_name: Column = field( + default_factory=lambda: Column("FiscalYearMonthName", True, 105000) + ) + fiscal_month_year_name: Column = field( + default_factory=lambda: Column("FiscalMonthYearName", True, 106000) + ) + fiscal_year_quarter_name: Column = field( + default_factory=lambda: Column("FiscalYearQuarterName", True, 107000) + ) + fiscal_year: Column = field( + default_factory=lambda: Column("FiscalYear", True, 108000) + ) + fiscal_year_week: Column = field( + default_factory=lambda: Column("FiscalYearWeek", True, 109000) + ) + fiscal_year_month: Column = field( + default_factory=lambda: Column("FiscalYearMonth", True, 110000) + ) + fiscal_year_quarter: Column = field( + default_factory=lambda: Column("FiscalYearQuarter", True, 111000) + ) + fiscal_day_of_month: Column = field( + default_factory=lambda: Column("FiscalDayOfMonth", True, 112000) + ) + fiscal_day_of_quarter: Column = field( + default_factory=lambda: Column("FiscalDayOfQuarter", True, 113000) + ) + fiscal_day_of_year: Column = field( + default_factory=lambda: Column("FiscalDayOfYear", True, 114000) + ) + fiscal_week_of_quarter: Column = field( + default_factory=lambda: Column("FiscalWeekOfQuarter", True, 115000) + ) + fiscal_week_of_year: Column = field( + default_factory=lambda: Column("FiscalWeekOfYear", True, 116000) + ) + fiscal_month: Column = field( + default_factory=lambda: Column("FiscalMonth", True, 117000) + ) + fiscal_month_of_quarter: Column = field( + default_factory=lambda: Column("FiscalMonthOfQuarter", True, 118000) + ) + fiscal_quarter: Column = field( + default_factory=lambda: Column("FiscalQuarter", True, 119000) + ) + fiscal_days_in_month: Column = field( + default_factory=lambda: Column("FiscalDaysInMonth", True, 120000) + ) + fiscal_days_in_quarter: Column = field( + default_factory=lambda: Column("FiscalDaysInQuarter", True, 121000) + ) + fiscal_days_in_year: Column = field( + default_factory=lambda: Column("FiscalDaysInYear", True, 122000) + ) + fiscal_current_month_flag: Column = field( + default_factory=lambda: Column("FiscalCurrentMonthFlag", True, 123000) + ) + fiscal_prior_month_flag: Column = field( + default_factory=lambda: Column("FiscalPriorMonthFlag", True, 124000) + ) + fiscal_next_month_flag: Column = field( + default_factory=lambda: Column("FiscalNextMonthFlag", True, 125000) + ) + fiscal_current_quarter_flag: Column = field( + default_factory=lambda: Column("FiscalCurrentQuarterFlag", True, 126000) + ) + fiscal_prior_quarter_flag: Column = field( + default_factory=lambda: Column("FiscalPriorQuarterFlag", True, 127000) + ) + fiscal_next_quarter_flag: Column = field( + default_factory=lambda: Column("FiscalNextQuarterFlag", True, 128000) + ) + fiscal_current_year_flag: Column = field( + default_factory=lambda: Column("FiscalCurrentYearFlag", True, 129000) + ) + fiscal_prior_year_flag: Column = field( + default_factory=lambda: Column("FiscalPriorYearFlag", True, 130000) + ) + fiscal_next_year_flag: Column = field( + default_factory=lambda: Column("FiscalNextYearFlag", True, 131000) + ) + fiscal_first_day_of_month_flag: Column = field( + default_factory=lambda: Column("FiscalFirstDayOfMonthFlag", True, 132000) + ) + fiscal_last_day_of_month_flag: Column = field( + default_factory=lambda: Column("FiscalLastDayOfMonthFlag", True, 133000) + ) + fiscal_first_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("FiscalFirstDayOfQuarterFlag", True, 134000) + ) + fiscal_last_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("FiscalLastDayOfQuarterFlag", True, 135000) + ) + fiscal_first_day_of_year_flag: Column = field( + default_factory=lambda: Column("FiscalFirstDayOfYearFlag", True, 136000) + ) + fiscal_last_day_of_year_flag: Column = field( + default_factory=lambda: Column("FiscalLastDayOfYearFlag", True, 137000) + ) + fiscal_fraction_of_month: Column = field( + default_factory=lambda: Column("FiscalFractionOfMonth", True, 138000) + ) + fiscal_fraction_of_quarter: Column = field( + default_factory=lambda: Column("FiscalFractionOfQuarter", True, 139000) + ) + fiscal_fraction_of_year: Column = field( + default_factory=lambda: Column("FiscalFractionOfYear", True, 140000) + ) + fiscal_current_month_start: Column = field( + default_factory=lambda: Column("FiscalCurrentMonthStart", True, 141000) + ) + fiscal_current_month_end: Column = field( + default_factory=lambda: Column("FiscalCurrentMonthEnd", True, 142000) + ) + fiscal_current_quarter_start: Column = field( + default_factory=lambda: Column("FiscalCurrentQuarterStart", True, 143000) + ) + fiscal_current_quarter_end: Column = field( + default_factory=lambda: Column("FiscalCurrentQuarterEnd", True, 144000) + ) + fiscal_current_year_start: Column = field( + default_factory=lambda: Column("FiscalCurrentYearStart", True, 145000) + ) + fiscal_current_year_end: Column = field( + default_factory=lambda: Column("FiscalCurrentYearEnd", True, 146000) + ) + fiscal_prior_month_start: Column = field( + default_factory=lambda: Column("FiscalPriorMonthStart", True, 147000) + ) + fiscal_prior_month_end: Column = field( + default_factory=lambda: Column("FiscalPriorMonthEnd", True, 148000) + ) + fiscal_prior_quarter_start: Column = field( + default_factory=lambda: Column("FiscalPriorQuarterStart", True, 149000) + ) + fiscal_prior_quarter_end: Column = field( + default_factory=lambda: Column("FiscalPriorQuarterEnd", True, 150000) + ) + fiscal_prior_year_start: Column = field( + default_factory=lambda: Column("FiscalPriorYearStart", True, 151000) + ) + fiscal_prior_year_end: Column = field( + default_factory=lambda: Column("FiscalPriorYearEnd", True, 152000) + ) + fiscal_next_month_start: Column = field( + default_factory=lambda: Column("FiscalNextMonthStart", True, 153000) + ) + fiscal_next_month_end: Column = field( + default_factory=lambda: Column("FiscalNextMonthEnd", True, 154000) + ) + fiscal_next_quarter_start: Column = field( + default_factory=lambda: Column("FiscalNextQuarterStart", True, 155000) + ) + fiscal_next_quarter_end: Column = field( + default_factory=lambda: Column("FiscalNextQuarterEnd", True, 156000) + ) + fiscal_next_year_start: Column = field( + default_factory=lambda: Column("FiscalNextYearStart", True, 157000) + ) + fiscal_next_year_end: Column = field( + default_factory=lambda: Column("FiscalNextYearEnd", True, 158000) + ) + fiscal_monthly_burnup: Column = field( + default_factory=lambda: Column("FiscalMonthlyBurnup", True, 159000) + ) + fiscal_quarterly_burnup: Column = field( + default_factory=lambda: Column("FiscalQuarterlyBurnup", True, 160000) + ) + fiscal_yearly_burnup: Column = field( + default_factory=lambda: Column("FiscalYearlyBurnup", True, 161000) + ) + + def __post_init__(self): + field_names = (f.name for f in fields(self)) + cols = [self.__dict__[name] for name in field_names] + assert ( + self.date_key.include + ), "DimDateColumns.date_key must be included, as it is the table key." + _assert_no_duplicate_colnames(DimDateColumns.__name__, cols) + _assert_no_duplicate_col_sortkeys(DimDateColumns.__name__, cols) + + +@dataclass(frozen=True) +class DimDateConfig: + table_schema: str = "dbo" + table_name: str = "DimDate" + columns: DimDateColumns = field(default_factory=lambda: DimDateColumns()) + column_factory: Callable[[str, Column], Column] = None + + def __post_init__(self): + if self.column_factory is not None: + col_fields = fields(self.columns) + new_cols: dict[str, Column] = {} + for f in col_fields: + col: Column = self.columns.__dict__[f.name] + new_col = self.column_factory(f.name, col) + assert isinstance( + new_col, Column + ), f"column_factory returned a value that was not a column. This is not allowed. Value: {new_col}" + new_cols[f.name] = new_col + object.__setattr__(self, "columns", DimDateColumns(**new_cols)) + + +@dataclass(frozen=True) +class DimFiscalMonthColumns: + month_start_key: Column = field( + default_factory=lambda: Column("MonthStartKey", True, 1000) + ) + month_end_key: Column = field( + default_factory=lambda: Column("MonthEndKey", True, 2000) + ) + month_start_date: Column = field( + default_factory=lambda: Column("MonthStartDate", True, 3000) + ) + month_end_date: Column = field( + default_factory=lambda: Column("MonthEndDate", True, 4000) + ) + month_start_iso_date_name: Column = field( + default_factory=lambda: Column("MonthStartISODateName", True, 5000) + ) + month_end_iso_date_name: Column = field( + default_factory=lambda: Column("MonthEndISODateName", True, 6000) + ) + month_start_iso_week_date_name: Column = field( + default_factory=lambda: Column("MonthStartISOWeekDateName", True, 7000) + ) + month_end_iso_week_date_name: Column = field( + default_factory=lambda: Column("MonthEndISOWeekDateName", True, 8000) + ) + month_start_american_date_name: Column = field( + default_factory=lambda: Column("MonthStartAmericanDateName", True, 9000) + ) + month_end_american_date_name: Column = field( + default_factory=lambda: Column("MonthEndAmericanDateName", True, 10000) + ) + month_name: Column = field(default_factory=lambda: Column("MonthName", True, 11000)) + month_abbrev: Column = field( + default_factory=lambda: Column("MonthAbbrev", True, 12000) + ) + month_start_year_week_name: Column = field( + default_factory=lambda: Column("MonthStartYearWeekName", True, 13000) + ) + month_end_year_week_name: Column = field( + default_factory=lambda: Column("MonthEndYearWeekName", True, 14000) + ) + year_month_name: Column = field( + default_factory=lambda: Column("YearMonthName", True, 15000) + ) + month_year_name: Column = field( + default_factory=lambda: Column("MonthYearName", True, 16000) + ) + year_quarter_name: Column = field( + default_factory=lambda: Column("YearQuarterName", True, 17000) + ) + year: Column = field(default_factory=lambda: Column("Year", True, 18000)) + month_start_year_week: Column = field( + default_factory=lambda: Column("MonthStartYearWeek", True, 19000) + ) + month_end_year_week: Column = field( + default_factory=lambda: Column("MonthEndYearWeek", True, 20000) + ) + year_month: Column = field(default_factory=lambda: Column("YearMonth", True, 21000)) + year_quarter: Column = field( + default_factory=lambda: Column("YearQuarter", True, 22000) + ) + month_start_day_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartDayOfQuarter", True, 23000) + ) + month_end_day_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndDayOfQuarter", True, 24000) + ) + month_start_day_of_year: Column = field( + default_factory=lambda: Column("MonthStartDayOfYear", True, 25000) + ) + month_end_day_of_year: Column = field( + default_factory=lambda: Column("MonthEndDayOfYear", True, 26000) + ) + month_start_week_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartWeekOfQuarter", True, 27000) + ) + month_end_week_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndWeekOfQuarter", True, 28000) + ) + month_start_week_of_year: Column = field( + default_factory=lambda: Column("MonthStartWeekOfYear", True, 29000) + ) + month_end_week_of_year: Column = field( + default_factory=lambda: Column("MonthEndWeekOfYear", True, 30000) + ) + month_of_quarter: Column = field( + default_factory=lambda: Column("MonthOfQuarter", True, 31000) + ) + quarter: Column = field(default_factory=lambda: Column("Quarter", True, 32000)) + days_in_month: Column = field( + default_factory=lambda: Column("DaysInMonth", True, 33000) + ) + days_in_quarter: Column = field( + default_factory=lambda: Column("DaysInQuarter", True, 34000) + ) + days_in_year: Column = field( + default_factory=lambda: Column("DaysInYear", True, 35000) + ) + current_month_flag: Column = field( + default_factory=lambda: Column("CurrentMonthFlag", True, 36000) + ) + prior_month_flag: Column = field( + default_factory=lambda: Column("PriorMonthFlag", True, 37000) + ) + next_month_flag: Column = field( + default_factory=lambda: Column("NextMonthFlag", True, 38000) + ) + current_quarter_flag: Column = field( + default_factory=lambda: Column("CurrentQuarterFlag", True, 39000) + ) + prior_quarter_flag: Column = field( + default_factory=lambda: Column("PriorQuarterFlag", True, 40000) + ) + next_quarter_flag: Column = field( + default_factory=lambda: Column("NextQuarterFlag", True, 41000) + ) + current_year_flag: Column = field( + default_factory=lambda: Column("CurrentYearFlag", True, 42000) + ) + prior_year_flag: Column = field( + default_factory=lambda: Column("PriorYearFlag", True, 43000) + ) + next_year_flag: Column = field( + default_factory=lambda: Column("NextYearFlag", True, 44000) + ) + first_day_of_month_flag: Column = field( + default_factory=lambda: Column("FirstDayOfMonthFlag", True, 45000) + ) + last_day_of_month_flag: Column = field( + default_factory=lambda: Column("LastDayOfMonthFlag", True, 46000) + ) + first_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("FirstDayOfQuarterFlag", True, 47000) + ) + last_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("LastDayOfQuarterFlag", True, 48000) + ) + first_day_of_year_flag: Column = field( + default_factory=lambda: Column("FirstDayOfYearFlag", True, 49000) + ) + last_day_of_year_flag: Column = field( + default_factory=lambda: Column("LastDayOfYearFlag", True, 50000) + ) + month_start_fraction_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartFractionOfQuarter", True, 51000) + ) + month_end_fraction_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndFractionOfQuarter", True, 52000) + ) + month_start_fraction_of_year: Column = field( + default_factory=lambda: Column("MonthStartFractionOfYear", True, 53000) + ) + month_end_fraction_of_year: Column = field( + default_factory=lambda: Column("MonthEndFractionOfYear", True, 54000) + ) + current_quarter_start: Column = field( + default_factory=lambda: Column("CurrentQuarterStart", True, 55000) + ) + current_quarter_end: Column = field( + default_factory=lambda: Column("CurrentQuarterEnd", True, 56000) + ) + current_year_start: Column = field( + default_factory=lambda: Column("CurrentYearStart", True, 57000) + ) + current_year_end: Column = field( + default_factory=lambda: Column("CurrentYearEnd", True, 58000) + ) + prior_month_start: Column = field( + default_factory=lambda: Column("PriorMonthStart", True, 59000) + ) + prior_month_end: Column = field( + default_factory=lambda: Column("PriorMonthEnd", True, 60000) + ) + prior_quarter_start: Column = field( + default_factory=lambda: Column("PriorQuarterStart", True, 61000) + ) + prior_quarter_end: Column = field( + default_factory=lambda: Column("PriorQuarterEnd", True, 62000) + ) + prior_year_start: Column = field( + default_factory=lambda: Column("PriorYearStart", True, 63000) + ) + prior_year_end: Column = field( + default_factory=lambda: Column("PriorYearEnd", True, 64000) + ) + next_month_start: Column = field( + default_factory=lambda: Column("NextMonthStart", True, 65000) + ) + next_month_end: Column = field( + default_factory=lambda: Column("NextMonthEnd", True, 66000) + ) + next_quarter_start: Column = field( + default_factory=lambda: Column("NextQuarterStart", True, 67000) + ) + next_quarter_end: Column = field( + default_factory=lambda: Column("NextQuarterEnd", True, 68000) + ) + next_year_start: Column = field( + default_factory=lambda: Column("NextYearStart", True, 69000) + ) + next_year_end: Column = field( + default_factory=lambda: Column("NextYearEnd", True, 70000) + ) + month_start_quarterly_burnup: Column = field( + default_factory=lambda: Column("MonthStartQuarterlyBurnup", True, 71000) + ) + month_end_quarterly_burnup: Column = field( + default_factory=lambda: Column("MonthEndQuarterlyBurnup", True, 72000) + ) + month_start_yearly_burnup: Column = field( + default_factory=lambda: Column("MonthStartYearlyBurnup", True, 73000) + ) + month_end_yearly_burnup: Column = field( + default_factory=lambda: Column("MonthEndYearlyBurnup", True, 74000) + ) + + def __post_init__(self): + field_names = (f.name for f in fields(self)) + cols = [self.__dict__[name] for name in field_names] + assert ( + self.month_start_key.include + ), "DimFiscalMonthColumns.month_start_key must be included, as it is part of the table key." + assert ( + self.month_end_key.include + ), "DimFiscalMonthColumns.month_end_key must be included, as it is part of the table key." + _assert_no_duplicate_colnames(DimFiscalMonthColumns.__name__, cols) + _assert_no_duplicate_col_sortkeys(DimFiscalMonthColumns.__name__, cols) + + +@dataclass(frozen=True) +class DimFiscalMonthConfig: + table_schema: str = "dbo" + table_name: str = "DimFiscalMonth" + columns: DimFiscalMonthColumns = field( + default_factory=lambda: DimFiscalMonthColumns() + ) + column_factory: Callable[[str, Column], Column] = None + + def __post_init__(self): + if self.column_factory is not None: + col_fields = fields(self.columns) + new_cols: dict[str, Column] = {} + for f in col_fields: + col: Column = self.columns.__dict__[f.name] + new_col = self.column_factory(f.name, col) + assert isinstance( + new_col, Column + ), f"column_factory returned a value that was not a column. This is not allowed. Value: {new_col}" + new_cols[f.name] = new_col + object.__setattr__(self, "columns", DimFiscalMonthColumns(**new_cols)) + + +@dataclass(frozen=True) +class DimCalendarMonthColumns: + month_start_key: Column = field( + default_factory=lambda: Column("MonthStartKey", True, 1000) + ) + month_end_key: Column = field( + default_factory=lambda: Column("MonthEndKey", True, 2000) + ) + month_start_date: Column = field( + default_factory=lambda: Column("MonthStartDate", True, 3000) + ) + month_end_date: Column = field( + default_factory=lambda: Column("MonthEndDate", True, 4000) + ) + month_start_iso_date_name: Column = field( + default_factory=lambda: Column("MonthStartISODateName", True, 5000) + ) + month_end_iso_date_name: Column = field( + default_factory=lambda: Column("MonthEndISODateName", True, 6000) + ) + month_start_iso_week_date_name: Column = field( + default_factory=lambda: Column("MonthStartISOWeekDateName", True, 7000) + ) + month_end_iso_week_date_name: Column = field( + default_factory=lambda: Column("MonthEndISOWeekDateName", True, 8000) + ) + month_start_american_date_name: Column = field( + default_factory=lambda: Column("MonthStartAmericanDateName", True, 9000) + ) + month_end_american_date_name: Column = field( + default_factory=lambda: Column("MonthEndAmericanDateName", True, 10000) + ) + month_name: Column = field(default_factory=lambda: Column("MonthName", True, 11000)) + month_abbrev: Column = field( + default_factory=lambda: Column("MonthAbbrev", True, 12000) + ) + month_start_year_week_name: Column = field( + default_factory=lambda: Column("MonthStartYearWeekName", True, 13000) + ) + month_end_year_week_name: Column = field( + default_factory=lambda: Column("MonthEndYearWeekName", True, 14000) + ) + year_month_name: Column = field( + default_factory=lambda: Column("YearMonthName", True, 15000) + ) + month_year_name: Column = field( + default_factory=lambda: Column("MonthYearName", True, 16000) + ) + year_quarter_name: Column = field( + default_factory=lambda: Column("YearQuarterName", True, 17000) + ) + year: Column = field(default_factory=lambda: Column("Year", True, 18000)) + month_start_year_week: Column = field( + default_factory=lambda: Column("MonthStartYearWeek", True, 19000) + ) + month_end_year_week: Column = field( + default_factory=lambda: Column("MonthEndYearWeek", True, 20000) + ) + year_month: Column = field(default_factory=lambda: Column("YearMonth", True, 21000)) + year_quarter: Column = field( + default_factory=lambda: Column("YearQuarter", True, 22000) + ) + month_start_day_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartDayOfQuarter", True, 23000) + ) + month_end_day_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndDayOfQuarter", True, 24000) + ) + month_start_day_of_year: Column = field( + default_factory=lambda: Column("MonthStartDayOfYear", True, 25000) + ) + month_end_day_of_year: Column = field( + default_factory=lambda: Column("MonthEndDayOfYear", True, 26000) + ) + month_start_week_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartWeekOfQuarter", True, 27000) + ) + month_end_week_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndWeekOfQuarter", True, 28000) + ) + month_start_week_of_year: Column = field( + default_factory=lambda: Column("MonthStartWeekOfYear", True, 29000) + ) + month_end_week_of_year: Column = field( + default_factory=lambda: Column("MonthEndWeekOfYear", True, 30000) + ) + month_of_quarter: Column = field( + default_factory=lambda: Column("MonthOfQuarter", True, 31000) + ) + quarter: Column = field(default_factory=lambda: Column("Quarter", True, 32000)) + days_in_month: Column = field( + default_factory=lambda: Column("DaysInMonth", True, 33000) + ) + days_in_quarter: Column = field( + default_factory=lambda: Column("DaysInQuarter", True, 34000) + ) + days_in_year: Column = field( + default_factory=lambda: Column("DaysInYear", True, 35000) + ) + current_month_flag: Column = field( + default_factory=lambda: Column("CurrentMonthFlag", True, 36000) + ) + prior_month_flag: Column = field( + default_factory=lambda: Column("PriorMonthFlag", True, 37000) + ) + next_month_flag: Column = field( + default_factory=lambda: Column("NextMonthFlag", True, 38000) + ) + current_quarter_flag: Column = field( + default_factory=lambda: Column("CurrentQuarterFlag", True, 39000) + ) + prior_quarter_flag: Column = field( + default_factory=lambda: Column("PriorQuarterFlag", True, 40000) + ) + next_quarter_flag: Column = field( + default_factory=lambda: Column("NextQuarterFlag", True, 41000) + ) + current_year_flag: Column = field( + default_factory=lambda: Column("CurrentYearFlag", True, 42000) + ) + prior_year_flag: Column = field( + default_factory=lambda: Column("PriorYearFlag", True, 43000) + ) + next_year_flag: Column = field( + default_factory=lambda: Column("NextYearFlag", True, 44000) + ) + first_day_of_month_flag: Column = field( + default_factory=lambda: Column("FirstDayOfMonthFlag", True, 45000) + ) + last_day_of_month_flag: Column = field( + default_factory=lambda: Column("LastDayOfMonthFlag", True, 46000) + ) + first_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("FirstDayOfQuarterFlag", True, 47000) + ) + last_day_of_quarter_flag: Column = field( + default_factory=lambda: Column("LastDayOfQuarterFlag", True, 48000) + ) + first_day_of_year_flag: Column = field( + default_factory=lambda: Column("FirstDayOfYearFlag", True, 49000) + ) + last_day_of_year_flag: Column = field( + default_factory=lambda: Column("LastDayOfYearFlag", True, 50000) + ) + month_start_fraction_of_quarter: Column = field( + default_factory=lambda: Column("MonthStartFractionOfQuarter", True, 51000) + ) + month_end_fraction_of_quarter: Column = field( + default_factory=lambda: Column("MonthEndFractionOfQuarter", True, 52000) + ) + month_start_fraction_of_year: Column = field( + default_factory=lambda: Column("MonthStartFractionOfYear", True, 53000) + ) + month_end_fraction_of_year: Column = field( + default_factory=lambda: Column("MonthEndFractionOfYear", True, 54000) + ) + current_quarter_start: Column = field( + default_factory=lambda: Column("CurrentQuarterStart", True, 55000) + ) + current_quarter_end: Column = field( + default_factory=lambda: Column("CurrentQuarterEnd", True, 56000) + ) + current_year_start: Column = field( + default_factory=lambda: Column("CurrentYearStart", True, 57000) + ) + current_year_end: Column = field( + default_factory=lambda: Column("CurrentYearEnd", True, 58000) + ) + prior_month_start: Column = field( + default_factory=lambda: Column("PriorMonthStart", True, 59000) + ) + prior_month_end: Column = field( + default_factory=lambda: Column("PriorMonthEnd", True, 60000) + ) + prior_quarter_start: Column = field( + default_factory=lambda: Column("PriorQuarterStart", True, 61000) + ) + prior_quarter_end: Column = field( + default_factory=lambda: Column("PriorQuarterEnd", True, 62000) + ) + prior_year_start: Column = field( + default_factory=lambda: Column("PriorYearStart", True, 63000) + ) + prior_year_end: Column = field( + default_factory=lambda: Column("PriorYearEnd", True, 64000) + ) + next_month_start: Column = field( + default_factory=lambda: Column("NextMonthStart", True, 65000) + ) + next_month_end: Column = field( + default_factory=lambda: Column("NextMonthEnd", True, 66000) + ) + next_quarter_start: Column = field( + default_factory=lambda: Column("NextQuarterStart", True, 67000) + ) + next_quarter_end: Column = field( + default_factory=lambda: Column("NextQuarterEnd", True, 68000) + ) + next_year_start: Column = field( + default_factory=lambda: Column("NextYearStart", True, 69000) + ) + next_year_end: Column = field( + default_factory=lambda: Column("NextYearEnd", True, 70000) + ) + month_start_quarterly_burnup: Column = field( + default_factory=lambda: Column("MonthStartQuarterlyBurnup", True, 71000) + ) + month_end_quarterly_burnup: Column = field( + default_factory=lambda: Column("MonthEndQuarterlyBurnup", True, 72000) + ) + month_start_yearly_burnup: Column = field( + default_factory=lambda: Column("MonthStartYearlyBurnup", True, 73000) + ) + month_end_yearly_burnup: Column = field( + default_factory=lambda: Column("MonthEndYearlyBurnup", True, 74000) + ) + + def __post_init__(self): + field_names = (f.name for f in fields(self)) + cols = [self.__dict__[name] for name in field_names] + assert ( + self.month_start_key.include + ), "DimCalendarMonthColumns.month_start_key must be included, as it is part of the table key." + assert ( + self.month_end_key.include + ), "DimCalendarMonthColumns.month_end_key must be included, as it is part of the table key." + _assert_no_duplicate_colnames(DimCalendarMonthColumns.__name__, cols) + _assert_no_duplicate_col_sortkeys(DimCalendarMonthColumns.__name__, cols) + + +@dataclass(frozen=True) +class DimCalendarMonthConfig: + table_schema: str = "dbo" + table_name: str = "DimCalendarMonth" + columns: DimCalendarMonthColumns = field( + default_factory=lambda: DimCalendarMonthColumns() + ) + column_factory: Callable[[str, Column], Column] = None + + def __post_init__(self): + if self.column_factory is not None: + col_fields = fields(self.columns) + new_cols: dict[str, Column] = {} + for f in col_fields: + col: Column = self.columns.__dict__[f.name] + new_col = self.column_factory(f.name, col) + assert isinstance( + new_col, Column + ), f"column_factory returned a value that was not a column. This is not allowed. Value: {new_col}" + new_cols[f.name] = new_col + object.__setattr__(self, "columns", DimCalendarMonthColumns(**new_cols)) + + +@dataclass +class Config: + output_dir: Path = field(default_factory=lambda: Path("./output")) + clear_output_dir: bool = False + date_range: DateRange = field(default_factory=DateRange) + fiscal: FiscalConfig = field(default_factory=FiscalConfig) + time_zone: tzinfo = field(default_factory=lambda: timezone("US/Mountain")) + holidays: HolidayConfig = field(default_factory=HolidayConfig) + dim_date: DimDateConfig = field(default_factory=DimDateConfig) + dim_fiscal_month: DimFiscalMonthConfig = field(default_factory=DimFiscalMonthConfig) + dim_calendar_month: DimCalendarMonthConfig = field( + default_factory=DimCalendarMonthConfig + ) + + +class ConfigVersion(Enum): + V1 = "v1" + + +def config_factory(version: ConfigVersion): + if version == ConfigVersion.V1: + return Config() + raise TypeError("version must be a valid instance of ConfigVersion") diff --git a/awesome_date_dimension/tsql.py b/awesome_date_dimension/tsql.py new file mode 100644 index 0000000..f47f619 --- /dev/null +++ b/awesome_date_dimension/tsql.py @@ -0,0 +1,392 @@ +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Callable, Iterable + +from ._internal.tsql.dim_calendar_month_constraints_template import ( + dim_calendar_month_constraints_template, +) +from ._internal.tsql.dim_calendar_month_insert_template import ( + dim_calendar_month_insert_template, +) +from ._internal.tsql.dim_calendar_month_refresh_template import ( + dim_calendar_month_refresh_template, +) +from ._internal.tsql.dim_date_constraints_template import dim_date_constraints_template +from ._internal.tsql.dim_date_insert_template import dim_date_insert_template +from ._internal.tsql.dim_date_refresh_template import dim_date_refresh_template +from ._internal.tsql.dim_fiscal_month_constraints_template import ( + dim_fiscal_month_constraints_template, +) +from ._internal.tsql.dim_fiscal_month_insert_template import ( + dim_fiscal_month_insert_template, +) +from ._internal.tsql.dim_fiscal_month_refresh_template import ( + dim_fiscal_month_refresh_template, +) +from ._internal.tsql.holiday_types_constraints_template import ( + holiday_types_constraints_template, +) +from ._internal.tsql.holiday_types_insert_template import holiday_types_insert_template +from ._internal.tsql.holidays_constraints_template import holidays_constraints_template +from ._internal.tsql.holidays_insert_template import holidays_insert_template +from ._internal.tsql.table_setup_template import table_setup_template +from ._internal.tsql.tsql_columns import ( + TSQLColumn, + TSQLDimCalendarMonthColumns, + TSQLDimDateColumns, + TSQLDimFiscalMonthColumns, +) +from .config import Config + + +class TSQLGenerator: + def __init__(self, config: Config): + self._config = config + self._dim_date_columns = TSQLDimDateColumns(config.dim_date.columns) + self._dim_fiscal_month_columns = TSQLDimFiscalMonthColumns( + config.dim_fiscal_month.columns + ) + self._dim_calendar_month_columns = TSQLDimCalendarMonthColumns( + config.dim_calendar_month.columns + ) + + self._dim_date_columns.add_holiday_columns(config.holidays) + self._dim_fiscal_month_columns.add_holiday_columns(config.holidays) + self._dim_calendar_month_columns.add_holiday_columns(config.holidays) + + dir_exists = config.output_dir.exists() + if dir_exists and config.clear_output_dir: + shutil.rmtree(config.output_dir) + config.output_dir.mkdir() + elif not dir_exists: + config.output_dir.mkdir() + + def generate_scripts(self) -> None: + folder_no = 0 + folder_no = self._generate_setup_scripts(folder_no) + folder_no = self._generate_build_scripts(folder_no) + folder_no = self._generate_refresh_procs(folder_no) + folder_no = self._generate_table_constraints(folder_no) + + def _generate_setup_scripts(self, folder_no: int) -> int: + file_no = 0 + base_path = self._generate_folder_path(folder_no, "setup") + if not base_path.exists(): + base_path.mkdir() + file_no = self._generate_dim_date_setup_scripts(file_no, base_path) + file_no = self._generate_dim_fiscal_month_setup_scripts(file_no, base_path) + file_no = self._generate_dim_calendar_month_setup_scripts(file_no, base_path) + file_no = self._generate_holiday_setup_scripts(file_no, base_path) + return folder_no + 1 + + def _generate_dim_date_setup_scripts(self, file_no: int, base_path: Path) -> int: + cfg = self._config.dim_date + table_gen = lambda config: TSQLGenerator._get_table_definition( + cfg.table_schema, cfg.table_name, self._dim_date_columns + ) + return TSQLGenerator._generate_file( + file_no, cfg.table_name, base_path, self._config, table_gen + ) + + def _generate_dim_fiscal_month_setup_scripts( + self, file_no: int, base_path: Path + ) -> int: + cfg = self._config.dim_fiscal_month + table_gen = lambda config: TSQLGenerator._get_table_definition( + cfg.table_schema, cfg.table_name, self._dim_fiscal_month_columns + ) + return TSQLGenerator._generate_file( + file_no, cfg.table_name, base_path, self._config, table_gen + ) + + def _generate_dim_calendar_month_setup_scripts( + self, file_no: int, base_path: Path + ) -> int: + cfg = self._config.dim_calendar_month + table_gen = lambda config: TSQLGenerator._get_table_definition( + cfg.table_schema, cfg.table_name, self._dim_calendar_month_columns + ) + return TSQLGenerator._generate_file( + file_no, cfg.table_name, base_path, self._config, table_gen + ) + + def _generate_holiday_setup_scripts(self, file_no: int, base_path: Path) -> int: + if self._config.holidays.generate_holidays: + # Honestly, not worth it to create templates for these since they're so simple. + # I'll take points off for "bad software", I suppose. + + # Holiday Types + ht_tabledef = [ + f"CREATE TABLE {self._config.holidays.holiday_types_schema_name}.{self._config.holidays.holiday_types_table_name} (", + f" {self._config.holidays.holiday_types_columns.holiday_type_key.name} int IDENTITY(1,1) NOT NULL,", + f" {self._config.holidays.holiday_types_columns.holiday_type_name.name} varchar(255) UNIQUE NOT NULL", + ");", + ] + file_path = base_path / TSQLGenerator._get_sql_filename( + file_no, self._config.holidays.holiday_types_table_name + ) + TSQLGenerator._assert_filepath_available(file_path) + file_path.write_text("\n".join(ht_tabledef)) + + file_no += 1 + + # Holidays + h_tabledef = [ + f"CREATE TABLE {self._config.holidays.holidays_schema_name}.{self._config.holidays.holidays_table_name} (", + f" {self._config.holidays.holidays_columns.date_key.name} int NOT NULL,", + f" {self._config.holidays.holidays_columns.holiday_name.name} varchar(255) NOT NULL,", + f" {self._config.holidays.holidays_columns.holiday_type_key.name} int NOT NULL", + ");", + ] + file_path = base_path / TSQLGenerator._get_sql_filename( + file_no, self._config.holidays.holidays_table_name + ) + TSQLGenerator._assert_filepath_available(file_path) + file_path.write_text("\n".join(h_tabledef)) + return file_no + 1 + return file_no + + def _generate_build_scripts(self, folder_no: int) -> int: + file_no = 0 + base_path = self._generate_folder_path(folder_no, "initial-build") + if not base_path.exists(): + base_path.mkdir() + file_no = self._generate_holiday_build_scripts(file_no, base_path) + file_no = self._generate_dim_date_build_scripts(file_no, base_path) + file_no = self._generate_dim_fiscal_month_build_scripts(file_no, base_path) + file_no = self._generate_dim_calendar_month_build_scripts(file_no, base_path) + return folder_no + 1 + + def _generate_holiday_type_build_script(self, file_no: int, base_path: Path) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.holidays.holiday_types_table_name, + base_path, + self._config, + holiday_types_insert_template, + ) + + def _generate_holidays_build_script(self, file_no: int, base_path: Path) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.holidays.holidays_table_name, + base_path, + self._config, + holidays_insert_template, + ) + + def _generate_holiday_build_scripts(self, file_no: int, base_path: Path) -> int: + if self._config.holidays.generate_holidays: + file_no = self._generate_holiday_type_build_script(file_no, base_path) + file_no = self._generate_holidays_build_script(file_no, base_path) + return file_no + + def _generate_dim_date_build_scripts(self, file_no: int, base_path: Path) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_date.table_name, + base_path, + self._config, + dim_date_insert_template, + columns=self._dim_date_columns, + ) + + def _generate_dim_fiscal_month_build_scripts( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_fiscal_month.table_name, + base_path, + self._config, + dim_fiscal_month_insert_template, + columns=self._dim_fiscal_month_columns, + ) + + def _generate_dim_calendar_month_build_scripts( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_calendar_month.table_name, + base_path, + self._config, + dim_calendar_month_insert_template, + columns=self._dim_calendar_month_columns, + ) + + def _generate_refresh_procs(self, folder_no: int) -> int: + file_no = 0 + base_path = self._generate_folder_path(folder_no, "refresh-procs") + if not base_path.exists(): + base_path.mkdir() + file_no = self._generate_dim_date_refresh_procs(file_no, base_path) + file_no = self._generate_dim_fiscal_month_refresh_procs(file_no, base_path) + file_no = self._generate_dim_calendar_month_refresh_procs(file_no, base_path) + return folder_no + 1 + + def _generate_dim_date_refresh_procs(self, file_no: int, base_path: Path) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_date.table_name, + base_path, + self._config, + dim_date_refresh_template, + columns=self._dim_date_columns, + ) + + def _generate_dim_fiscal_month_refresh_procs( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_fiscal_month.table_name, + base_path, + self._config, + dim_fiscal_month_refresh_template, + columns=self._dim_fiscal_month_columns, + ) + + def _generate_dim_calendar_month_refresh_procs( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_calendar_month.table_name, + base_path, + self._config, + dim_calendar_month_refresh_template, + columns=self._dim_calendar_month_columns, + ) + + def _generate_table_constraints(self, folder_no: int) -> int: + file_no = 0 + base_path = self._generate_folder_path(folder_no, "table-constraints") + if not base_path.exists(): + base_path.mkdir() + file_no = self._generate_dim_date_table_constraints(file_no, base_path) + file_no = self._generate_dim_fiscal_month_table_constraints(file_no, base_path) + file_no = self._generate_dim_calendar_month_table_constraints( + file_no, base_path + ) + file_no = self._generate_holiday_table_constraints(file_no, base_path) + return folder_no + 1 + + def _generate_dim_date_table_constraints( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_date.table_name, + base_path, + self._config, + dim_date_constraints_template, + ) + + def _generate_dim_fiscal_month_table_constraints( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_fiscal_month.table_name, + base_path, + self._config, + dim_fiscal_month_constraints_template, + ) + + def _generate_dim_calendar_month_table_constraints( + self, file_no: int, base_path: Path + ) -> int: + return TSQLGenerator._generate_file( + file_no, + self._config.dim_calendar_month.table_name, + base_path, + self._config, + dim_calendar_month_constraints_template, + ) + + def _generate_holiday_table_constraints(self, file_no: int, base_path: Path) -> int: + file_no = TSQLGenerator._generate_file( + file_no, + self._config.holidays.holiday_types_table_name, + base_path, + self._config, + holiday_types_constraints_template, + ) + return TSQLGenerator._generate_file( + file_no, + self._config.holidays.holidays_table_name, + base_path, + self._config, + holidays_constraints_template, + ) + + def _generate_folder_path(self, folder_no: int, name: str) -> Path: + # note: Internal use. Does not attempt to sanitize folder name. + if folder_no < 0 or folder_no > 99: + raise ValueError("folder_no must be between 0 and 99 inclusive.") + + return self._config.output_dir / f"{str(folder_no).zfill(2)}-{name}" + + @staticmethod + def _get_sql_filename(file_no: int, file_name: str): + if file_no < 0 or file_no > 99: + raise ValueError("file_no must be between 0 and 99 inclusive") + return f"{str(file_no).zfill(2)}-{file_name}.sql" + + @staticmethod + def _generate_file( + file_no: int, + table_name: str, + base_path: Path, + config: Config, + script_gen_func: Callable[[Config], str], + **kwargs, + ) -> int: + scriptdef = script_gen_func(config, **kwargs) + file_path = base_path / TSQLGenerator._get_sql_filename(file_no, table_name) + TSQLGenerator._assert_filepath_available(file_path) + file_path.write_text(scriptdef) + return file_no + 1 + + @staticmethod + def _get_constraint_str(constraint_def: str) -> str: + return f"CONSTRAINT {constraint_def} " if constraint_def is not None else "" + + @staticmethod + def _get_column_def(tsql_column: TSQLColumn) -> str: + return f'{tsql_column.name} {tsql_column.sql_datatype} {TSQLGenerator._get_constraint_str(tsql_column.constraint)}{"NULL" if tsql_column.nullable else "NOT NULL"}' + + @staticmethod + def _get_table_definition( + table_schema: str, table_name: str, columns: Iterable[TSQLColumn] + ) -> str: + column_def = [] + for col in columns: + column_def.append(TSQLGenerator._get_column_def(col)) + return table_setup_template(table_schema, table_name, column_def) + + @staticmethod + def _generate_table_setup_scripts( + table_schema: str, + table_name: str, + columns: Iterable[TSQLColumn], + file_path: Path, + ): + table_def = TSQLGenerator._get_table_definition( + table_name, table_schema, columns + ) + TSQLGenerator._assert_filepath_available(file_path) + file_path.write_text(table_def) + + @staticmethod + def _assert_filepath_available(path: Path) -> None: + if path.exists(): + TSQLGenerator._raise_fileexistserror(path) + + @staticmethod + def _raise_fileexistserror(file_name: str) -> None: + raise FileExistsError( + f"The file {file_name} already exists. Please delete it and try again." + ) diff --git a/t-sql/01-setup/01-DimDateTableDef.sql b/output/t-sql/01-setup/01-DimDateTableDef.sql similarity index 100% rename from t-sql/01-setup/01-DimDateTableDef.sql rename to output/t-sql/01-setup/01-DimDateTableDef.sql diff --git a/t-sql/01-setup/02-DimFiscalMonth.sql b/output/t-sql/01-setup/02-DimFiscalMonth.sql similarity index 100% rename from t-sql/01-setup/02-DimFiscalMonth.sql rename to output/t-sql/01-setup/02-DimFiscalMonth.sql diff --git a/t-sql/01-setup/03-DimCalendarMonth.sql b/output/t-sql/01-setup/03-DimCalendarMonth.sql similarity index 100% rename from t-sql/01-setup/03-DimCalendarMonth.sql rename to output/t-sql/01-setup/03-DimCalendarMonth.sql diff --git a/t-sql/01-setup/04-HolidayTypesTableDef.sql b/output/t-sql/01-setup/04-HolidayTypesTableDef.sql similarity index 100% rename from t-sql/01-setup/04-HolidayTypesTableDef.sql rename to output/t-sql/01-setup/04-HolidayTypesTableDef.sql diff --git a/t-sql/01-setup/05-HolidaysTableDef.sql b/output/t-sql/01-setup/05-HolidaysTableDef.sql similarity index 100% rename from t-sql/01-setup/05-HolidaysTableDef.sql rename to output/t-sql/01-setup/05-HolidaysTableDef.sql diff --git a/t-sql/02-initial-build/01-InsertHolidayTypes.sql b/output/t-sql/02-initial-build/01-InsertHolidayTypes.sql similarity index 100% rename from t-sql/02-initial-build/01-InsertHolidayTypes.sql rename to output/t-sql/02-initial-build/01-InsertHolidayTypes.sql diff --git a/t-sql/02-initial-build/02-InsertHolidays.sql b/output/t-sql/02-initial-build/02-InsertHolidays.sql similarity index 100% rename from t-sql/02-initial-build/02-InsertHolidays.sql rename to output/t-sql/02-initial-build/02-InsertHolidays.sql diff --git a/t-sql/02-initial-build/03-InsertDimDateRecords.sql b/output/t-sql/02-initial-build/03-InsertDimDateRecords.sql similarity index 99% rename from t-sql/02-initial-build/03-InsertDimDateRecords.sql rename to output/t-sql/02-initial-build/03-InsertDimDateRecords.sql index 1879ace..edb0cad 100644 --- a/t-sql/02-initial-build/03-InsertDimDateRecords.sql +++ b/output/t-sql/02-initial-build/03-InsertDimDateRecords.sql @@ -292,7 +292,7 @@ BaseDatesThird AS ( FiscalYearEnd ), - FiscalPeriodYearReferenceTotday = IIF( + FiscalPeriodYearReferenceToday = IIF( @FiscalYearPeriodEndMatchesCalendar = 0, FiscalYearStartToday, FiscalYearEndToday diff --git a/t-sql/03-refresh-procs/01-sp_build_DimDate.sql b/output/t-sql/03-refresh-procs/01-sp_build_DimDate.sql similarity index 100% rename from t-sql/03-refresh-procs/01-sp_build_DimDate.sql rename to output/t-sql/03-refresh-procs/01-sp_build_DimDate.sql diff --git a/t-sql/03-refresh-procs/02-sp_build_DimFiscalMonth.sql b/output/t-sql/03-refresh-procs/02-sp_build_DimFiscalMonth.sql similarity index 100% rename from t-sql/03-refresh-procs/02-sp_build_DimFiscalMonth.sql rename to output/t-sql/03-refresh-procs/02-sp_build_DimFiscalMonth.sql diff --git a/t-sql/03-refresh-procs/03-sp_build_DimCalendarMonth.sql b/output/t-sql/03-refresh-procs/03-sp_build_DimCalendarMonth.sql similarity index 100% rename from t-sql/03-refresh-procs/03-sp_build_DimCalendarMonth.sql rename to output/t-sql/03-refresh-procs/03-sp_build_DimCalendarMonth.sql diff --git a/t-sql/04-table-constraints/01-DimDateConstraints.sql b/output/t-sql/04-table-constraints/01-DimDateConstraints.sql similarity index 100% rename from t-sql/04-table-constraints/01-DimDateConstraints.sql rename to output/t-sql/04-table-constraints/01-DimDateConstraints.sql diff --git a/t-sql/04-table-constraints/02-DimFiscalMonth.sql b/output/t-sql/04-table-constraints/02-DimFiscalMonth.sql similarity index 100% rename from t-sql/04-table-constraints/02-DimFiscalMonth.sql rename to output/t-sql/04-table-constraints/02-DimFiscalMonth.sql diff --git a/t-sql/04-table-constraints/03-DimCalendarMonth.sql b/output/t-sql/04-table-constraints/03-DimCalendarMonth.sql similarity index 100% rename from t-sql/04-table-constraints/03-DimCalendarMonth.sql rename to output/t-sql/04-table-constraints/03-DimCalendarMonth.sql diff --git a/t-sql/04-table-constraints/04-manual_HolidayTypes.sql b/output/t-sql/04-table-constraints/04-manual_HolidayTypes.sql similarity index 100% rename from t-sql/04-table-constraints/04-manual_HolidayTypes.sql rename to output/t-sql/04-table-constraints/04-manual_HolidayTypes.sql diff --git a/t-sql/04-table-constraints/05-manual_Holidays.sql b/output/t-sql/04-table-constraints/05-manual_Holidays.sql similarity index 100% rename from t-sql/04-table-constraints/05-manual_Holidays.sql rename to output/t-sql/04-table-constraints/05-manual_Holidays.sql diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5a3c46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..c98f422 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +isort +black +pre-commit \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7529c03 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytz==2021.3 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..78f8647 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = awesome_date_dimension +version = 0.0.1 +author = S. Elliott Johnson +author_email = sejohnson@torchcloudconsulting.com +description = Generate an awesome date dimension in your chosen format, complete with many common options. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/tcc-sejohnson/awesome-date-dimension +project_urls = + Bug Tracker = https://github.com/tcc-sejohnson/awesome-date-dimension/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.8 + +[options.packages.find] +where = src \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/files/config_factory_ConfigVersion.V1.pickle b/tests/files/config_factory_ConfigVersion.V1.pickle new file mode 100644 index 0000000..7316446 Binary files /dev/null and b/tests/files/config_factory_ConfigVersion.V1.pickle differ diff --git a/tests/files/default_company_holidays.pickle b/tests/files/default_company_holidays.pickle new file mode 100644 index 0000000..241aa06 Binary files /dev/null and b/tests/files/default_company_holidays.pickle differ diff --git a/tests/files/default_config.pickle b/tests/files/default_config.pickle new file mode 100644 index 0000000..7316446 Binary files /dev/null and b/tests/files/default_config.pickle differ diff --git a/tests/files/default_dim_calendar_month_columns.pickle b/tests/files/default_dim_calendar_month_columns.pickle new file mode 100644 index 0000000..d2dbce9 Binary files /dev/null and b/tests/files/default_dim_calendar_month_columns.pickle differ diff --git a/tests/files/default_dim_calendar_month_config.pickle b/tests/files/default_dim_calendar_month_config.pickle new file mode 100644 index 0000000..e1ef1a7 Binary files /dev/null and b/tests/files/default_dim_calendar_month_config.pickle differ diff --git a/tests/files/default_dim_date_columns.pickle b/tests/files/default_dim_date_columns.pickle new file mode 100644 index 0000000..95a3f58 Binary files /dev/null and b/tests/files/default_dim_date_columns.pickle differ diff --git a/tests/files/default_dim_date_config.pickle b/tests/files/default_dim_date_config.pickle new file mode 100644 index 0000000..4d4e605 Binary files /dev/null and b/tests/files/default_dim_date_config.pickle differ diff --git a/tests/files/default_dim_fiscal_month_columns.pickle b/tests/files/default_dim_fiscal_month_columns.pickle new file mode 100644 index 0000000..be18872 Binary files /dev/null and b/tests/files/default_dim_fiscal_month_columns.pickle differ diff --git a/tests/files/default_dim_fiscal_month_config.pickle b/tests/files/default_dim_fiscal_month_config.pickle new file mode 100644 index 0000000..795f63c Binary files /dev/null and b/tests/files/default_dim_fiscal_month_config.pickle differ diff --git a/tests/files/default_holiday_config.pickle b/tests/files/default_holiday_config.pickle new file mode 100644 index 0000000..1f58d0c Binary files /dev/null and b/tests/files/default_holiday_config.pickle differ diff --git a/tests/files/default_holiday_types_columns.pickle b/tests/files/default_holiday_types_columns.pickle new file mode 100644 index 0000000..7422dcc Binary files /dev/null and b/tests/files/default_holiday_types_columns.pickle differ diff --git a/tests/files/default_holidays_columns.pickle b/tests/files/default_holidays_columns.pickle new file mode 100644 index 0000000..584e993 Binary files /dev/null and b/tests/files/default_holidays_columns.pickle differ diff --git a/tests/files/default_us_public_holidays.pickle b/tests/files/default_us_public_holidays.pickle new file mode 100644 index 0000000..ad789a4 Binary files /dev/null and b/tests/files/default_us_public_holidays.pickle differ diff --git a/tests/test_config/__init__.py b/tests/test_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py new file mode 100644 index 0000000..3e9b999 --- /dev/null +++ b/tests/test_config/test_config.py @@ -0,0 +1,16 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Config + + +class TestConfig(unittest.TestCase): + def test_defaults_are_immutable(self): + with open("./tests/files/default_config.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(Config(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_config_factory.py b/tests/test_config/test_config_factory.py new file mode 100644 index 0000000..1bf3285 --- /dev/null +++ b/tests/test_config/test_config_factory.py @@ -0,0 +1,27 @@ +import pickle +import unittest + +from awesome_date_dimension.config import ConfigVersion, config_factory + + +class TestConfigFactory(unittest.TestCase): + def test_invalid_version_raises_typeerror(self): + with self.assertRaises(TypeError): + config_factory("v-1") + + def test_defaults_are_immutable(self): + for version in ConfigVersion: + template = ( + "Defaults for a Config version have changed or were never stored. Did you forget to pickle " + "defaults for a new version? version={version}" + ) + with self.subTest(template.format(version=version)): + with open( + f"./tests/files/config_factory_{version}.pickle", "rb" + ) as file: + saved_defaults = pickle.load(file) + self.assertEqual(config_factory(version), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_daterange.py b/tests/test_config/test_daterange.py new file mode 100644 index 0000000..a2c580f --- /dev/null +++ b/tests/test_config/test_daterange.py @@ -0,0 +1,25 @@ +import unittest +from datetime import datetime + +from awesome_date_dimension.config import DateRange + + +class TestDateRange(unittest.TestCase): + def test_failure_when_less_than_0_years(self): + with self.assertRaises(AssertionError): + DateRange(datetime.fromisoformat("2000-01-01").date(), -1) + + def test_failure_when_0_years(self): + with self.assertRaises(AssertionError): + DateRange(datetime.fromisoformat("2000-01-01").date(), 0) + + def test_defaults(self): + default_range = DateRange() + self.assertEqual( + default_range.start_date, datetime.fromisoformat("2000-01-01").date() + ) + self.assertEqual(default_range.num_years, 100) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimcalendarmonthcolumns.py b/tests/test_config/test_dimcalendarmonthcolumns.py new file mode 100644 index 0000000..a86e625 --- /dev/null +++ b/tests/test_config/test_dimcalendarmonthcolumns.py @@ -0,0 +1,41 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Column, DimCalendarMonthColumns + + +class TestDimCalendarMonthColumns(unittest.TestCase): + def test_failure_when_start_key_column_is_excluded(self): + with self.assertRaises(AssertionError): + DimCalendarMonthColumns( + month_start_key=Column("MonthStartKey", False, 1000) + ) + + def test_failure_when_end_key_column_is_excluded(self): + with self.assertRaises(AssertionError): + DimCalendarMonthColumns(month_end_key=Column("MonthEndKey", False, 1000)) + + def test_failure_when_duplicate_colnames(self): + with self.assertRaises(AssertionError): + DimCalendarMonthColumns( + Column("Dupe", True, 1000), Column("Dupe", True, 2000) + ) + + def test_failure_when_duplicate_sortkeys(self): + with self.assertRaises(AssertionError): + DimCalendarMonthColumns( + Column("DateKey", True, 1000), + Column("SomethingElse", True, 1000), + ) + + def test_defaults_are_immutable(self): + with open( + "./tests/files/default_dim_calendar_month_columns.pickle", "rb" + ) as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimCalendarMonthColumns(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimcalendarmonthconfig.py b/tests/test_config/test_dimcalendarmonthconfig.py new file mode 100644 index 0000000..6b54bbc --- /dev/null +++ b/tests/test_config/test_dimcalendarmonthconfig.py @@ -0,0 +1,65 @@ +import pickle +import unittest +from dataclasses import fields + +from awesome_date_dimension.config import Column, DimCalendarMonthConfig + + +class TestDimCalendarMonthConfig(unittest.TestCase): + def test_error_when_column_factory_returns_non_column_value(self): + # This is the closest you can get to being correct: Returning a dictionary that LOOKS like a column. + with self.assertRaises(AssertionError): + DimCalendarMonthConfig( + column_factory=lambda name, c: { + "name": c.name, + "include": c.include, + "sort_index": c.sort_index, + } + ) + + def test_creating_duplicate_column_names_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column("dupe", column.include, column.sort_index) + + with self.assertRaises(AssertionError): + DimCalendarMonthConfig(column_factory=column_factory) + + def test_creating_duplicate_sort_keys_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column(column.name, column.include, 1000) + + with self.assertRaises(AssertionError): + DimCalendarMonthConfig(column_factory=column_factory) + + def test_prefixing_with_column_factory(self): + name_map: dict[str, str] = {} + + def column_factory(field_name: str, column: Column) -> Column: + if "Calendar" in field_name: + new_name = "CompanyName" + column.name + name_map[field_name] = new_name + return Column(new_name, column.include, column.sort_index) + return column + + cfg = DimCalendarMonthConfig(column_factory=column_factory) + + col_field_names = (f.name for f in fields(cfg.columns)) + for f_name in col_field_names: + col: Column = cfg.columns.__dict__[f_name] + with self.subTest( + f"column name should be prefixed with 'CompanyName' if it previously contained 'Calendar': {col.name}" + ): + if "Calendar" in f_name: + self.assertEqual(name_map[f_name], col.name) + + def test_defaults_are_immutable(self): + with open( + "./tests/files/default_dim_calendar_month_config.pickle", "rb" + ) as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimCalendarMonthConfig(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimdatecolumns.py b/tests/test_config/test_dimdatecolumns.py new file mode 100644 index 0000000..ea77597 --- /dev/null +++ b/tests/test_config/test_dimdatecolumns.py @@ -0,0 +1,31 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Column, DimDateColumns + + +class TestDimDateColumns(unittest.TestCase): + def test_failure_when_key_column_is_excluded(self): + with self.assertRaises(AssertionError): + DimDateColumns(date_key=Column("DateKey", False, 1000)) + + def test_failure_when_duplicate_colnames(self): + with self.assertRaises(AssertionError): + DimDateColumns(Column("Dupe", True, 1000), Column("Dupe", True, 2000)) + + def test_failure_when_duplicate_sortkeys(self): + with self.assertRaises(AssertionError): + DimDateColumns( + Column("DateKey", True, 1000), + Column("DateKey", True, 1000), + ) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_dim_date_columns.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimDateColumns(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimdateconfig.py b/tests/test_config/test_dimdateconfig.py new file mode 100644 index 0000000..cceaa44 --- /dev/null +++ b/tests/test_config/test_dimdateconfig.py @@ -0,0 +1,63 @@ +import pickle +import unittest +from dataclasses import fields + +from awesome_date_dimension.config import Column, DimDateConfig + + +class TestDimDateConfig(unittest.TestCase): + def test_error_when_column_factory_returns_non_column_value(self): + # This is the closest you can get to being correct: Returning a dictionary that LOOKS like a column. + with self.assertRaises(AssertionError): + DimDateConfig( + column_factory=lambda name, c: { + "name": c.name, + "include": c.include, + "sort_index": c.sort_index, + } + ) + + def test_creating_duplicate_column_names_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column("dupe", column.include, column.sort_index) + + with self.assertRaises(AssertionError): + DimDateConfig(column_factory=column_factory) + + def test_creating_duplicate_sort_keys_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column(column.name, column.include, 1000) + + with self.assertRaises(AssertionError): + DimDateConfig(column_factory=column_factory) + + def test_prefixing_with_column_factory(self): + name_map: dict[str, str] = {} + + def column_factory(field_name: str, column: Column) -> Column: + if "fiscal" in field_name: + new_name = "CompanyName" + column.name + name_map[field_name] = new_name + return Column(new_name, column.include, column.sort_index) + return column + + cfg = DimDateConfig(column_factory=column_factory) + + col_field_names = (f.name for f in fields(cfg.columns)) + for f_name in col_field_names: + col: Column = cfg.columns.__dict__[f_name] + with self.subTest( + f"column name should be prefixed with 'CompanyName' if it previously contained 'fiscal': {col.name}" + ): + if "fiscal" in f_name: + self.assertEqual(name_map[f_name], col.name) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_dim_date_config.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimDateConfig(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimfiscalmonthcolumns.py b/tests/test_config/test_dimfiscalmonthcolumns.py new file mode 100644 index 0000000..652247d --- /dev/null +++ b/tests/test_config/test_dimfiscalmonthcolumns.py @@ -0,0 +1,39 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Column, DimFiscalMonthColumns + + +class TestDimFiscalMonthColumns(unittest.TestCase): + def test_failure_when_start_key_column_is_excluded(self): + with self.assertRaises(AssertionError): + DimFiscalMonthColumns(month_start_key=Column("MonthStartKey", False, 1000)) + + def test_failure_when_end_key_column_is_excluded(self): + with self.assertRaises(AssertionError): + DimFiscalMonthColumns(month_end_key=Column("MonthEndKey", False, 1000)) + + def test_failure_when_duplicate_colnames(self): + with self.assertRaises(AssertionError): + DimFiscalMonthColumns( + Column("Dupe", True, 1000), Column("Dupe", True, 2000) + ) + + def test_failure_when_duplicate_sortkeys(self): + with self.assertRaises(AssertionError): + DimFiscalMonthColumns( + Column("DateKey", True, 1000), + Column("SomethingElse", True, 1000), + ) + + def test_defaults_are_immutable(self): + with open( + "./tests/files/default_dim_fiscal_month_columns.pickle", "rb" + ) as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimFiscalMonthColumns(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_dimfiscalmonthconfig.py b/tests/test_config/test_dimfiscalmonthconfig.py new file mode 100644 index 0000000..0e5227f --- /dev/null +++ b/tests/test_config/test_dimfiscalmonthconfig.py @@ -0,0 +1,63 @@ +import pickle +import unittest +from dataclasses import fields + +from awesome_date_dimension.config import Column, DimFiscalMonthConfig + + +class TestDimFiscalMonthConfig(unittest.TestCase): + def test_error_when_column_factory_returns_non_column_value(self): + # This is the closest you can get to being correct: Returning a dictionary that LOOKS like a column. + with self.assertRaises(AssertionError): + DimFiscalMonthConfig( + column_factory=lambda name, c: { + "name": c.name, + "include": c.include, + "sort_index": c.sort_index, + } + ) + + def test_creating_duplicate_column_names_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column("dupe", column.include, column.sort_index) + + with self.assertRaises(AssertionError): + DimFiscalMonthConfig(column_factory=column_factory) + + def test_creating_duplicate_sort_keys_with_column_factory_causes_error(self): + def column_factory(field_name: str, column: Column) -> Column: + return Column(column.name, column.include, 1000) + + with self.assertRaises(AssertionError): + DimFiscalMonthConfig(column_factory=column_factory) + + def test_prefixing_with_column_factory(self): + name_map: dict[str, str] = {} + + def column_factory(field_name: str, column: Column) -> Column: + if "fiscal" in field_name: + new_name = "CompanyName" + column.name + name_map[field_name] = new_name + return Column(new_name, column.include, column.sort_index) + return column + + cfg = DimFiscalMonthConfig(column_factory=column_factory) + + col_field_names = (f.name for f in fields(cfg.columns)) + for f_name in col_field_names: + col: Column = cfg.columns.__dict__[f_name] + with self.subTest( + f"column name should be prefixed with 'CompanyName' if it previously contained 'fiscal': {col.name}" + ): + if "fiscal" in f_name: + self.assertEqual(name_map[f_name], col.name) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_dim_fiscal_month_config.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(DimFiscalMonthConfig(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_fiscalconfig.py b/tests/test_config/test_fiscalconfig.py new file mode 100644 index 0000000..caa189b --- /dev/null +++ b/tests/test_config/test_fiscalconfig.py @@ -0,0 +1,41 @@ +import unittest + +from awesome_date_dimension.config import FiscalConfig + + +class TestFiscalConfig(unittest.TestCase): + def test_fiscal_date_range_fails_over_28(self): + with self.assertRaises(AssertionError): + FiscalConfig(month_start_day=29) + + def test_fiscal_date_range_succeeds_1_thru_28(self): + for i in range(1, 29): + FiscalConfig(month_start_day=i) + + def test_fiscal_date_range_fails_under_1(self): + with self.assertRaises(AssertionError): + FiscalConfig(month_start_day=0) + + def test_fiscal_month_range_fails_over_12(self): + with self.assertRaises(AssertionError): + FiscalConfig(year_start_month=13) + + def test_fiscal_month_range_succeeds_1_thru_12(self): + for i in range(1, 13): + FiscalConfig(year_start_month=i) + + def test_fiscal_month_range_fails_under_1(self): + with self.assertRaises(AssertionError): + FiscalConfig(year_start_month=0) + + def test_defaults_are_immutable(self): + default_config = FiscalConfig() + self.assertEqual(default_config.month_start_day, 1) + self.assertEqual(default_config.year_start_month, 1) + self.assertTrue(default_config.month_end_matches_calendar) + self.assertTrue(default_config.quarter_end_matches_calendar) + self.assertTrue(default_config.year_end_matches_calendar) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_holiday_calendars.py b/tests/test_config/test_holiday_calendars.py new file mode 100644 index 0000000..4d5af1e --- /dev/null +++ b/tests/test_config/test_holiday_calendars.py @@ -0,0 +1,65 @@ +import pickle +import unittest +from datetime import datetime +from typing import Callable + +from awesome_date_dimension.config import ( + Holiday, + HolidayCalendar, + HolidayType, + default_company_holidays, + default_us_public_holidays, +) + + +# Default holidays are pickled. The default holidays when using the default_holidays functions from +class TestHolidayCalendars(unittest.TestCase): + def test_failure_on_duplicate_holidays_within_calendar(self): + nyd = datetime.fromisoformat("2021-01-01").date() + with self.assertRaises(AssertionError): + HolidayCalendar( + HolidayType( + "Anything can be a holiday if you want it to be!", "AnythingHoliday" + ), + [Holiday("New Year's Day", nyd), Holiday("New Year's Day", nyd)], + ) + + def test_failure_on_holidays_with_same_date_within_calendar(self): + xmas = datetime.fromisoformat("2021-12-25").date() + with self.assertRaises(AssertionError): + HolidayCalendar( + HolidayType("Ho HO HOHOHOHO", "HoHoliday"), + [ + Holiday("Christmas", xmas), + Holiday("DayBeforeChristmasWrongDay", xmas), + ], + ) + + def test_default_company_holidays(self): + self.generic_default_holidays_test( + "./tests/files/default_company_holidays.pickle", default_company_holidays + ) + + def test_default_us_public_holidays(self): + self.generic_default_holidays_test( + "./tests/files/default_us_public_holidays.pickle", + default_us_public_holidays, + ) + + def generic_default_holidays_test( + self, pickle_path: str, default_holiday_func: Callable[[], HolidayCalendar] + ): + with open(pickle_path, "rb") as file: + saved_default_holidays = pickle.load(file) + + default = default_holiday_func() + + self.assertEqual(saved_default_holidays.holiday_type, default.holiday_type) + for i, h in enumerate(default.holidays): + saved_h = saved_default_holidays.holidays[i] + with self.subTest(f"{h.holiday_name} equals {saved_h.holiday_name}"): + self.assertEqual(saved_h, h) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_holidaycolumns.py b/tests/test_config/test_holidaycolumns.py new file mode 100644 index 0000000..965d7b7 --- /dev/null +++ b/tests/test_config/test_holidaycolumns.py @@ -0,0 +1,37 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Column, HolidaysColumns + + +class TestHolidayColumns(unittest.TestCase): + def test_failure_when_one_or_more_columns_are_excluded(self): + with self.assertRaises(AssertionError): + HolidaysColumns(date_key=Column("DateKey", False, 1000)) + with self.assertRaises(AssertionError): + HolidaysColumns(holiday_name=Column("HolidayName", False, 2000)) + with self.assertRaises(AssertionError): + HolidaysColumns( + Column("DateKey", False, 1000), Column("HolidayName", False, 2000) + ) + + def test_failure_when_duplicate_colnames(self): + with self.assertRaises(AssertionError): + HolidaysColumns(Column("Dupe", True, 1000), Column("Dupe", True, 2000)) + + def test_failure_when_duplicate_sortkeys(self): + with self.assertRaises(AssertionError): + HolidaysColumns( + Column("DateKey", True, 1000), + Column("HolidayName", True, 1000), + ) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_holidays_columns.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(HolidaysColumns(), saved_defaults) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_holidayconfig.py b/tests/test_config/test_holidayconfig.py new file mode 100644 index 0000000..def9545 --- /dev/null +++ b/tests/test_config/test_holidayconfig.py @@ -0,0 +1,29 @@ +import pickle +import unittest + +from awesome_date_dimension.config import HolidayConfig, default_company_holidays + + +class TestHolidayConfig(unittest.TestCase): + def test_failure_when_hol_types_and_hols_tables_have_same_name(self): + with self.assertRaises(AssertionError): + HolidayConfig(holiday_types_table_name="dupe", holidays_table_name="dupe") + + def test_failure_with_duplicate_holtypes(self): + with self.assertRaises(AssertionError): + HolidayConfig( + holiday_calendars=[ + default_company_holidays(), + default_company_holidays(), + ] + ) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_holiday_config.pickle", "rb") as file: + saved_default_hconfig = pickle.load(file) + + self.assertEqual(HolidayConfig(), saved_default_hconfig) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_holidaytype.py b/tests/test_config/test_holidaytype.py new file mode 100644 index 0000000..bfb9bc9 --- /dev/null +++ b/tests/test_config/test_holidaytype.py @@ -0,0 +1,27 @@ +import unittest + +from awesome_date_dimension.config import HolidayType + + +class TestHolidayType(unittest.TestCase): + def test_defaults_are_immutable(self): + holiday_name = "Everyday's a holiday if you're optimistic enough" + holiday_prefix = "EverydayHoliday" + default_holiday_type = HolidayType(holiday_name, holiday_prefix) + self.assertEqual(default_holiday_type.name, holiday_name) + self.assertEqual(default_holiday_type.generated_column_prefix, holiday_prefix) + self.assertEqual( + default_holiday_type.generated_flag_column_postfix, "HolidayFlag" + ) + self.assertEqual( + default_holiday_type.generated_name_column_postfix, "HolidayName" + ) + self.assertEqual( + default_holiday_type.generated_monthly_count_column_postfix, + "HolidaysInMonth", + ) + self.assertFalse(default_holiday_type.included_in_business_day_calc) + + +if __name__ == "main": + unittest.main() diff --git a/tests/test_config/test_holidaytypescolumns.py b/tests/test_config/test_holidaytypescolumns.py new file mode 100644 index 0000000..4b6c662 --- /dev/null +++ b/tests/test_config/test_holidaytypescolumns.py @@ -0,0 +1,35 @@ +import pickle +import unittest + +from awesome_date_dimension.config import Column, HolidayTypesColumns + + +class TestHolidayTypesColumns(unittest.TestCase): + def test_failure_when_one_or_more_columns_are_excluded(self): + with self.assertRaises(AssertionError): + HolidayTypesColumns(holiday_type_key=Column("HolidayTypeKey", False, 1000)) + with self.assertRaises(AssertionError): + HolidayTypesColumns( + holiday_type_name=Column("HolidayTypeName", False, 2000) + ) + + def test_failure_when_duplicate_colnames(self): + with self.assertRaises(AssertionError): + HolidayTypesColumns(Column("Dupe", True, 1000), Column("Dupe", True, 2000)) + + def test_failure_when_duplicate_sortkeys(self): + with self.assertRaises(AssertionError): + HolidayTypesColumns( + Column("HolidayTypeKey", True, 1000), + Column("HolidayTypeName", True, 1000), + ) + + def test_defaults_are_immutable(self): + with open("./tests/files/default_holiday_types_columns.pickle", "rb") as file: + saved_defaults = pickle.load(file) + + self.assertEqual(HolidayTypesColumns(), saved_defaults) + + +if __name__ == "main": + unittest.main()