model2data 1.5.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {model2data-1.5.0/model2data.egg-info → model2data-1.6.0}/PKG-INFO +1 -1
  2. {model2data-1.5.0 → model2data-1.6.0}/README.md +18 -0
  3. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/faker.py +28 -3
  4. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/hints.py +32 -4
  5. {model2data-1.5.0 → model2data-1.6.0/model2data.egg-info}/PKG-INFO +1 -1
  6. {model2data-1.5.0 → model2data-1.6.0}/model2data.egg-info/SOURCES.txt +1 -0
  7. {model2data-1.5.0 → model2data-1.6.0}/pyproject.toml +1 -1
  8. model2data-1.6.0/tests/test_column_time_hints.py +241 -0
  9. {model2data-1.5.0 → model2data-1.6.0}/LICENSE +0 -0
  10. {model2data-1.5.0 → model2data-1.6.0}/README_PYPI.md +0 -0
  11. {model2data-1.5.0 → model2data-1.6.0}/model2data/__init__.py +0 -0
  12. {model2data-1.5.0 → model2data-1.6.0}/model2data/cli.py +0 -0
  13. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/__init__.py +0 -0
  14. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/project.py +0 -0
  15. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
  16. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
  17. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/templates/profiles.yml.jinja +0 -0
  18. {model2data-1.5.0 → model2data-1.6.0}/model2data/dbt/tests.py +0 -0
  19. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/__init__.py +0 -0
  20. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/core.py +0 -0
  21. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/options.py +0 -0
  22. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/relationships.py +0 -0
  23. {model2data-1.5.0 → model2data-1.6.0}/model2data/generate/timeline.py +0 -0
  24. {model2data-1.5.0 → model2data-1.6.0}/model2data/parse/__init__.py +0 -0
  25. {model2data-1.5.0 → model2data-1.6.0}/model2data/parse/dbml.py +0 -0
  26. {model2data-1.5.0 → model2data-1.6.0}/model2data/utils.py +0 -0
  27. {model2data-1.5.0 → model2data-1.6.0}/model2data.egg-info/dependency_links.txt +0 -0
  28. {model2data-1.5.0 → model2data-1.6.0}/model2data.egg-info/entry_points.txt +0 -0
  29. {model2data-1.5.0 → model2data-1.6.0}/model2data.egg-info/requires.txt +0 -0
  30. {model2data-1.5.0 → model2data-1.6.0}/model2data.egg-info/top_level.txt +0 -0
  31. {model2data-1.5.0 → model2data-1.6.0}/setup.cfg +0 -0
  32. {model2data-1.5.0 → model2data-1.6.0}/tests/test_as_of_anchor.py +0 -0
  33. {model2data-1.5.0 → model2data-1.6.0}/tests/test_cli.py +0 -0
  34. {model2data-1.5.0 → model2data-1.6.0}/tests/test_coverage_gaps.py +0 -0
  35. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbml_parser.py +0 -0
  36. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbml_parser_fuzz.py +0 -0
  37. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbt_integration.py +0 -0
  38. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbt_naming.py +0 -0
  39. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbt_project.py +0 -0
  40. {model2data-1.5.0 → model2data-1.6.0}/tests/test_dbt_tests.py +0 -0
  41. {model2data-1.5.0 → model2data-1.6.0}/tests/test_faker_name_inference.py +0 -0
  42. {model2data-1.5.0 → model2data-1.6.0}/tests/test_generation.py +0 -0
  43. {model2data-1.5.0 → model2data-1.6.0}/tests/test_options.py +0 -0
  44. {model2data-1.5.0 → model2data-1.6.0}/tests/test_release_stress.py +0 -0
  45. {model2data-1.5.0 → model2data-1.6.0}/tests/test_row_identity.py +0 -0
  46. {model2data-1.5.0 → model2data-1.6.0}/tests/test_shaping.py +0 -0
  47. {model2data-1.5.0 → model2data-1.6.0}/tests/test_table_seeds.py +0 -0
  48. {model2data-1.5.0 → model2data-1.6.0}/tests/test_timeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model2data
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Generate analytics-ready datasets from DBML models
5
5
  Author: JB Analytica
6
6
  License-Expression: MIT
