lea-cli 0.7.2__tar.gz → 0.7.3__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.
- {lea_cli-0.7.2 → lea_cli-0.7.3}/PKG-INFO +1 -1
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/conductor.py +66 -51
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/dag.py +18 -3
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/databases.py +22 -4
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/scripts.py +20 -5
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/table_ref.py +12 -6
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/test_big_query.py +51 -31
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/test_table_ref.py +16 -10
- {lea_cli-0.7.2 → lea_cli-0.7.3}/pyproject.toml +1 -1
- {lea_cli-0.7.2 → lea_cli-0.7.3}/LICENSE +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/__init__.py +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/assertions/NO_NULLS.sql.jinja +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/assertions/SET.sql.jinja +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/assertions/UNIQUE.sql.jinja +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/assertions/UNIQUE_BY.sql.jinja +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/cli.py +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/comment.py +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/dialects.py +0 -0
- {lea_cli-0.7.2 → lea_cli-0.7.3}/lea/field.py +0 -0
|
@@ -115,7 +115,7 @@ class Session:
|
|
|
115
115
|
|
|
116
116
|
if self.incremental_field_name is not None:
|
|
117
117
|
self.filterable_table_refs = {
|
|
118
|
-
table_ref
|
|
118
|
+
table_ref.replace_dataset(self.write_dataset)
|
|
119
119
|
for table_ref in scripts
|
|
120
120
|
if any(
|
|
121
121
|
field.name == incremental_field_name
|
|
@@ -123,7 +123,7 @@ class Session:
|
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
125
|
self.incremental_table_refs = {
|
|
126
|
-
table_ref
|
|
126
|
+
table_ref.replace_dataset(self.write_dataset)
|
|
127
127
|
for table_ref in selected_table_refs | set(existing_audit_tables)
|
|
128
128
|
if any(
|
|
129
129
|
field.name == incremental_field_name and FieldTag.INCREMENTAL in field.tags
|
|
@@ -140,36 +140,18 @@ class Session:
|
|
|
140
140
|
def add_write_context_to_table_ref(self, table_ref: TableRef) -> TableRef:
|
|
141
141
|
table_ref = table_ref.replace_dataset(self.write_dataset)
|
|
142
142
|
table_ref = table_ref.add_audit_suffix()
|
|
143
|
-
if isinstance(self.database_client, databases.BigQueryClient):
|
|
144
|
-
table_ref = table_ref.replace_project(self.database_client.write_project_id)
|
|
145
143
|
return table_ref
|
|
146
144
|
|
|
147
145
|
def remove_write_context_from_table_ref(self, table_ref: TableRef) -> TableRef:
|
|
148
146
|
table_ref = table_ref.replace_dataset(self.base_dataset)
|
|
149
147
|
table_ref = table_ref.remove_audit_suffix()
|
|
150
|
-
if isinstance(self.database_client, databases.BigQueryClient):
|
|
151
|
-
table_ref = table_ref.replace_project(None)
|
|
152
148
|
return table_ref
|
|
153
149
|
|
|
154
150
|
def add_context_to_script(self, script: Script) -> Script:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if script.table_ref in self.incremental_table_refs:
|
|
159
|
-
script = dataclasses.replace(
|
|
160
|
-
script,
|
|
161
|
-
code=script.sql_dialect.add_dependency_filters(
|
|
162
|
-
code=script.code,
|
|
163
|
-
incremental_field_name=self.incremental_field_name,
|
|
164
|
-
incremental_field_values=self.incremental_field_values,
|
|
165
|
-
# One caveat is the dependencies which are not incremental do not have to be
|
|
166
|
-
# filtered. Indeed, they are already filtered by the fact that they are
|
|
167
|
-
# incremental.
|
|
168
|
-
dependencies_to_filter=self.filterable_table_refs - self.incremental_table_refs,
|
|
169
|
-
),
|
|
170
|
-
)
|
|
151
|
+
def add_context_to_dependency(dependency: TableRef) -> TableRef | None:
|
|
152
|
+
if dependency.project != script.table_ref.project:
|
|
153
|
+
return None
|
|
171
154
|
|
|
172
|
-
def add_context_to_dependency(dependency: TableRef) -> TableRef:
|
|
173
155
|
if (
|
|
174
156
|
dependency.replace_dataset(self.base_dataset)
|
|
175
157
|
in self.selected_table_refs
|
|
@@ -180,27 +162,36 @@ class Session:
|
|
|
180
162
|
and dependency.replace_dataset(self.base_dataset) in self.scripts
|
|
181
163
|
):
|
|
182
164
|
dependency = dependency.add_audit_suffix()
|
|
165
|
+
|
|
183
166
|
dependency = dependency.replace_dataset(self.write_dataset)
|
|
184
167
|
|
|
185
|
-
# Replace occurences of the dataset in script queries clauses to ensure it points
|
|
186
|
-
# to the right project. If we don't do that and a separate compute project is used,
|
|
187
|
-
# queries will default to looking for tables in the compute project, which is not
|
|
188
|
-
# what we want.
|
|
189
|
-
if (
|
|
190
|
-
isinstance(self.database_client, databases.BigQueryClient)
|
|
191
|
-
and dependency.project is None
|
|
192
|
-
):
|
|
193
|
-
dependency = dependency.replace_project(self.database_client.write_project_id)
|
|
194
168
|
return dependency
|
|
195
169
|
|
|
196
170
|
script = replace_script_dependencies(script=script, replace_func=add_context_to_dependency)
|
|
197
171
|
|
|
172
|
+
# If a script is marked as incremental, it implies that it can be run incrementally. This
|
|
173
|
+
# means that we have to filter the script's dependencies, as well as filter the output.
|
|
174
|
+
# This logic is implemented by the script's SQL dialect.
|
|
175
|
+
if script.table_ref.replace_dataset(self.write_dataset) in self.incremental_table_refs:
|
|
176
|
+
script = dataclasses.replace(
|
|
177
|
+
script,
|
|
178
|
+
code=script.sql_dialect.add_dependency_filters(
|
|
179
|
+
code=script.code,
|
|
180
|
+
incremental_field_name=self.incremental_field_name,
|
|
181
|
+
incremental_field_values=self.incremental_field_values,
|
|
182
|
+
# One caveat is the dependencies which are not incremental do not have to be
|
|
183
|
+
# filtered. Indeed, they are already filtered by the fact that they are
|
|
184
|
+
# incremental.
|
|
185
|
+
dependencies_to_filter=self.filterable_table_refs - self.incremental_table_refs,
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
198
189
|
# If the script is not incremental, we're not out of the woods! All scripts are
|
|
199
190
|
# materialized into side-tables which we call "audit" tables. This is the WAP pattern.
|
|
200
191
|
# Therefore, if a script is not incremental, but it depends on an incremental script, we
|
|
201
192
|
# have to modify the script to use both the incremental and non-incremental versions of
|
|
202
193
|
# the dependency. This is handled by the script's SQL dialect.
|
|
203
|
-
|
|
194
|
+
elif self.incremental_table_refs:
|
|
204
195
|
script = dataclasses.replace(
|
|
205
196
|
script,
|
|
206
197
|
code=script.sql_dialect.handle_incremental_dependencies(
|
|
@@ -208,10 +199,8 @@ class Session:
|
|
|
208
199
|
incremental_field_name=self.incremental_field_name,
|
|
209
200
|
incremental_field_values=self.incremental_field_values,
|
|
210
201
|
incremental_dependencies={
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
).remove_audit_suffix(): self.add_write_context_to_table_ref(table_ref)
|
|
214
|
-
for table_ref in self.incremental_table_refs
|
|
202
|
+
incremental_table_ref: incremental_table_ref.add_audit_suffix()
|
|
203
|
+
for incremental_table_ref in self.incremental_table_refs
|
|
215
204
|
},
|
|
216
205
|
),
|
|
217
206
|
)
|
|
@@ -234,7 +223,8 @@ class Session:
|
|
|
234
223
|
self.jobs.append(job)
|
|
235
224
|
|
|
236
225
|
msg = f"{job.status} {script.table_ref}"
|
|
237
|
-
|
|
226
|
+
|
|
227
|
+
if script.table_ref.remove_audit_suffix() in self.incremental_table_refs:
|
|
238
228
|
msg += " (incremental)"
|
|
239
229
|
log.info(msg)
|
|
240
230
|
|
|
@@ -291,9 +281,7 @@ class Session:
|
|
|
291
281
|
to_table_ref = table_ref.remove_audit_suffix()
|
|
292
282
|
|
|
293
283
|
is_incremental = (
|
|
294
|
-
self.incremental_field_name is not None
|
|
295
|
-
and to_table_ref.replace_dataset(self.base_dataset).replace_project(None)
|
|
296
|
-
in self.incremental_table_refs
|
|
284
|
+
self.incremental_field_name is not None and to_table_ref in self.incremental_table_refs
|
|
297
285
|
)
|
|
298
286
|
if is_incremental:
|
|
299
287
|
database_job = self.database_client.delete_and_insert(
|
|
@@ -345,11 +333,21 @@ def replace_script_dependencies(
|
|
|
345
333
|
|
|
346
334
|
"""
|
|
347
335
|
code = script.code
|
|
336
|
+
|
|
348
337
|
for dependency_to_edit in script.dependencies:
|
|
349
|
-
dependency_to_edit_str = script.sql_dialect.format_table_ref(dependency_to_edit)
|
|
350
338
|
new_dependency = replace_func(dependency_to_edit)
|
|
339
|
+
if new_dependency is None:
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
dependency_to_edit_without_project_str = script.sql_dialect.format_table_ref(
|
|
343
|
+
dependency_to_edit.replace_project(None)
|
|
344
|
+
)
|
|
351
345
|
new_dependency_str = script.sql_dialect.format_table_ref(new_dependency)
|
|
352
|
-
code = re.sub(
|
|
346
|
+
code = re.sub(
|
|
347
|
+
rf"\b{dependency_to_edit_without_project_str}\b",
|
|
348
|
+
new_dependency_str,
|
|
349
|
+
code,
|
|
350
|
+
)
|
|
353
351
|
|
|
354
352
|
# We also have to handle the case where the table is referenced to access a field.
|
|
355
353
|
# TODO: refactor this with the above
|
|
@@ -394,32 +392,45 @@ def delete_table_refs(
|
|
|
394
392
|
|
|
395
393
|
|
|
396
394
|
class Conductor:
|
|
397
|
-
def __init__(
|
|
395
|
+
def __init__(
|
|
396
|
+
self, scripts_dir: str, dataset_name: str | None = None, project_name: str | None = None
|
|
397
|
+
):
|
|
398
398
|
# Load environment variables from .env file
|
|
399
399
|
# TODO: is is Pythonic to do this here?
|
|
400
400
|
dotenv.load_dotenv(".env", verbose=True)
|
|
401
401
|
|
|
402
|
+
self.warehouse = os.environ["LEA_WAREHOUSE"].lower()
|
|
403
|
+
|
|
402
404
|
self.scripts_dir = pathlib.Path(scripts_dir)
|
|
403
405
|
if not self.scripts_dir.is_dir():
|
|
404
406
|
raise ValueError(f"Directory {self.scripts_dir} not found")
|
|
405
|
-
|
|
407
|
+
|
|
408
|
+
if dataset_name is None:
|
|
409
|
+
if self.warehouse == "bigquery":
|
|
410
|
+
dataset_name = os.environ.get("LEA_BQ_DATASET_NAME")
|
|
406
411
|
if dataset_name is None:
|
|
407
412
|
raise ValueError("Dataset name could not be inferred")
|
|
408
413
|
self.dataset_name = dataset_name
|
|
409
414
|
|
|
415
|
+
if project_name is None:
|
|
416
|
+
if self.warehouse == "bigquery":
|
|
417
|
+
project_name = os.environ.get("LEA_BQ_PROJECT_ID")
|
|
418
|
+
if project_name is None:
|
|
419
|
+
raise ValueError("Project name could not be inferred")
|
|
420
|
+
self.project_name = project_name
|
|
421
|
+
|
|
410
422
|
log.info("📝 Reading scripts")
|
|
411
423
|
|
|
412
424
|
self.dag = DAGOfScripts.from_directory(
|
|
413
425
|
scripts_dir=self.scripts_dir,
|
|
414
426
|
sql_dialect=BigQueryDialect(),
|
|
415
427
|
dataset_name=self.dataset_name,
|
|
428
|
+
project_name=self.project_name,
|
|
416
429
|
)
|
|
417
430
|
log.info(f"{len(self.dag.scripts):,d} scripts found")
|
|
418
431
|
|
|
419
432
|
def make_client(self, dry_run: bool = False, print_mode: bool = False) -> DatabaseClient:
|
|
420
|
-
warehouse
|
|
421
|
-
|
|
422
|
-
if warehouse.lower() == "bigquery":
|
|
433
|
+
if self.warehouse.lower() == "bigquery":
|
|
423
434
|
# Do imports here to avoid loading them all the time
|
|
424
435
|
from google.oauth2 import service_account
|
|
425
436
|
|
|
@@ -448,7 +459,7 @@ class Conductor:
|
|
|
448
459
|
print_mode=print_mode,
|
|
449
460
|
)
|
|
450
461
|
|
|
451
|
-
raise ValueError(f"Unsupported warehouse {warehouse!r}")
|
|
462
|
+
raise ValueError(f"Unsupported warehouse {self.warehouse!r}")
|
|
452
463
|
|
|
453
464
|
def name_user_dataset(self) -> str:
|
|
454
465
|
username = os.environ.get("LEA_USERNAME", getpass.getuser())
|
|
@@ -611,10 +622,14 @@ def promote_audit_tables(session: Session):
|
|
|
611
622
|
|
|
612
623
|
|
|
613
624
|
def delete_audit_tables(session: Session):
|
|
614
|
-
|
|
625
|
+
table_refs_to_delete = set(session.existing_audit_tables) | {
|
|
626
|
+
session.add_write_context_to_table_ref(table_ref)
|
|
627
|
+
for table_ref in session.selected_table_refs
|
|
628
|
+
}
|
|
629
|
+
if table_refs_to_delete:
|
|
615
630
|
log.info("🧹 Deleting audit tables")
|
|
616
631
|
delete_table_refs(
|
|
617
|
-
table_refs=
|
|
632
|
+
table_refs=table_refs_to_delete,
|
|
618
633
|
database_client=session.database_client,
|
|
619
634
|
executor=concurrent.futures.ThreadPoolExecutor(max_workers=None),
|
|
620
635
|
verbose=False,
|
|
@@ -19,19 +19,28 @@ class DAGOfScripts(graphlib.TopologicalSorter):
|
|
|
19
19
|
scripts: list[Script],
|
|
20
20
|
scripts_dir: pathlib.Path,
|
|
21
21
|
dataset_name: str,
|
|
22
|
+
project_name: str,
|
|
22
23
|
):
|
|
23
24
|
graphlib.TopologicalSorter.__init__(self, dependency_graph)
|
|
24
25
|
self.dependency_graph = dependency_graph
|
|
25
26
|
self.scripts = {script.table_ref: script for script in scripts}
|
|
26
27
|
self.scripts_dir = scripts_dir
|
|
27
28
|
self.dataset_name = dataset_name
|
|
29
|
+
self.project_name = project_name
|
|
28
30
|
|
|
29
31
|
@classmethod
|
|
30
32
|
def from_directory(
|
|
31
|
-
cls,
|
|
33
|
+
cls,
|
|
34
|
+
scripts_dir: pathlib.Path,
|
|
35
|
+
sql_dialect: SQLDialect,
|
|
36
|
+
dataset_name: str,
|
|
37
|
+
project_name: str,
|
|
32
38
|
) -> DAGOfScripts:
|
|
33
39
|
scripts = read_scripts(
|
|
34
|
-
scripts_dir=scripts_dir,
|
|
40
|
+
scripts_dir=scripts_dir,
|
|
41
|
+
sql_dialect=sql_dialect,
|
|
42
|
+
dataset_name=dataset_name,
|
|
43
|
+
project_name=project_name,
|
|
35
44
|
)
|
|
36
45
|
|
|
37
46
|
# Fields in the script's code may contain tags. These tags induce assertion tests, which
|
|
@@ -53,6 +62,7 @@ class DAGOfScripts(graphlib.TopologicalSorter):
|
|
|
53
62
|
scripts=scripts,
|
|
54
63
|
scripts_dir=scripts_dir,
|
|
55
64
|
dataset_name=dataset_name,
|
|
65
|
+
project_name=project_name,
|
|
56
66
|
)
|
|
57
67
|
|
|
58
68
|
def select(self, *queries: str) -> set[TableRef]:
|
|
@@ -111,7 +121,12 @@ class DAGOfScripts(graphlib.TopologicalSorter):
|
|
|
111
121
|
return
|
|
112
122
|
|
|
113
123
|
*schema, name = query.split(".")
|
|
114
|
-
table_ref = TableRef(
|
|
124
|
+
table_ref = TableRef(
|
|
125
|
+
dataset=self.dataset_name,
|
|
126
|
+
schema=tuple(schema),
|
|
127
|
+
name=name,
|
|
128
|
+
project=self.project_name,
|
|
129
|
+
)
|
|
115
130
|
yield table_ref
|
|
116
131
|
if include_ancestors:
|
|
117
132
|
yield from self.iter_ancestors(node=table_ref)
|
|
@@ -285,7 +285,9 @@ class BigQueryClient:
|
|
|
285
285
|
"""
|
|
286
286
|
job = self.client.query(query, location=self.location)
|
|
287
287
|
return {
|
|
288
|
-
BigQueryDialect.parse_table_ref(
|
|
288
|
+
BigQueryDialect.parse_table_ref(
|
|
289
|
+
f"{self.write_project_id}.{dataset_name}.{row['table_id']}"
|
|
290
|
+
): TableStats(
|
|
289
291
|
n_rows=row["row_count"],
|
|
290
292
|
n_bytes=row["size_bytes"],
|
|
291
293
|
updated_at=(
|
|
@@ -302,9 +304,9 @@ class BigQueryClient:
|
|
|
302
304
|
"""
|
|
303
305
|
job = self.client.query(query, location=self.location)
|
|
304
306
|
return {
|
|
305
|
-
BigQueryDialect.parse_table_ref(
|
|
306
|
-
|
|
307
|
-
]
|
|
307
|
+
BigQueryDialect.parse_table_ref(
|
|
308
|
+
f"{self.write_project_id}.{dataset_name}.{table_name}"
|
|
309
|
+
): [scripts.Field(name=row["column_name"]) for _, row in rows.iterrows()]
|
|
308
310
|
for table_name, rows in job.result()
|
|
309
311
|
.to_dataframe()
|
|
310
312
|
.sort_values(["table_name", "column_name"])
|
|
@@ -320,3 +322,19 @@ class BigQueryClient:
|
|
|
320
322
|
dry_run=self.dry_run,
|
|
321
323
|
**kwargs,
|
|
322
324
|
)
|
|
325
|
+
|
|
326
|
+
# The approach we use works best if the tables are clustered by account_slug. This is
|
|
327
|
+
# because we only need to refresh the data for a subset of accounts, and clustering by
|
|
328
|
+
# account_slug allows BigQuery to only scan the data for the accounts that need to be
|
|
329
|
+
# refreshed. This is a good practice in general, but it's particularly important in this
|
|
330
|
+
# case.
|
|
331
|
+
# WIP
|
|
332
|
+
# if (
|
|
333
|
+
# script is not None
|
|
334
|
+
# and not script.table_ref.is_test
|
|
335
|
+
# and script.table_ref.name.endswith("___audit")
|
|
336
|
+
# and "account_slug" in {field.name for field in script.fields}
|
|
337
|
+
# ):
|
|
338
|
+
# job_config.clustering_fields = ["account_slug"]
|
|
339
|
+
|
|
340
|
+
# return job_config
|
|
@@ -72,7 +72,11 @@ class SQLScript:
|
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
74
|
def from_path(
|
|
75
|
-
cls,
|
|
75
|
+
cls,
|
|
76
|
+
scripts_dir: pathlib.Path,
|
|
77
|
+
relative_path: pathlib.Path,
|
|
78
|
+
sql_dialect: SQLDialect,
|
|
79
|
+
project_name: str,
|
|
76
80
|
) -> SQLScript:
|
|
77
81
|
# Either the file is a Jinja template
|
|
78
82
|
if relative_path.suffixes == [".sql", ".jinja"]:
|
|
@@ -85,7 +89,9 @@ class SQLScript:
|
|
|
85
89
|
code = (scripts_dir / relative_path).read_text().rstrip().rstrip(";")
|
|
86
90
|
|
|
87
91
|
return cls(
|
|
88
|
-
table_ref=TableRef.from_path(
|
|
92
|
+
table_ref=TableRef.from_path(
|
|
93
|
+
scripts_dir=scripts_dir, relative_path=relative_path, project_name=project_name
|
|
94
|
+
),
|
|
89
95
|
code=code,
|
|
90
96
|
sql_dialect=sql_dialect,
|
|
91
97
|
updated_at=dt.datetime.fromtimestamp(
|
|
@@ -107,8 +113,15 @@ class SQLScript:
|
|
|
107
113
|
|
|
108
114
|
@functools.cached_property
|
|
109
115
|
def dependencies(self) -> set[TableRef]:
|
|
116
|
+
def add_default_project(table_ref: TableRef) -> TableRef:
|
|
117
|
+
if table_ref.project is None:
|
|
118
|
+
return table_ref.replace_project(self.table_ref.project)
|
|
119
|
+
return table_ref
|
|
120
|
+
|
|
110
121
|
return {
|
|
111
|
-
|
|
122
|
+
add_default_project(
|
|
123
|
+
self.sql_dialect.parse_table_ref(table_ref=sqlglot.exp.table_name(table))
|
|
124
|
+
)
|
|
112
125
|
for scope in sqlglot.optimizer.scope.traverse_scope(self.ast)
|
|
113
126
|
for table in scope.tables
|
|
114
127
|
if (
|
|
@@ -136,6 +149,7 @@ class SQLScript:
|
|
|
136
149
|
dataset=self.table_ref.dataset,
|
|
137
150
|
schema=("tests",),
|
|
138
151
|
name=f"{'__'.join(self.table_ref.schema)}__{self.table_ref.name}__{field.name}___{tag.lower().lstrip('#')}",
|
|
152
|
+
project=self.table_ref.project,
|
|
139
153
|
)
|
|
140
154
|
|
|
141
155
|
def make_assertion_test(table_ref, field, tag):
|
|
@@ -181,14 +195,14 @@ class SQLScript:
|
|
|
181
195
|
def __rich__(self):
|
|
182
196
|
code = textwrap.dedent(self.code).strip()
|
|
183
197
|
code_with_table_ref = f"""-- {self.table_ref}\n\n{code}\n"""
|
|
184
|
-
return rich.syntax.Syntax(code_with_table_ref, "sql", line_numbers=
|
|
198
|
+
return rich.syntax.Syntax(code_with_table_ref, "sql", line_numbers=False, theme="ansi_dark")
|
|
185
199
|
|
|
186
200
|
|
|
187
201
|
Script = SQLScript
|
|
188
202
|
|
|
189
203
|
|
|
190
204
|
def read_scripts(
|
|
191
|
-
scripts_dir: pathlib.Path, sql_dialect: SQLDialect, dataset_name: str
|
|
205
|
+
scripts_dir: pathlib.Path, sql_dialect: SQLDialect, dataset_name: str, project_name: str
|
|
192
206
|
) -> list[Script]:
|
|
193
207
|
def read_script(path: pathlib.Path) -> Script:
|
|
194
208
|
match tuple(path.suffixes):
|
|
@@ -197,6 +211,7 @@ def read_scripts(
|
|
|
197
211
|
scripts_dir=scripts_dir,
|
|
198
212
|
relative_path=path.relative_to(scripts_dir),
|
|
199
213
|
sql_dialect=sql_dialect,
|
|
214
|
+
project_name=project_name,
|
|
200
215
|
)
|
|
201
216
|
case _:
|
|
202
217
|
raise ValueError(f"Unsupported script type: {path}")
|
|
@@ -12,33 +12,39 @@ class TableRef:
|
|
|
12
12
|
dataset: str
|
|
13
13
|
schema: tuple[str, ...]
|
|
14
14
|
name: str
|
|
15
|
-
project: str | None
|
|
15
|
+
project: str | None
|
|
16
16
|
|
|
17
17
|
def __str__(self):
|
|
18
18
|
return ".".join(filter(None, [self.project, self.dataset, *self.schema, self.name]))
|
|
19
19
|
|
|
20
20
|
@classmethod
|
|
21
|
-
def from_path(
|
|
21
|
+
def from_path(
|
|
22
|
+
cls, scripts_dir: pathlib.Path, relative_path: pathlib.Path, project_name: str
|
|
23
|
+
) -> TableRef:
|
|
22
24
|
parts = list(filter(None, relative_path.parts))
|
|
23
25
|
*schema, filename = parts
|
|
24
26
|
return cls(
|
|
25
27
|
dataset=scripts_dir.name,
|
|
26
28
|
schema=tuple(schema),
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
+
name=filename.split(".")[0], # remove the extension
|
|
30
|
+
project=project_name,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
def replace_dataset(self, dataset: str) -> TableRef:
|
|
32
34
|
return dataclasses.replace(self, dataset=dataset)
|
|
33
35
|
|
|
34
|
-
def replace_project(self, project: str
|
|
36
|
+
def replace_project(self, project: str) -> TableRef:
|
|
35
37
|
return dataclasses.replace(self, project=project)
|
|
36
38
|
|
|
37
39
|
def add_audit_suffix(self) -> TableRef:
|
|
40
|
+
if self.is_audit_table:
|
|
41
|
+
return self
|
|
38
42
|
return dataclasses.replace(self, name=f"{self.name}{AUDIT_TABLE_SUFFIX}")
|
|
39
43
|
|
|
40
44
|
def remove_audit_suffix(self) -> TableRef:
|
|
41
|
-
|
|
45
|
+
if self.is_audit_table:
|
|
46
|
+
return dataclasses.replace(self, name=re.sub(rf"{AUDIT_TABLE_SUFFIX}$", "", self.name))
|
|
47
|
+
return self
|
|
42
48
|
|
|
43
49
|
@property
|
|
44
50
|
def is_audit_table(self) -> bool:
|
|
@@ -19,7 +19,7 @@ def scripts() -> dict[TableRef, Script]:
|
|
|
19
19
|
script.table_ref: script
|
|
20
20
|
for script in [
|
|
21
21
|
Script(
|
|
22
|
-
table_ref=TableRef("read", ("raw",), "users"),
|
|
22
|
+
table_ref=TableRef("read", ("raw",), "users", "test_project"),
|
|
23
23
|
code="""
|
|
24
24
|
SELECT * FROM UNNEST([
|
|
25
25
|
STRUCT(1 AS id, 'Alice' AS name, 30 AS age),
|
|
@@ -30,7 +30,7 @@ def scripts() -> dict[TableRef, Script]:
|
|
|
30
30
|
sql_dialect=BigQueryDialect(),
|
|
31
31
|
),
|
|
32
32
|
Script(
|
|
33
|
-
table_ref=TableRef("read", ("core",), "users"),
|
|
33
|
+
table_ref=TableRef("read", ("core",), "users", "test_project"),
|
|
34
34
|
code="""
|
|
35
35
|
SELECT
|
|
36
36
|
id,
|
|
@@ -42,7 +42,7 @@ def scripts() -> dict[TableRef, Script]:
|
|
|
42
42
|
sql_dialect=BigQueryDialect(),
|
|
43
43
|
),
|
|
44
44
|
Script(
|
|
45
|
-
table_ref=TableRef("read", ("analytics",), "n_users"),
|
|
45
|
+
table_ref=TableRef("read", ("analytics",), "n_users", "test_project"),
|
|
46
46
|
code="""
|
|
47
47
|
SELECT COUNT(*)
|
|
48
48
|
FROM read.core__users
|
|
@@ -70,7 +70,9 @@ def test_simple_run(scripts):
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
assert_queries_are_equal(
|
|
73
|
-
session.add_context_to_script(
|
|
73
|
+
session.add_context_to_script(
|
|
74
|
+
scripts[TableRef("read", ("raw",), "users", "test_project")]
|
|
75
|
+
).code,
|
|
74
76
|
"""
|
|
75
77
|
SELECT * FROM UNNEST([
|
|
76
78
|
STRUCT(1 AS id, 'Alice' AS name, 30 AS age),
|
|
@@ -80,10 +82,12 @@ def test_simple_run(scripts):
|
|
|
80
82
|
""",
|
|
81
83
|
)
|
|
82
84
|
assert_queries_are_equal(
|
|
83
|
-
session.add_context_to_script(
|
|
85
|
+
session.add_context_to_script(
|
|
86
|
+
scripts[TableRef("read", ("analytics",), "n_users", "test_project")]
|
|
87
|
+
).code,
|
|
84
88
|
"""
|
|
85
89
|
SELECT COUNT(*)
|
|
86
|
-
FROM write.core__users___audit
|
|
90
|
+
FROM `test_project`.write.core__users___audit
|
|
87
91
|
""",
|
|
88
92
|
)
|
|
89
93
|
|
|
@@ -101,29 +105,33 @@ def test_incremental_field(scripts):
|
|
|
101
105
|
)
|
|
102
106
|
|
|
103
107
|
assert_queries_are_equal(
|
|
104
|
-
session.add_context_to_script(
|
|
108
|
+
session.add_context_to_script(
|
|
109
|
+
scripts[TableRef("read", ("core",), "users", "test_project")]
|
|
110
|
+
).code,
|
|
105
111
|
"""
|
|
106
112
|
SELECT *
|
|
107
113
|
FROM (
|
|
108
114
|
SELECT id, name, age
|
|
109
|
-
FROM write.raw__users___audit
|
|
115
|
+
FROM `test_project`.write.raw__users___audit
|
|
110
116
|
)
|
|
111
117
|
WHERE name IN ('Alice')
|
|
112
118
|
""",
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
assert_queries_are_equal(
|
|
116
|
-
session.add_context_to_script(
|
|
122
|
+
session.add_context_to_script(
|
|
123
|
+
scripts[TableRef("read", ("analytics",), "n_users", "test_project")]
|
|
124
|
+
).code,
|
|
117
125
|
"""
|
|
118
126
|
SELECT COUNT(*) FROM (
|
|
119
127
|
SELECT *
|
|
120
|
-
FROM write.core__users___audit
|
|
128
|
+
FROM `test_project`.write.core__users___audit
|
|
121
129
|
WHERE name IN ('Alice')
|
|
122
130
|
|
|
123
131
|
UNION ALL
|
|
124
132
|
|
|
125
133
|
SELECT *
|
|
126
|
-
FROM write.core__users
|
|
134
|
+
FROM `test_project`.write.core__users
|
|
127
135
|
WHERE name NOT IN ('Alice')
|
|
128
136
|
)
|
|
129
137
|
""",
|
|
@@ -136,21 +144,23 @@ def test_incremental_field_but_no_incremental_table_selected(scripts):
|
|
|
136
144
|
base_dataset="read",
|
|
137
145
|
write_dataset="write",
|
|
138
146
|
scripts=scripts,
|
|
139
|
-
selected_table_refs={TableRef("read", ("analytics",), "n_users")},
|
|
147
|
+
selected_table_refs={TableRef("read", ("analytics",), "n_users", "test_project")},
|
|
140
148
|
existing_audit_tables={},
|
|
141
149
|
incremental_field_name="name",
|
|
142
150
|
incremental_field_values={"Alice"},
|
|
143
151
|
)
|
|
144
152
|
|
|
145
153
|
assert_queries_are_equal(
|
|
146
|
-
session.add_context_to_script(
|
|
154
|
+
session.add_context_to_script(
|
|
155
|
+
scripts[TableRef("read", ("core",), "users", "test_project")]
|
|
156
|
+
).code,
|
|
147
157
|
"""
|
|
148
158
|
SELECT
|
|
149
159
|
id,
|
|
150
160
|
-- #INCREMENTAL
|
|
151
161
|
name,
|
|
152
162
|
age
|
|
153
|
-
FROM write.raw__users
|
|
163
|
+
FROM `test_project`.write.raw__users
|
|
154
164
|
""",
|
|
155
165
|
)
|
|
156
166
|
|
|
@@ -161,19 +171,21 @@ def test_incremental_field_with_just_incremental_table_selected(scripts):
|
|
|
161
171
|
base_dataset="read",
|
|
162
172
|
write_dataset="write",
|
|
163
173
|
scripts=scripts,
|
|
164
|
-
selected_table_refs={TableRef("read", ("core",), "users")},
|
|
174
|
+
selected_table_refs={TableRef("read", ("core",), "users", "test_project")},
|
|
165
175
|
existing_audit_tables={},
|
|
166
176
|
incremental_field_name="name",
|
|
167
177
|
incremental_field_values={"Alice"},
|
|
168
178
|
)
|
|
169
179
|
|
|
170
180
|
assert_queries_are_equal(
|
|
171
|
-
session.add_context_to_script(
|
|
181
|
+
session.add_context_to_script(
|
|
182
|
+
scripts[TableRef("read", ("core",), "users", "test_project")]
|
|
183
|
+
).code,
|
|
172
184
|
"""
|
|
173
185
|
SELECT *
|
|
174
186
|
FROM (
|
|
175
187
|
SELECT id, name, age
|
|
176
|
-
FROM write.raw__users
|
|
188
|
+
FROM `test_project`.write.raw__users
|
|
177
189
|
)
|
|
178
190
|
WHERE name IN ('Alice')
|
|
179
191
|
""",
|
|
@@ -188,19 +200,23 @@ def test_incremental_field_with_just_incremental_table_selected_and_materialized
|
|
|
188
200
|
base_dataset="read",
|
|
189
201
|
write_dataset="write",
|
|
190
202
|
scripts=scripts,
|
|
191
|
-
selected_table_refs={TableRef("read", ("core",), "users")},
|
|
192
|
-
existing_audit_tables={
|
|
203
|
+
selected_table_refs={TableRef("read", ("core",), "users", "test_project")},
|
|
204
|
+
existing_audit_tables={
|
|
205
|
+
TableRef("read", ("raw",), "users", "test_project"): DUMMY_TABLE_STATS
|
|
206
|
+
},
|
|
193
207
|
incremental_field_name="name",
|
|
194
208
|
incremental_field_values={"Alice"},
|
|
195
209
|
)
|
|
196
210
|
|
|
197
211
|
assert_queries_are_equal(
|
|
198
|
-
session.add_context_to_script(
|
|
212
|
+
session.add_context_to_script(
|
|
213
|
+
scripts[TableRef("read", ("core",), "users", "test_project")]
|
|
214
|
+
).code,
|
|
199
215
|
"""
|
|
200
216
|
SELECT *
|
|
201
217
|
FROM (
|
|
202
218
|
SELECT id, name, age
|
|
203
|
-
FROM write.raw__users___audit
|
|
219
|
+
FROM `test_project`.write.raw__users___audit
|
|
204
220
|
)
|
|
205
221
|
WHERE name IN ('Alice')
|
|
206
222
|
""",
|
|
@@ -215,27 +231,29 @@ def test_incremental_field_but_no_incremental_table_selected_and_yet_dependency_
|
|
|
215
231
|
base_dataset="read",
|
|
216
232
|
write_dataset="write",
|
|
217
233
|
scripts=scripts,
|
|
218
|
-
selected_table_refs={TableRef("read", ("analytics",), "n_users")},
|
|
234
|
+
selected_table_refs={TableRef("read", ("analytics",), "n_users", "test_project")},
|
|
219
235
|
existing_audit_tables={
|
|
220
|
-
TableRef("read", ("core",), "users"): DUMMY_TABLE_STATS,
|
|
236
|
+
TableRef("read", ("core",), "users", "test_project"): DUMMY_TABLE_STATS,
|
|
221
237
|
},
|
|
222
238
|
incremental_field_name="name",
|
|
223
239
|
incremental_field_values={"Alice"},
|
|
224
240
|
)
|
|
225
241
|
|
|
226
242
|
assert_queries_are_equal(
|
|
227
|
-
session.add_context_to_script(
|
|
243
|
+
session.add_context_to_script(
|
|
244
|
+
scripts[TableRef("read", ("analytics",), "n_users", "test_project")]
|
|
245
|
+
).code,
|
|
228
246
|
"""
|
|
229
247
|
SELECT COUNT(*)
|
|
230
248
|
FROM (
|
|
231
249
|
SELECT *
|
|
232
|
-
FROM write.core__users___audit
|
|
250
|
+
FROM `test_project`.write.core__users___audit
|
|
233
251
|
WHERE name IN ('Alice')
|
|
234
252
|
|
|
235
253
|
UNION ALL
|
|
236
254
|
|
|
237
255
|
SELECT *
|
|
238
|
-
FROM write.core__users
|
|
256
|
+
FROM `test_project`.write.core__users
|
|
239
257
|
WHERE name NOT IN ('Alice')
|
|
240
258
|
)
|
|
241
259
|
""",
|
|
@@ -255,27 +273,29 @@ def test_incremental_field_but_no_incremental_table_selected_and_yet_dependency_
|
|
|
255
273
|
base_dataset="read",
|
|
256
274
|
write_dataset="write",
|
|
257
275
|
scripts=scripts,
|
|
258
|
-
selected_table_refs={TableRef("read", ("analytics",), "n_users")},
|
|
276
|
+
selected_table_refs={TableRef("read", ("analytics",), "n_users", "test_project")},
|
|
259
277
|
existing_audit_tables={
|
|
260
|
-
TableRef("read", ("core",), "users"): DUMMY_TABLE_STATS,
|
|
278
|
+
TableRef("read", ("core",), "users", "test_project"): DUMMY_TABLE_STATS,
|
|
261
279
|
},
|
|
262
280
|
incremental_field_name="name",
|
|
263
281
|
incremental_field_values={"Alice"},
|
|
264
282
|
)
|
|
265
283
|
|
|
266
284
|
assert_queries_are_equal(
|
|
267
|
-
session.add_context_to_script(
|
|
285
|
+
session.add_context_to_script(
|
|
286
|
+
scripts[TableRef("read", ("analytics",), "n_users", "test_project")]
|
|
287
|
+
).code,
|
|
268
288
|
"""
|
|
269
289
|
SELECT COUNT(*)
|
|
270
290
|
FROM (
|
|
271
291
|
SELECT *
|
|
272
|
-
FROM `
|
|
292
|
+
FROM `test_project`.write.core__users___audit
|
|
273
293
|
WHERE name IN ('Alice')
|
|
274
294
|
|
|
275
295
|
UNION ALL
|
|
276
296
|
|
|
277
297
|
SELECT *
|
|
278
|
-
FROM `
|
|
298
|
+
FROM `test_project`.write.core__users
|
|
279
299
|
WHERE name NOT IN ('Alice')
|
|
280
300
|
)
|
|
281
301
|
""",
|
|
@@ -12,11 +12,17 @@ from lea.table_ref import TableRef
|
|
|
12
12
|
[
|
|
13
13
|
pytest.param(table_ref, expected, id=str(table_ref))
|
|
14
14
|
for table_ref, expected in [
|
|
15
|
-
(TableRef("my_dataset", ("my_schema",), "my_table"), "my_dataset.my_schema.my_table"),
|
|
16
|
-
(TableRef("my_dataset", (), "my_table"), "my_dataset.my_table"),
|
|
17
15
|
(
|
|
18
|
-
TableRef("my_dataset", ("my_schema", "
|
|
19
|
-
"my_dataset.my_schema.
|
|
16
|
+
TableRef("my_dataset", ("my_schema",), "my_table", "my_project"),
|
|
17
|
+
"my_project.my_dataset.my_schema.my_table",
|
|
18
|
+
),
|
|
19
|
+
(
|
|
20
|
+
TableRef("my_dataset", (), "my_table", "my_project"),
|
|
21
|
+
"my_project.my_dataset.my_table",
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
TableRef("my_dataset", ("my_schema", "my_subschema"), "my_table", "my_project"),
|
|
25
|
+
"my_project.my_dataset.my_schema.my_subschema.my_table",
|
|
20
26
|
),
|
|
21
27
|
]
|
|
22
28
|
],
|
|
@@ -31,16 +37,16 @@ def test_str(table_ref, expected):
|
|
|
31
37
|
pytest.param(table_ref, expected, id=str(table_ref))
|
|
32
38
|
for table_ref, expected in [
|
|
33
39
|
(
|
|
34
|
-
TableRef("my_dataset", ("my_schema",), "my_table"),
|
|
40
|
+
TableRef("my_dataset", ("my_schema",), "my_table", None),
|
|
35
41
|
"TableRef(dataset='my_dataset', schema=('my_schema',), name='my_table', project=None)",
|
|
36
42
|
),
|
|
37
43
|
(
|
|
38
|
-
TableRef("my_dataset", (), "my_table"),
|
|
44
|
+
TableRef("my_dataset", (), "my_table", None),
|
|
39
45
|
"TableRef(dataset='my_dataset', schema=(), name='my_table', project=None)",
|
|
40
46
|
),
|
|
41
47
|
(
|
|
42
|
-
TableRef("my_dataset", ("my_schema", "my_subschema"), "my_table"),
|
|
43
|
-
"TableRef(dataset='my_dataset', schema=('my_schema', 'my_subschema'), name='my_table', project=
|
|
48
|
+
TableRef("my_dataset", ("my_schema", "my_subschema"), "my_table", "my_project"),
|
|
49
|
+
"TableRef(dataset='my_dataset', schema=('my_schema', 'my_subschema'), name='my_table', project='my_project')",
|
|
44
50
|
),
|
|
45
51
|
]
|
|
46
52
|
],
|
|
@@ -52,5 +58,5 @@ def test_repr(table_ref, expected):
|
|
|
52
58
|
def test_from_path():
|
|
53
59
|
scripts_dir = pathlib.Path("my_dataset")
|
|
54
60
|
relative_path = pathlib.Path("my_schema/my_table.sql")
|
|
55
|
-
table_ref = TableRef.from_path(scripts_dir, relative_path)
|
|
56
|
-
assert table_ref == TableRef("my_dataset", ("my_schema",), "my_table")
|
|
61
|
+
table_ref = TableRef.from_path(scripts_dir, relative_path, "my_project")
|
|
62
|
+
assert table_ref == TableRef("my_dataset", ("my_schema",), "my_table", "my_project")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|