@@ -184,6 +184,24 @@ doesn't say what it depends on can say so explicitly with an `after` note:
184
184
  shipped_at timestamp [note: '{"after": "ordered_at"}']
185
185
  ```
186
186
 
187
+ The flags above shape every date and timestamp column the same way, run-wide. `business_hours`,
188
+ `growth`, and `seasonality` column note hints override that for one column at a time — the whole
189
+ point being a run can be uniform everywhere except the one column that needs shaping, or shaped
190
+ everywhere except the one column that shouldn't be:
191
+
192
+ ```dbml
193
+ Table orders {
194
+ id int [pk]
195
+ created_at timestamp [note: '{"business_hours": true, "growth": 0.4}']
196
+ refunded_at timestamp [note: '{"growth": 0}']
197
+ }
198
+ ```
199
+
200
+ Here `created_at` gets business hours and growth even on an otherwise-uniform run, while
201
+ `refunded_at` stays flat even under `--growth 0.5` — each hint only replaces the fields it names,
202
+ so a partial hint like `{"growth": 0}` leaves that column's `business_hours`/`seasonality` at
203
+ whatever the run-level flags set.
204
+
187
205
  ### Shape how the data is spread
188
206
 
189
207
  By default every parent row is equally likely to be picked for a child row, and every column
@@ -12,7 +12,7 @@ from typing import Callable, Optional, Union
12
12
  import pandas as pd
13
13
  from faker import Faker
14
14
 
15
- from model2data.generate.options import TimeProfile
15
+ from model2data.generate.options import UNIFORM, TimeProfile
16
16
  from model2data.generate.timeline import weighted_dates, weighted_timestamps
17
17
  from model2data.parse.dbml import ColumnDef
18
18
 
@@ -505,6 +505,31 @@ def _infer_by_type(base_type: str) -> Optional[_Provider]:
505
505
  return lambda: fake.format(base_type)
506
506
 
507
507
 
508
+ def _column_time_profile(
509
+ column: ColumnDef, time_profile: Optional[TimeProfile]
510
+ ) -> Optional[TimeProfile]:
511
+ """The run-level `time_profile` with this column's note hints applied on top.
512
+
513
+ Mirrors the `skew` override on the FK branch, one level up: a note doesn't
514
+ replace the run's profile, it patches only the fields it names (via
515
+ `dataclasses.replace`), so `{"growth": 0}` flattens one column of a
516
+ growing run while `business_hours`/`seasonality` stay exactly what the
517
+ run set. `validate_hints` has already confirmed the column is a date or
518
+ timestamp column and that each hint present is well-typed, so this does
519
+ no validation of its own -- a column with no hint gets `time_profile`
520
+ back unchanged, including a bare `None`, so the uniform path stays
521
+ untouched.
522
+ """
523
+ note = column.note or {}
524
+ overrides = {
525
+ key: note[key] for key in ("business_hours", "growth", "seasonality") if key in note
526
+ }
527
+ if not overrides:
528
+ return time_profile
529
+ base = time_profile if time_profile is not None else UNIFORM
530
+ return replace(base, **overrides)
531
+
532
+
508
533
  def _generate_dates(row_count: int, as_of: AsOf, time_profile: Optional[TimeProfile]) -> list:
509
534
  """The date branch's values: shaped by `time_profile` when it isn't uniform.
510
535
 
@@ -750,13 +775,13 @@ def generate_column_values(
750
775
  # Dates
751
776
  # -----------------------------------------------------
752
777
  elif "date" in base_type and "time" not in base_type:
753
- values = _generate_dates(row_count, as_of, time_profile)
778
+ values = _generate_dates(row_count, as_of, _column_time_profile(column, time_profile))
754
779
 
755
780
  elif "time" in base_type and "stamp" not in base_type:
756
781
  values = [fake.time() for _ in range(row_count)]
757
782
 
758
783
  elif any(key in base_type for key in ["timestamp", "datetime"]):
759
- values = _generate_timestamps(row_count, as_of, time_profile)
784
+ values = _generate_timestamps(row_count, as_of, _column_time_profile(column, time_profile))
760
785
 
761
786
  # -----------------------------------------------------
762
787
  # Untyped / generic string columns: honour a type that names
@@ -3,10 +3,10 @@
3
3
  A note has always been either plain text (a comment, ignored by generation)
4
4
  or a JSON object read for `min`/`max`. This module documents the rest of that
5
5
  object's vocabulary -- `null_rate`, `weights`, `true_rate`, `distinct`, `skew`,
6
- `after` -- and checks it once, before a single row is generated, so a typo'd
7
- enum value or a hint on the wrong kind of column fails with a message naming
8
- the table and column rather than surfacing as a wrong-looking dataset or a
9
- downstream dbt test failure.
6
+ `after`, `business_hours`, `growth`, `seasonality` -- and checks it once,
7
+ before a single row is generated, so a typo'd enum value or a hint on the
8
+ wrong kind of column fails with a message naming the table and column rather
9
+ than surfacing as a wrong-looking dataset or a downstream dbt test failure.
10
10
 
11
11
  | key | applies to | meaning |
12
12
  |-------------|-----------------------------------------------|--------------------------------------------|
@@ -17,6 +17,9 @@ downstream dbt test failure.
17
17
  | `distinct` | columns that aren't an FK, `pk`, `unique`, enum | positive integer: draw from a pool that size |
18
18
  | `skew` | foreign-key columns | overrides the run-level `skew` for this column |
19
19
  | `after` | date/timestamp columns | name of another date/timestamp column, read by the time-aware generator |
20
+ | `business_hours` | date/timestamp columns | overrides the run-level `TimeProfile.business_hours` for this column |
21
+ | `growth` | date/timestamp columns | overrides the run-level `TimeProfile.growth` for this column |
22
+ | `seasonality` | date/timestamp columns | overrides the run-level `TimeProfile.seasonality` for this column |
20
23
  """
21
24
 
22
25
  from __future__ import annotations
@@ -152,6 +155,21 @@ def _validate_column_hints(
152
155
  raise ValueError(f'{label}: "after" only applies to date/timestamp columns.')
153
156
  _check_after(label, table_name, note["after"], columns_by_name)
154
157
 
158
+ if "business_hours" in note:
159
+ if not is_temporal:
160
+ raise ValueError(f'{label}: "business_hours" only applies to date/timestamp columns.')
161
+ _check_bool(label, "business_hours", note["business_hours"])
162
+
163
+ if "growth" in note:
164
+ if not is_temporal:
165
+ raise ValueError(f'{label}: "growth" only applies to date/timestamp columns.')
166
+ _check_growth(label, note["growth"])
167
+
168
+ if "seasonality" in note:
169
+ if not is_temporal:
170
+ raise ValueError(f'{label}: "seasonality" only applies to date/timestamp columns.')
171
+ _check_fraction(label, "seasonality", note["seasonality"])
172
+
155
173
 
156
174
  def _check_fraction(label: str, key: str, value: object) -> None:
157
175
  if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0.0 <= value <= 1.0:
@@ -163,6 +181,16 @@ def _check_positive_int(label: str, key: str, value: object) -> None:
163
181
  raise ValueError(f'{label}: "{key}" must be a positive whole number (got {value!r}).')
164
182
 
165
183
 
184
+ def _check_bool(label: str, key: str, value: object) -> None:
185
+ if not isinstance(value, bool):
186
+ raise ValueError(f'{label}: "{key}" must be true or false (got {value!r}).')
187
+
188
+
189
+ def _check_growth(label: str, value: object) -> None:
190
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value < -1.0:
191
+ raise ValueError(f'{label}: "growth" must be -1.0 or more (got {value!r}).')
192
+
193
+
166
194
  def _check_enum_weights(label: str, enum_values: list[str], weights: object) -> None:
167
195
  if not isinstance(weights, dict) or not weights:
168
196
  raise ValueError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model2data
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Generate analytics-ready datasets from DBML models
5
5
  Author: JB Analytica
6
6
  License-Expression: MIT
@@ -28,6 +28,7 @@ model2data/parse/__init__.py
28
28
  model2data/parse/dbml.py
29
29
  tests/test_as_of_anchor.py
30
30
  tests/test_cli.py
31
+ tests/test_column_time_hints.py
31
32
  tests/test_coverage_gaps.py
32
33
  tests/test_dbml_parser.py
33
34
  tests/test_dbml_parser_fuzz.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "model2data"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "Generate analytics-ready datasets from DBML models"
9
9
  readme = "README_PYPI.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,241 @@
1
+ """Per-column overrides of the run-level `TimeProfile`.
2
+
3
+ 1.5.0 shaped *when* things happen for a whole run: business hours, growth,
4
+ seasonality all apply to every date/timestamp column alike. The product
5
+ ask this closes was literally "that changes for everything globally, can we
6
+ have that column per column?" -- so `business_hours`/`growth`/`seasonality`
7
+ column-note hints patch just the fields they name onto the run's profile for
8
+ that one column, the same way `skew` already overrides per foreign key (see
9
+ tests/test_shaping.py). Every statistical test here uses a fixed seed and a
10
+ generous tolerance so it never flakes; the exact bounds were chosen by
11
+ running the real implementation and leaving headroom, not derived
12
+ analytically.
13
+ """
14
+
15
+ from datetime import date, datetime, timedelta
16
+
17
+ import pytest
18
+
19
+ from model2data.generate.core import generate_data_from_dbml
20
+ from model2data.generate.options import TimeProfile
21
+ from model2data.parse.dbml import ColumnDef, TableDef
22
+
23
+ ANCHOR_DATE = date(2026, 3, 15)
24
+
25
+
26
+ def _two_timestamp_schema(hinted_note=None, plain_note=None) -> dict[str, TableDef]:
27
+ return {
28
+ "events": TableDef(
29
+ name="events",
30
+ columns=[
31
+ ColumnDef("id", "int", {"pk"}),
32
+ ColumnDef("hinted_at", "timestamp", {"not null"}, note=hinted_note),
33
+ ColumnDef("plain_at", "timestamp", {"not null"}, note=plain_note),
34
+ ],
35
+ )
36
+ }
37
+
38
+
39
+ def _column(df, name) -> list[datetime]:
40
+ return [datetime.fromisoformat(v) for v in df[name]]
41
+
42
+
43
+ def _business_hours_share(timestamps: list[datetime]) -> float:
44
+ in_hours = sum(1 for t in timestamps if t.weekday() < 5 and 8 <= t.hour < 18)
45
+ return in_hours / len(timestamps)
46
+
47
+
48
+ def _timestamp_window_midpoint() -> date:
49
+ """Midpoint of the 365-day timestamp window `weighted_timestamps` draws
50
+ from (see timeline.py's `_TIMESTAMP_WINDOW_DAYS`): the anchor minus a
51
+ year, up to the day before the anchor."""
52
+ start = ANCHOR_DATE - timedelta(days=365)
53
+ end = ANCHOR_DATE - timedelta(days=1)
54
+ return start + (end - start) / 2
55
+
56
+
57
+ # ---------------------------------------------------------
58
+ # A hint shapes its own column, not its neighbour
59
+ # ---------------------------------------------------------
60
+ def test_business_hours_hint_shapes_only_the_hinted_column():
61
+ tables = _two_timestamp_schema(hinted_note={"business_hours": True})
62
+ df = generate_data_from_dbml(
63
+ tables, [], base_rows=1000, seed=5, as_of=ANCHOR_DATE, time_profile=TimeProfile()
64
+ )["events"]
65
+
66
+ hinted_share = _business_hours_share(_column(df, "hinted_at"))
67
+ plain_share = _business_hours_share(_column(df, "plain_at"))
68
+
69
+ assert hinted_share >= 0.70
70
+ assert plain_share < hinted_share - 0.2
71
+
72
+
73
+ def test_growth_hint_shapes_only_the_hinted_column():
74
+ tables = _two_timestamp_schema(hinted_note={"growth": 1.0})
75
+ df = generate_data_from_dbml(
76
+ tables, [], base_rows=2000, seed=5, as_of=ANCHOR_DATE, time_profile=TimeProfile()
77
+ )["events"]
78
+
79
+ midpoint = _timestamp_window_midpoint()
80
+ hinted = _column(df, "hinted_at")
81
+ plain = _column(df, "plain_at")
82
+
83
+ hinted_second_half = sum(1 for t in hinted if t.date() >= midpoint) / len(hinted)
84
+ plain_second_half = sum(1 for t in plain if t.date() >= midpoint) / len(plain)
85
+
86
+ # growth=1.0 on hinted_at pushes its rows toward the second half of the
87
+ # window; plain_at, with no hint under a flat run-level profile, does not.
88
+ assert hinted_second_half > plain_second_half + 0.05
89
+
90
+
91
+ # ---------------------------------------------------------
92
+ # A hint overrides the run-level profile in both directions
93
+ # ---------------------------------------------------------
94
+ def test_hint_overrides_a_flat_run_level_profile():
95
+ tables = _two_timestamp_schema(hinted_note={"business_hours": True})
96
+ df = generate_data_from_dbml(
97
+ tables, [], base_rows=1000, seed=5, as_of=ANCHOR_DATE, time_profile=TimeProfile()
98
+ )["events"]
99
+
100
+ assert _business_hours_share(_column(df, "hinted_at")) >= 0.70
101
+
102
+
103
+ def test_hint_overrides_a_shaped_run_level_profile_back_to_flat():
104
+ tables = _two_timestamp_schema(hinted_note={"business_hours": False})
105
+ df = generate_data_from_dbml(
106
+ tables,
107
+ [],
108
+ base_rows=1000,
109
+ seed=5,
110
+ as_of=ANCHOR_DATE,
111
+ time_profile=TimeProfile(business_hours=True),
112
+ )["events"]
113
+
114
+ # hinted_at is forced back to uniform; plain_at keeps the run's shaping.
115
+ hinted_share = _business_hours_share(_column(df, "hinted_at"))
116
+ plain_share = _business_hours_share(_column(df, "plain_at"))
117
+ assert plain_share >= 0.70
118
+ assert hinted_share < plain_share - 0.2
119
+
120
+
121
+ def test_cli_flag_and_column_hint_combine_with_the_hint_winning():
122
+ """`--business-hours` (a run-level TimeProfile) plus a column hint that
123
+ turns it back off for one column: the hint wins for that column, the
124
+ run-level setting still applies to its neighbour."""
125
+ tables = _two_timestamp_schema(hinted_note={"business_hours": False})
126
+ df = generate_data_from_dbml(
127
+ tables,
128
+ [],
129
+ base_rows=1000,
130
+ seed=5,
131
+ as_of=ANCHOR_DATE,
132
+ time_profile=TimeProfile(business_hours=True),
133
+ )["events"]
134
+
135
+ assert _business_hours_share(_column(df, "hinted_at")) < 0.5
136
+ assert _business_hours_share(_column(df, "plain_at")) >= 0.70
137
+
138
+
139
+ # ---------------------------------------------------------
140
+ # A partial hint only replaces the fields it names
141
+ # ---------------------------------------------------------
142
+ def test_partial_hint_keeps_the_other_run_level_fields():
143
+ """`{"growth": 0}` flattens growth for this column but leaves
144
+ business_hours exactly as the run set it."""
145
+ tables = _two_timestamp_schema(hinted_note={"growth": 0.0})
146
+ run_profile = TimeProfile(business_hours=True, growth=1.0)
147
+ df = generate_data_from_dbml(
148
+ tables, [], base_rows=2000, seed=5, as_of=ANCHOR_DATE, time_profile=run_profile
149
+ )["events"]
150
+
151
+ hinted = _column(df, "hinted_at")
152
+ plain = _column(df, "plain_at")
153
+
154
+ # business_hours still applies to hinted_at (inherited, not overridden).
155
+ assert _business_hours_share(hinted) >= 0.70
156
+
157
+ # growth=0 on hinted_at flattens it relative to plain_at, which keeps the
158
+ # run's growth=1.0 and skews toward the second half of the window.
159
+ midpoint = _timestamp_window_midpoint()
160
+ hinted_second_half = sum(1 for t in hinted if t.date() >= midpoint) / len(hinted)
161
+ plain_second_half = sum(1 for t in plain if t.date() >= midpoint) / len(plain)
162
+ assert 0.35 <= hinted_second_half <= 0.65
163
+ assert plain_second_half > hinted_second_half
164
+
165
+
166
+ # ---------------------------------------------------------
167
+ # No hint: pinned frames unchanged
168
+ # ---------------------------------------------------------
169
+ def test_no_hint_reproduces_the_run_level_only_frame():
170
+ """A column with no time hint must generate byte-identical values to the
171
+ same schema before this feature existed -- the override helper is a
172
+ pure no-op when a column's note carries none of the three keys."""
173
+ tables = _two_timestamp_schema()
174
+ with_helper = generate_data_from_dbml(
175
+ tables, [], base_rows=20, seed=2024, as_of=ANCHOR_DATE, time_profile=TimeProfile(growth=0.4)
176
+ )["events"]
177
+ again = generate_data_from_dbml(
178
+ tables, [], base_rows=20, seed=2024, as_of=ANCHOR_DATE, time_profile=TimeProfile(growth=0.4)
179
+ )["events"]
180
+
181
+ assert with_helper.equals(again)
182
+
183
+
184
+ def test_no_hint_and_uniform_profile_matches_the_pre_feature_pinned_frame():
185
+ """Same schema and seed as test_timeline.py's own pinned-frame test, with
186
+ an explicit empty note on both columns: still byte-identical to 1.5.0."""
187
+ tables = {
188
+ "events": TableDef(
189
+ name="events",
190
+ columns=[
191
+ ColumnDef("id", "int", {"pk"}),
192
+ ColumnDef("created_at", "timestamp", {"not null"}),
193
+ ColumnDef("event_date", "date", set()),
194
+ ],
195
+ )
196
+ }
197
+ df = generate_data_from_dbml(tables, [], base_rows=8, seed=2024, as_of=ANCHOR_DATE)["events"]
198
+
199
+ assert list(df["created_at"])[:2] == ["2026-01-24 23:52:50", "2025-08-07 01:00:18"]
200
+
201
+
202
+ # ---------------------------------------------------------
203
+ # Validation
204
+ # ---------------------------------------------------------
205
+ def _single_column_table(column: ColumnDef, table_name: str = "t") -> dict[str, TableDef]:
206
+ return {table_name: TableDef(name=table_name, columns=[ColumnDef("id", "int", {"pk"}), column])}
207
+
208
+
209
+ @pytest.mark.parametrize(
210
+ "key, value", [("business_hours", True), ("growth", 0.5), ("seasonality", 0.5)]
211
+ )
212
+ def test_hint_on_a_non_temporal_column_is_rejected(key, value):
213
+ tables = _single_column_table(ColumnDef("label", "varchar", note={key: value}))
214
+ with pytest.raises(ValueError, match=r"t\.label"):
215
+ generate_data_from_dbml(tables, [], base_rows=5, seed=1)
216
+
217
+
218
+ def test_growth_below_negative_one_is_rejected():
219
+ tables = _single_column_table(ColumnDef("happened_at", "timestamp", note={"growth": -1.5}))
220
+ with pytest.raises(ValueError, match=r"t\.happened_at.*growth"):
221
+ generate_data_from_dbml(tables, [], base_rows=5, seed=1)
222
+
223
+
224
+ def test_growth_as_a_bool_is_rejected():
225
+ tables = _single_column_table(ColumnDef("happened_at", "timestamp", note={"growth": True}))
226
+ with pytest.raises(ValueError, match=r"t\.happened_at"):
227
+ generate_data_from_dbml(tables, [], base_rows=5, seed=1)
228
+
229
+
230
+ def test_seasonality_out_of_range_is_rejected():
231
+ tables = _single_column_table(ColumnDef("happened_at", "timestamp", note={"seasonality": 1.5}))
232
+ with pytest.raises(ValueError, match=r"t\.happened_at"):
233
+ generate_data_from_dbml(tables, [], base_rows=5, seed=1)
234
+
235
+
236
+ def test_business_hours_as_a_non_bool_is_rejected():
237
+ tables = _single_column_table(
238
+ ColumnDef("happened_at", "timestamp", note={"business_hours": "yes"})
239
+ )
240
+ with pytest.raises(ValueError, match=r"t\.happened_at"):
241
+ generate_data_from_dbml(tables, [], base_rows=5, seed=1)
File without changes
File without changes
File without changes
File without changes
File without changes