From 54a8cf0075178e53e371cdefd3c0c8fdebcf8ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zawadzki?= Date: Wed, 4 Dec 2024 17:35:34 +0100 Subject: [PATCH 01/23] Docs: add missing mention of the required `endpoint_url` config (#2120) --- docs/website/docs/dlt-ecosystem/destinations/clickhouse.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/website/docs/dlt-ecosystem/destinations/clickhouse.md b/docs/website/docs/dlt-ecosystem/destinations/clickhouse.md index 3bd1ae8e15..40ee5d71e8 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/clickhouse.md +++ b/docs/website/docs/dlt-ecosystem/destinations/clickhouse.md @@ -229,8 +229,7 @@ To set up GCS staging with HMAC authentication in dlt: 1. Create HMAC keys for your GCS service account by following the [Google Cloud guide](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create). -2. Configure the HMAC keys (`aws_access_key_id` and `aws_secret_access_key`) in your dlt project's ClickHouse destination settings in `config.toml`, similar to how you would configure AWS S3 - credentials: +2. Configure the HMAC keys (`aws_access_key_id` and `aws_secret_access_key`) as well as `endpoint_url` in your dlt project's ClickHouse destination settings in `config.toml`, similar to how you would configure AWS S3 credentials: ```toml [destination.filesystem] From 31fa78c063d39582a43acf89eaa60eb08035d2a6 Mon Sep 17 00:00:00 2001 From: Anton Burnashev Date: Wed, 4 Dec 2024 17:37:15 +0100 Subject: [PATCH 02/23] Fix formatting in dlt.common.libs.pyarrow (#2102) From 6602f70dd63703b9953d43edb9aca1a719a8f7a3 Mon Sep 17 00:00:00 2001 From: rudolfix Date: Tue, 10 Dec 2024 21:41:17 +0100 Subject: [PATCH 03/23] checks notebook presence before finding userdata (#2117) --- dlt/common/configuration/providers/toml.py | 6 ++++++ .../configuration/test_toml_provider.py | 21 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dlt/common/configuration/providers/toml.py b/dlt/common/configuration/providers/toml.py index 3636565fae..e586fef225 100644 --- a/dlt/common/configuration/providers/toml.py +++ b/dlt/common/configuration/providers/toml.py @@ -124,6 +124,12 @@ def _read_google_colab_secrets(self, name: str, file_name: str) -> tomlkit.TOMLD """Try to load the toml from google colab userdata object""" try: from google.colab import userdata + from dlt.common.runtime.exec_info import is_notebook + + # make sure we work in interactive mode (get_ipython() is available) + # when dlt cli is run, userdata is available but without a kernel + if not is_notebook(): + return None try: return tomlkit.loads(userdata.get(file_name)) diff --git a/tests/common/configuration/test_toml_provider.py b/tests/common/configuration/test_toml_provider.py index 481c21b7bb..9538849976 100644 --- a/tests/common/configuration/test_toml_provider.py +++ b/tests/common/configuration/test_toml_provider.py @@ -4,6 +4,7 @@ import yaml from typing import Any, Dict, Type import datetime # noqa: I251 +from unittest.mock import Mock import dlt from dlt.common import pendulum, json @@ -538,11 +539,28 @@ def loader() -> Dict[str, Any]: def test_colab_toml() -> None: + import builtins + # use a path without any settings files try: sys.path.append("tests/common/cases/modules") - # secrets are in user data + + # ipython not present provider: SettingsTomlProvider = SecretsTomlProvider("tests/common/null", global_dir=None) + assert provider.is_empty + + get_ipython_m = Mock() + get_ipython_m.return_value = "google.colab.Shell" + # make it available to all modules + builtins.get_ipython = get_ipython_m # type: ignore[attr-defined] + # test mock + assert get_ipython() == "google.colab.Shell" # type: ignore[name-defined] # noqa + from dlt.common.runtime.exec_info import is_notebook + + assert is_notebook() + + # secrets are in user data + provider = SecretsTomlProvider("tests/common/null", global_dir=None) assert provider.to_toml() == 'api_key="api"' # config is not in userdata provider = ConfigTomlProvider("tests/common/null", "unknown") @@ -551,4 +569,5 @@ def test_colab_toml() -> None: provider = SecretsTomlProvider("tests/common/cases/configuration/.dlt", global_dir=None) assert provider.get_value("secret_value", str, None) == ("2137", "secret_value") finally: + delattr(builtins, "get_ipython") sys.path.pop() From 51b11d24acf579d4f12abc15f2b661778f2995d9 Mon Sep 17 00:00:00 2001 From: Steinthor Palsson Date: Tue, 10 Dec 2024 17:35:22 -0500 Subject: [PATCH 04/23] Add open/closed range arguments for incremental (#1991) * Add open/closed range arguments for incremental * Docs for incremental range args * Docstring * Typo * Ensure deduplication is disabled when range_start=='open' * Cache transformer settings --- dlt/common/incremental/typing.py | 4 + dlt/extract/incremental/__init__.py | 60 +++-- dlt/extract/incremental/transform.py | 75 ++++-- dlt/sources/sql_database/helpers.py | 12 +- .../verified-sources/sql_database/advanced.md | 49 +++- .../docs/general-usage/incremental-loading.md | 5 +- tests/extract/test_incremental.py | 111 +++++++- .../load/sources/sql_database/test_helpers.py | 237 ++++++++++++------ .../sql_database/test_sql_database_source.py | 17 +- 9 files changed, 434 insertions(+), 136 deletions(-) diff --git a/dlt/common/incremental/typing.py b/dlt/common/incremental/typing.py index 460e2f234b..2ca981bff0 100644 --- a/dlt/common/incremental/typing.py +++ b/dlt/common/incremental/typing.py @@ -8,6 +8,8 @@ LastValueFunc = Callable[[Sequence[TCursorValue]], Any] OnCursorValueMissing = Literal["raise", "include", "exclude"] +TIncrementalRange = Literal["open", "closed"] + class IncrementalColumnState(TypedDict): initial_value: Optional[Any] @@ -26,3 +28,5 @@ class IncrementalArgs(TypedDict, total=False): allow_external_schedulers: Optional[bool] lag: Optional[Union[float, int]] on_cursor_value_missing: Optional[OnCursorValueMissing] + range_start: Optional[TIncrementalRange] + range_end: Optional[TIncrementalRange] diff --git a/dlt/extract/incremental/__init__.py b/dlt/extract/incremental/__init__.py index 28d33bb71f..5e7bae49c6 100644 --- a/dlt/extract/incremental/__init__.py +++ b/dlt/extract/incremental/__init__.py @@ -42,6 +42,7 @@ LastValueFunc, OnCursorValueMissing, IncrementalArgs, + TIncrementalRange, ) from dlt.extract.items import SupportsPipe, TTableHintTemplate, ItemTransform from dlt.extract.incremental.transform import ( @@ -104,6 +105,11 @@ class Incremental(ItemTransform[TDataItem], BaseConfiguration, Generic[TCursorVa Note that if logical "end date" is present then also "end_value" will be set which means that resource state is not used and exactly this range of date will be loaded on_cursor_value_missing: Specify what happens when the cursor_path does not exist in a record or a record has `None` at the cursor_path: raise, include, exclude lag: Optional value used to define a lag or attribution window. For datetime cursors, this is interpreted as seconds. For other types, it uses the + or - operator depending on the last_value_func. + range_start: Decide whether the incremental filtering range is `open` or `closed` on the start value side. Default is `closed`. + Setting this to `open` means that items with the same cursor value as the last value from the previous run (or `initial_value`) are excluded from the result. + The `open` range disables deduplication logic so it can serve as an optimization when you know cursors don't overlap between pipeline runs. + range_end: Decide whether the incremental filtering range is `open` or `closed` on the end value side. Default is `open` (exact `end_value` is excluded). + Setting this to `closed` means that items with the exact same cursor value as the `end_value` are included in the result. """ # this is config/dataclass so declare members @@ -116,6 +122,8 @@ class Incremental(ItemTransform[TDataItem], BaseConfiguration, Generic[TCursorVa on_cursor_value_missing: OnCursorValueMissing = "raise" lag: Optional[float] = None duplicate_cursor_warning_threshold: ClassVar[int] = 200 + range_start: TIncrementalRange = "closed" + range_end: TIncrementalRange = "open" # incremental acting as empty EMPTY: ClassVar["Incremental[Any]"] = None @@ -132,6 +140,8 @@ def __init__( allow_external_schedulers: bool = False, on_cursor_value_missing: OnCursorValueMissing = "raise", lag: Optional[float] = None, + range_start: TIncrementalRange = "closed", + range_end: TIncrementalRange = "open", ) -> None: # make sure that path is valid if cursor_path: @@ -174,9 +184,11 @@ def __init__( self.start_out_of_range: bool = False """Becomes true on the first item that is out of range of `start_value`. I.e. when using `max` this is a value that is lower than `start_value`""" - self._transformers: Dict[str, IncrementalTransform] = {} + self._transformers: Dict[Type[IncrementalTransform], IncrementalTransform] = {} self._bound_pipe: SupportsPipe = None """Bound pipe""" + self.range_start = range_start + self.range_end = range_end @property def primary_key(self) -> Optional[TTableHintTemplate[TColumnNames]]: @@ -190,22 +202,6 @@ def primary_key(self, value: str) -> None: for transform in self._transformers.values(): transform.primary_key = value - def _make_transforms(self) -> None: - types = [("arrow", ArrowIncremental), ("json", JsonIncremental)] - for dt, kls in types: - self._transformers[dt] = kls( - self.resource_name, - self.cursor_path, - self.initial_value, - self.start_value, - self.end_value, - self.last_value_func, - self._primary_key, - set(self._cached_state["unique_hashes"]), - self.on_cursor_value_missing, - self.lag, - ) - @classmethod def from_existing_state( cls, resource_name: str, cursor_path: str @@ -489,7 +485,8 @@ def bind(self, pipe: SupportsPipe) -> "Incremental[TCursorValue]": ) # cache state self._cached_state = self.get_state() - self._make_transforms() + # Clear transforms so we get new instances + self._transformers.clear() return self def can_close(self) -> bool: @@ -520,15 +517,34 @@ def __str__(self) -> str: f" {self.last_value_func}" ) + def _make_or_get_transformer(self, cls: Type[IncrementalTransform]) -> IncrementalTransform: + if transformer := self._transformers.get(cls): + return transformer + transformer = self._transformers[cls] = cls( + self.resource_name, + self.cursor_path, + self.initial_value, + self.start_value, + self.end_value, + self.last_value_func, + self._primary_key, + set(self._cached_state["unique_hashes"]), + self.on_cursor_value_missing, + self.lag, + self.range_start, + self.range_end, + ) + return transformer + def _get_transformer(self, items: TDataItems) -> IncrementalTransform: # Assume list is all of the same type for item in items if isinstance(items, list) else [items]: if is_arrow_item(item): - return self._transformers["arrow"] + return self._make_or_get_transformer(ArrowIncremental) elif pandas is not None and isinstance(item, pandas.DataFrame): - return self._transformers["arrow"] - return self._transformers["json"] - return self._transformers["json"] + return self._make_or_get_transformer(ArrowIncremental) + return self._make_or_get_transformer(JsonIncremental) + return self._make_or_get_transformer(JsonIncremental) def __call__(self, rows: TDataItems, meta: Any = None) -> Optional[TDataItems]: if rows is None: diff --git a/dlt/extract/incremental/transform.py b/dlt/extract/incremental/transform.py index 22b1194b51..1d213e26c2 100644 --- a/dlt/extract/incremental/transform.py +++ b/dlt/extract/incremental/transform.py @@ -13,7 +13,12 @@ IncrementalPrimaryKeyMissing, IncrementalCursorPathHasValueNone, ) -from dlt.common.incremental.typing import TCursorValue, LastValueFunc, OnCursorValueMissing +from dlt.common.incremental.typing import ( + TCursorValue, + LastValueFunc, + OnCursorValueMissing, + TIncrementalRange, +) from dlt.extract.utils import resolve_column_value from dlt.extract.items import TTableHintTemplate @@ -57,6 +62,8 @@ def __init__( unique_hashes: Set[str], on_cursor_value_missing: OnCursorValueMissing = "raise", lag: Optional[float] = None, + range_start: TIncrementalRange = "closed", + range_end: TIncrementalRange = "open", ) -> None: self.resource_name = resource_name self.cursor_path = cursor_path @@ -71,6 +78,9 @@ def __init__( self.start_unique_hashes = set(unique_hashes) self.on_cursor_value_missing = on_cursor_value_missing self.lag = lag + self.range_start = range_start + self.range_end = range_end + # compile jsonpath self._compiled_cursor_path = compile_path(cursor_path) # for simple column name we'll fallback to search in dict @@ -107,6 +117,8 @@ def __call__( def deduplication_disabled(self) -> bool: """Skip deduplication when length of the key is 0 or if lag is applied.""" # disable deduplication if end value is set - state is not saved + if self.range_start == "open": + return True if self.end_value is not None: return True # disable deduplication if lag is applied - destination must deduplicate ranges @@ -191,10 +203,10 @@ def __call__( # Filter end value ranges exclusively, so in case of "max" function we remove values >= end_value if self.end_value is not None: try: - if ( - last_value_func((row_value, self.end_value)) != self.end_value - or last_value_func((row_value,)) == self.end_value - ): + if last_value_func((row_value, self.end_value)) != self.end_value: + return None, False, True + + if self.range_end == "open" and last_value_func((row_value,)) == self.end_value: return None, False, True except Exception as ex: raise IncrementalCursorInvalidCoercion( @@ -221,6 +233,9 @@ def __call__( ) from ex # new_value is "less" or equal to last_value (the actual max) if last_value == new_value: + if self.range_start == "open": + # We only want greater than last_value + return None, False, False # use func to compute row_value into last_value compatible processed_row_value = last_value_func((row_value,)) # skip the record that is not a start_value or new_value: that record was already processed @@ -258,6 +273,31 @@ def __call__( class ArrowIncremental(IncrementalTransform): _dlt_index = "_dlt_index" + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + if self.last_value_func is max: + self.compute = pa.compute.max + self.end_compare = ( + pa.compute.less if self.range_end == "open" else pa.compute.less_equal + ) + self.last_value_compare = ( + pa.compute.greater_equal if self.range_start == "closed" else pa.compute.greater + ) + self.new_value_compare = pa.compute.greater + elif self.last_value_func is min: + self.compute = pa.compute.min + self.end_compare = ( + pa.compute.greater if self.range_end == "open" else pa.compute.greater_equal + ) + self.last_value_compare = ( + pa.compute.less_equal if self.range_start == "closed" else pa.compute.less + ) + self.new_value_compare = pa.compute.less + else: + raise NotImplementedError( + "Only min or max last_value_func is supported for arrow tables" + ) + def compute_unique_values(self, item: "TAnyArrowItem", unique_columns: List[str]) -> List[str]: if not unique_columns: return [] @@ -312,28 +352,13 @@ def __call__( if not tbl: # row is None or empty arrow table return tbl, start_out_of_range, end_out_of_range - if self.last_value_func is max: - compute = pa.compute.max - end_compare = pa.compute.less - last_value_compare = pa.compute.greater_equal - new_value_compare = pa.compute.greater - elif self.last_value_func is min: - compute = pa.compute.min - end_compare = pa.compute.greater - last_value_compare = pa.compute.less_equal - new_value_compare = pa.compute.less - else: - raise NotImplementedError( - "Only min or max last_value_func is supported for arrow tables" - ) - # TODO: Json path support. For now assume the cursor_path is a column name cursor_path = self.cursor_path # The new max/min value try: # NOTE: datetimes are always pendulum in UTC - row_value = from_arrow_scalar(compute(tbl[cursor_path])) + row_value = from_arrow_scalar(self.compute(tbl[cursor_path])) cursor_data_type = tbl.schema.field(cursor_path).type row_value_scalar = to_arrow_scalar(row_value, cursor_data_type) except KeyError as e: @@ -364,10 +389,10 @@ def __call__( cursor_data_type, str(ex), ) from ex - tbl = tbl.filter(end_compare(tbl[cursor_path], end_value_scalar)) + tbl = tbl.filter(self.end_compare(tbl[cursor_path], end_value_scalar)) # Is max row value higher than end value? # NOTE: pyarrow bool *always* evaluates to python True. `as_py()` is necessary - end_out_of_range = not end_compare(row_value_scalar, end_value_scalar).as_py() + end_out_of_range = not self.end_compare(row_value_scalar, end_value_scalar).as_py() if self.start_value is not None: try: @@ -383,7 +408,7 @@ def __call__( str(ex), ) from ex # Remove rows lower or equal than the last start value - keep_filter = last_value_compare(tbl[cursor_path], start_value_scalar) + keep_filter = self.last_value_compare(tbl[cursor_path], start_value_scalar) start_out_of_range = bool(pa.compute.any(pa.compute.invert(keep_filter)).as_py()) tbl = tbl.filter(keep_filter) if not self.deduplication_disabled: @@ -407,7 +432,7 @@ def __call__( if ( self.last_value is None - or new_value_compare( + or self.new_value_compare( row_value_scalar, to_arrow_scalar(self.last_value, cursor_data_type) ).as_py() ): # Last value has changed diff --git a/dlt/sources/sql_database/helpers.py b/dlt/sources/sql_database/helpers.py index a8be2a6427..ee38c7dd98 100644 --- a/dlt/sources/sql_database/helpers.py +++ b/dlt/sources/sql_database/helpers.py @@ -94,12 +94,16 @@ def __init__( self.end_value = incremental.end_value self.row_order: TSortOrder = self.incremental.row_order self.on_cursor_value_missing = self.incremental.on_cursor_value_missing + self.range_start = self.incremental.range_start + self.range_end = self.incremental.range_end else: self.cursor_column = None self.last_value = None self.end_value = None self.row_order = None self.on_cursor_value_missing = None + self.range_start = None + self.range_end = None def _make_query(self) -> SelectAny: table = self.table @@ -110,11 +114,11 @@ def _make_query(self) -> SelectAny: # generate where if last_value_func is max: # Query ordered and filtered according to last_value function - filter_op = operator.ge - filter_op_end = operator.lt + filter_op = operator.ge if self.range_start == "closed" else operator.gt + filter_op_end = operator.lt if self.range_end == "open" else operator.le elif last_value_func is min: - filter_op = operator.le - filter_op_end = operator.gt + filter_op = operator.le if self.range_start == "closed" else operator.lt + filter_op_end = operator.gt if self.range_end == "open" else operator.ge else: # Custom last_value, load everything and let incremental handle filtering return query # type: ignore[no-any-return] diff --git a/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md b/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md index 6ff3a267d2..c532f6d357 100644 --- a/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md +++ b/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md @@ -16,7 +16,7 @@ Efficient data management often requires loading only new or updated data from y Incremental loading uses a cursor column (e.g., timestamp or auto-incrementing ID) to load only data newer than a specified initial value, enhancing efficiency by reducing processing time and resource use. Read [here](../../../walkthroughs/sql-incremental-configuration) for more details on incremental loading with `dlt`. -#### How to configure +### How to configure 1. **Choose a cursor column**: Identify a column in your SQL table that can serve as a reliable indicator of new or updated rows. Common choices include timestamp columns or auto-incrementing IDs. 1. **Set an initial value**: Choose a starting value for the cursor to begin loading data. This could be a specific timestamp or ID from which you wish to start loading data. 1. **Deduplication**: When using incremental loading, the system automatically handles the deduplication of rows based on the primary key (if available) or row hash for tables without a primary key. @@ -27,7 +27,7 @@ Incremental loading uses a cursor column (e.g., timestamp or auto-incrementing I If your cursor column name contains special characters (e.g., `$`) you need to escape it when passing it to the `incremental` function. For example, if your cursor column is `example_$column`, you should pass it as `"'example_$column'"` or `'"example_$column"'` to the `incremental` function: `incremental("'example_$column'", initial_value=...)`. ::: -#### Examples +### Examples 1. **Incremental loading with the resource `sql_table`**. @@ -52,7 +52,7 @@ If your cursor column name contains special characters (e.g., `$`) you need to e print(extract_info) ``` - Behind the scene, the loader generates a SQL query filtering rows with `last_modified` values greater than the incremental value. In the first run, this is the initial value (midnight (00:00:00) January 1, 2024). + Behind the scene, the loader generates a SQL query filtering rows with `last_modified` values greater or equal to the incremental value. In the first run, this is the initial value (midnight (00:00:00) January 1, 2024). In subsequent runs, it is the latest value of `last_modified` that `dlt` stores in [state](../../../general-usage/state). 2. **Incremental loading with the source `sql_database`**. @@ -78,6 +78,49 @@ If your cursor column name contains special characters (e.g., `$`) you need to e * `apply_hints` is a powerful method that enables schema modifications after resource creation, like adjusting write disposition and primary keys. You can choose from various tables and use `apply_hints` multiple times to create pipelines with merged, appended, or replaced resources. ::: +### Inclusive and exclusive filtering + +By default the incremental filtering is inclusive on the start value side so that +rows with cursor equal to the last run's cursor are fetched again from the database. + +The SQL query generated looks something like this (assuming `last_value_func` is `max`): + +```sql +SELECT * FROM family +WHERE last_modified >= :start_value +ORDER BY last_modified ASC +``` + +That means some rows overlapping with the previous load are fetched from the database. +Duplicates are then filtered out by dlt using either the primary key or a hash of the row's contents. + +This ensures there are no gaps in the extracted sequence. But it does come with some performance overhead, +both due to the deduplication processing and the cost of fetching redundant records from the database. + +This is not always needed. If you know that your data does not contain overlapping cursor values then you +can optimize extraction by passing `range_start="open"` to incremental. + +This both disables the deduplication process and changes the operator used in the SQL `WHERE` clause from `>=` (greater-or-equal) to `>` (greater than), so that no overlapping rows are fetched. + +E.g. + +```py +table = sql_table( + table='family', + incremental=dlt.sources.incremental( + 'last_modified', # Cursor column name + initial_value=pendulum.DateTime(2024, 1, 1, 0, 0, 0), # Initial cursor value + range_start="open", # exclude the start value + ) +) +``` + +It's a good option if: + +* The cursor is an auto incrementing ID +* The cursor is a high precision timestamp and two records are never created at exactly the same time +* Your pipeline runs are timed in such a way that new data is not generated during the load + ## Parallelized extraction You can extract each table in a separate thread (no multiprocessing at this point). This will decrease loading time if your queries take time to execute or your network latency/speed is low. To enable this, declare your sources/resources as follows: diff --git a/docs/website/docs/general-usage/incremental-loading.md b/docs/website/docs/general-usage/incremental-loading.md index 3f452f0d16..5008795ed4 100644 --- a/docs/website/docs/general-usage/incremental-loading.md +++ b/docs/website/docs/general-usage/incremental-loading.md @@ -693,7 +693,7 @@ august_issues = repo_issues( ... ``` -Note that dlt's incremental filtering considers the ranges half-closed. `initial_value` is inclusive, `end_value` is exclusive, so chaining ranges like above works without overlaps. +Note that dlt's incremental filtering considers the ranges half-closed. `initial_value` is inclusive, `end_value` is exclusive, so chaining ranges like above works without overlaps. This behaviour can be changed with the `range_start` (default `"closed"`) and `range_end` (default `"open"`) arguments. ### Declare row order to not request unnecessary data @@ -793,6 +793,9 @@ def some_data(last_timestamp=dlt.sources.incremental("item.ts", primary_key=())) yield {"delta": i, "item": {"ts": pendulum.now().timestamp()}} ``` +This deduplication process is always enabled when `range_start` is set to `"closed"` (default). +When you pass `range_start="open"` no deduplication is done as it is not needed as rows with the previous cursor value are excluded. This can be a useful optimization to avoid the performance overhead of deduplication if the cursor field is guaranteed to be unique. + ### Using `dlt.sources.incremental` with dynamically created resources When resources are [created dynamically](source.md#create-resources-dynamically), it is possible to use the `dlt.sources.incremental` definition as well. diff --git a/tests/extract/test_incremental.py b/tests/extract/test_incremental.py index 725872b621..3ebc9d1201 100644 --- a/tests/extract/test_incremental.py +++ b/tests/extract/test_incremental.py @@ -5,7 +5,7 @@ from datetime import datetime, date # noqa: I251 from itertools import chain, count from time import sleep -from typing import Any, Optional, Literal, Sequence, Dict +from typing import Any, Optional, Literal, Sequence, Dict, Iterable from unittest import mock import duckdb @@ -1522,6 +1522,7 @@ def some_data(last_timestamp=dlt.sources.incremental("ts", primary_key=())): @pytest.mark.parametrize("item_type", ALL_TEST_DATA_ITEM_FORMATS) def test_apply_hints_incremental(item_type: TestDataItemFormat) -> None: + os.environ["COMPLETED_PROB"] = "1.0" # make it complete immediately p = dlt.pipeline(pipeline_name=uniq_id(), destination="dummy") data = [{"created_at": 1}, {"created_at": 2}, {"created_at": 3}] source_items = data_to_item_format(item_type, data) @@ -3851,3 +3852,111 @@ def some_data(): for col in table_schema["columns"].values(): assert "incremental" not in col + + +@pytest.mark.parametrize("item_type", ALL_TEST_DATA_ITEM_FORMATS) +@pytest.mark.parametrize("last_value_func", [min, max]) +def test_start_range_open(item_type: TestDataItemFormat, last_value_func: Any) -> None: + data_range: Iterable[int] = range(1, 12) + if last_value_func == max: + initial_value = 5 + # Only items higher than inital extracted + expected_items = list(range(6, 12)) + order_dir = "ASC" + elif last_value_func == min: + data_range = reversed(data_range) # type: ignore[call-overload] + initial_value = 5 + # Only items lower than inital extracted + expected_items = list(reversed(range(1, 5))) + order_dir = "DESC" + + @dlt.resource + def some_data( + updated_at: dlt.sources.incremental[int] = dlt.sources.incremental( + "updated_at", + initial_value=initial_value, + range_start="open", + last_value_func=last_value_func, + ), + ) -> Any: + data = [{"updated_at": i} for i in data_range] + yield data_to_item_format(item_type, data) + + pipeline = dlt.pipeline(pipeline_name=uniq_id(), destination="duckdb") + pipeline.run(some_data()) + + with pipeline.sql_client() as client: + items = [ + row[0] + for row in client.execute_sql( + f"SELECT updated_at FROM some_data ORDER BY updated_at {order_dir}" + ) + ] + + assert items == expected_items + + +@pytest.mark.parametrize("item_type", ALL_TEST_DATA_ITEM_FORMATS) +def test_start_range_open_no_deduplication(item_type: TestDataItemFormat) -> None: + @dlt.source + def dummy(): + @dlt.resource + def some_data( + updated_at: dlt.sources.incremental[int] = dlt.sources.incremental( + "updated_at", + range_start="open", + ) + ): + yield [{"updated_at": i} for i in range(3)] + + yield some_data + + pipeline = dlt.pipeline(pipeline_name=uniq_id()) + pipeline.extract(dummy()) + + state = pipeline.state["sources"]["dummy"]["resources"]["some_data"]["incremental"][ + "updated_at" + ] + + # No unique values should be computed + assert state["unique_hashes"] == [] + + +@pytest.mark.parametrize("item_type", ALL_TEST_DATA_ITEM_FORMATS) +@pytest.mark.parametrize("last_value_func", [min, max]) +def test_end_range_closed(item_type: TestDataItemFormat, last_value_func: Any) -> None: + values = [5, 10] + expected_items = list(range(5, 11)) + if last_value_func == max: + order_dir = "ASC" + elif last_value_func == min: + values = list(reversed(values)) + expected_items = list(reversed(expected_items)) + order_dir = "DESC" + + @dlt.resource + def some_data( + updated_at: dlt.sources.incremental[int] = dlt.sources.incremental( + "updated_at", + initial_value=values[0], + end_value=values[1], + range_end="closed", + last_value_func=last_value_func, + ), + ) -> Any: + data = [{"updated_at": i} for i in range(1, 12)] + yield data_to_item_format(item_type, data) + + pipeline = dlt.pipeline(pipeline_name=uniq_id(), destination="duckdb") + pipeline.run(some_data()) + + with pipeline.sql_client() as client: + items = [ + row[0] + for row in client.execute_sql( + f"SELECT updated_at FROM some_data ORDER BY updated_at {order_dir}" + ) + ] + + # Includes values 5-10 inclusive + assert items == expected_items diff --git a/tests/load/sources/sql_database/test_helpers.py b/tests/load/sources/sql_database/test_helpers.py index def5430146..43da9c955f 100644 --- a/tests/load/sources/sql_database/test_helpers.py +++ b/tests/load/sources/sql_database/test_helpers.py @@ -1,3 +1,6 @@ +from typing import Callable, Any, TYPE_CHECKING +from dataclasses import dataclass + import pytest import dlt @@ -14,6 +17,18 @@ pytest.skip("Tests require sql alchemy", allow_module_level=True) +@dataclass +class MockIncremental: + last_value: Any + last_value_func: Callable[[Any], Any] + cursor_path: str + row_order: str = None + end_value: Any = None + on_cursor_value_missing: str = "raise" + range_start: str = "closed" + range_end: str = "open" + + @pytest.mark.parametrize("backend", ["sqlalchemy", "pyarrow", "pandas", "connectorx"]) def test_cursor_or_unique_column_not_in_table( sql_source_db: SQLAlchemySourceDB, backend: TableBackend @@ -36,13 +51,12 @@ def test_make_query_incremental_max( ) -> None: """Verify query is generated according to incremental settings""" - class MockIncremental: - last_value = dlt.common.pendulum.now() - last_value_func = max - cursor_path = "created_at" - row_order = "asc" - end_value = None - on_cursor_value_missing = "raise" + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=max, + cursor_path="created_at", + row_order="asc", + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -50,14 +64,14 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() expected = ( table.select() .order_by(table.c.created_at.asc()) - .where(table.c.created_at >= MockIncremental.last_value) + .where(table.c.created_at >= incremental.last_value) ) assert query.compare(expected) @@ -67,13 +81,14 @@ class MockIncremental: def test_make_query_incremental_min( sql_source_db: SQLAlchemySourceDB, backend: TableBackend ) -> None: - class MockIncremental: - last_value = dlt.common.pendulum.now() - last_value_func = min - cursor_path = "created_at" - row_order = "desc" - end_value = None - on_cursor_value_missing = "raise" + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=min, + cursor_path="created_at", + row_order="desc", + end_value=None, + on_cursor_value_missing="raise", + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -81,14 +96,14 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() expected = ( table.select() .order_by(table.c.created_at.asc()) # `min` func swaps order - .where(table.c.created_at <= MockIncremental.last_value) + .where(table.c.created_at <= incremental.last_value) ) assert query.compare(expected) @@ -103,13 +118,14 @@ def test_make_query_incremental_on_cursor_value_missing_set( with_end_value: bool, cursor_value_missing: str, ) -> None: - class MockIncremental: - last_value = dlt.common.pendulum.now() - last_value_func = max - cursor_path = "created_at" - row_order = "asc" - end_value = None if not with_end_value else dlt.common.pendulum.now().add(hours=1) - on_cursor_value_missing = cursor_value_missing + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=max, + cursor_path="created_at", + row_order="asc", + end_value=None if not with_end_value else dlt.common.pendulum.now().add(hours=1), + on_cursor_value_missing=cursor_value_missing, + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -117,7 +133,7 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() @@ -131,14 +147,14 @@ class MockIncremental: if with_end_value: where_clause = operator( sa.and_( - table.c.created_at >= MockIncremental.last_value, - table.c.created_at < MockIncremental.end_value, + table.c.created_at >= incremental.last_value, + table.c.created_at < incremental.end_value, ), missing_cond, ) else: where_clause = operator( - table.c.created_at >= MockIncremental.last_value, + table.c.created_at >= incremental.last_value, missing_cond, ) expected = table.select().order_by(table.c.created_at.asc()).where(where_clause) @@ -152,13 +168,14 @@ def test_make_query_incremental_on_cursor_value_missing_no_last_value( backend: TableBackend, cursor_value_missing: str, ) -> None: - class MockIncremental: - last_value = None - last_value_func = max - cursor_path = "created_at" - row_order = "asc" - end_value = None - on_cursor_value_missing = cursor_value_missing + incremental = MockIncremental( + last_value=None, + last_value_func=max, + cursor_path="created_at", + row_order="asc", + end_value=None, + on_cursor_value_missing=cursor_value_missing, + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -166,7 +183,7 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() @@ -189,13 +206,14 @@ def test_make_query_incremental_end_value( ) -> None: now = dlt.common.pendulum.now() - class MockIncremental: - last_value = now - last_value_func = min - cursor_path = "created_at" - end_value = now.add(hours=1) - row_order = None - on_cursor_value_missing = "raise" + incremental = MockIncremental( + last_value=now, + last_value_func=min, + cursor_path="created_at", + end_value=now.add(hours=1), + row_order=None, + on_cursor_value_missing="raise", + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -203,14 +221,14 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() expected = table.select().where( sa.and_( - table.c.created_at <= MockIncremental.last_value, - table.c.created_at > MockIncremental.end_value, + table.c.created_at <= incremental.last_value, + table.c.created_at > incremental.end_value, ) ) @@ -221,13 +239,14 @@ class MockIncremental: def test_make_query_incremental_any_fun( sql_source_db: SQLAlchemySourceDB, backend: TableBackend ) -> None: - class MockIncremental: - last_value = dlt.common.pendulum.now() - last_value_func = lambda x: x[-1] - cursor_path = "created_at" - row_order = "asc" - end_value = dlt.common.pendulum.now() - on_cursor_value_missing = "raise" + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=lambda x: x[-1], + cursor_path="created_at", + row_order="asc", + end_value=dlt.common.pendulum.now(), + on_cursor_value_missing="raise", + ) table = sql_source_db.get_table("chat_message") loader = TableLoader( @@ -235,7 +254,7 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) query = loader.make_query() @@ -256,12 +275,11 @@ def test_cursor_path_field_name_with_a_special_chars( if special_field_name not in table.c: table.append_column(sa.Column(special_field_name, sa.String)) - class MockIncremental: - cursor_path = "'id$field'" - last_value = None - end_value = None - row_order = None - on_cursor_value_missing = None + incremental = MockIncremental( + cursor_path="'id$field'", + last_value=None, + last_value_func=max, + ) # Should not raise any exception loader = TableLoader( @@ -269,7 +287,7 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) assert loader.cursor_column == table.c[special_field_name] @@ -281,12 +299,11 @@ def test_cursor_path_multiple_fields( """Test that a cursor_path with multiple fields raises a ValueError.""" table = sql_source_db.get_table("chat_message") - class MockIncremental: - cursor_path = "created_at,updated_at" - last_value = None - end_value = None - row_order = None - on_cursor_value_missing = None + incremental = MockIncremental( + cursor_path="created_at,updated_at", + last_value=None, + last_value_func=max, + ) with pytest.raises(ValueError) as excinfo: TableLoader( @@ -294,7 +311,7 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) assert "must be a simple column name" in str(excinfo.value) @@ -306,12 +323,11 @@ def test_cursor_path_complex_expression( """Test that a complex JSONPath expression in cursor_path raises a ValueError.""" table = sql_source_db.get_table("chat_message") - class MockIncremental: - cursor_path = "$.users[0].id" - last_value = None - end_value = None - row_order = None - on_cursor_value_missing = None + incremental = MockIncremental( + cursor_path="$.users[0].id", + last_value=None, + last_value_func=max, + ) with pytest.raises(ValueError) as excinfo: TableLoader( @@ -319,11 +335,80 @@ class MockIncremental: backend, table, table_to_columns(table), - incremental=MockIncremental(), # type: ignore[arg-type] + incremental=incremental, # type: ignore[arg-type] ) assert "must be a simple column name" in str(excinfo.value) +@pytest.mark.parametrize("backend", ["sqlalchemy", "pyarrow", "pandas", "connectorx"]) +@pytest.mark.parametrize("last_value_func", [min, max]) +def test_make_query_incremental_range_start_open( + sql_source_db: SQLAlchemySourceDB, backend: TableBackend, last_value_func: Callable[[Any], Any] +) -> None: + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=last_value_func, + cursor_path="created_at", + end_value=None, + on_cursor_value_missing="raise", + range_start="open", + ) + + table = sql_source_db.get_table("chat_message") + + loader = TableLoader( + sql_source_db.engine, + backend, + table, + table_to_columns(table), + incremental=incremental, # type: ignore[arg-type] + ) + + query = loader.make_query() + expected = table.select() + + if last_value_func == min: + expected = expected.where(table.c.created_at < incremental.last_value) + else: + expected = expected.where(table.c.created_at > incremental.last_value) + + assert query.compare(expected) + + +@pytest.mark.parametrize("backend", ["sqlalchemy", "pyarrow", "pandas", "connectorx"]) +@pytest.mark.parametrize("last_value_func", [min, max]) +def test_make_query_incremental_range_end_closed( + sql_source_db: SQLAlchemySourceDB, backend: TableBackend, last_value_func: Callable[[Any], Any] +) -> None: + incremental = MockIncremental( + last_value=dlt.common.pendulum.now(), + last_value_func=last_value_func, + cursor_path="created_at", + end_value=None, + on_cursor_value_missing="raise", + range_end="closed", + ) + + table = sql_source_db.get_table("chat_message") + loader = TableLoader( + sql_source_db.engine, + backend, + table, + table_to_columns(table), + incremental=incremental, # type: ignore[arg-type] + ) + + query = loader.make_query() + expected = table.select() + + if last_value_func == min: + expected = expected.where(table.c.created_at <= incremental.last_value) + else: + expected = expected.where(table.c.created_at >= incremental.last_value) + + assert query.compare(expected) + + def mock_json_column(field: str) -> TDataItem: """""" import pyarrow as pa diff --git a/tests/load/sources/sql_database/test_sql_database_source.py b/tests/load/sources/sql_database/test_sql_database_source.py index 9079638586..00257471e0 100644 --- a/tests/load/sources/sql_database/test_sql_database_source.py +++ b/tests/load/sources/sql_database/test_sql_database_source.py @@ -13,6 +13,7 @@ from dlt.common.utils import uniq_id from dlt.extract.exceptions import ResourceExtractionError +from dlt.extract.incremental.transform import JsonIncremental, ArrowIncremental from dlt.sources import DltResource from tests.pipeline.utils import ( @@ -831,8 +832,12 @@ def _assert_incremental(item): else: assert _r.incremental.primary_key == ["id"] assert _r.incremental._incremental.primary_key == ["id"] - assert _r.incremental._incremental._transformers["json"].primary_key == ["id"] - assert _r.incremental._incremental._transformers["arrow"].primary_key == ["id"] + assert _r.incremental._incremental._make_or_get_transformer( + JsonIncremental + ).primary_key == ["id"] + assert _r.incremental._incremental._make_or_get_transformer( + ArrowIncremental + ).primary_key == ["id"] return item pipeline = make_pipeline("duckdb") @@ -841,8 +846,12 @@ def _assert_incremental(item): assert resource.incremental.primary_key == ["id"] assert resource.incremental._incremental.primary_key == ["id"] - assert resource.incremental._incremental._transformers["json"].primary_key == ["id"] - assert resource.incremental._incremental._transformers["arrow"].primary_key == ["id"] + assert resource.incremental._incremental._make_or_get_transformer( + JsonIncremental + ).primary_key == ["id"] + assert resource.incremental._incremental._make_or_get_transformer( + ArrowIncremental + ).primary_key == ["id"] @pytest.mark.parametrize("backend", ["sqlalchemy", "pyarrow", "pandas", "connectorx"]) From 80ef80401b97646901b48e15dade262ef5c3fd52 Mon Sep 17 00:00:00 2001 From: David Scharf Date: Tue, 10 Dec 2024 23:44:01 +0100 Subject: [PATCH 05/23] bump semver to minimum version 3.0.0 (#2132) --- poetry.lock | 104 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 732ba0e219..6232b383c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "about-time" @@ -3900,6 +3900,106 @@ files = [ {file = "google_re2-1.1-4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f4d4f0823e8b2f6952a145295b1ff25245ce9bb136aff6fe86452e507d4c1dd"}, {file = "google_re2-1.1-4-cp39-cp39-win32.whl", hash = "sha256:1afae56b2a07bb48cfcfefaa15ed85bae26a68f5dc7f9e128e6e6ea36914e847"}, {file = "google_re2-1.1-4-cp39-cp39-win_amd64.whl", hash = "sha256:aa7d6d05911ab9c8adbf3c225a7a120ab50fd2784ac48f2f0d140c0b7afc2b55"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:222fc2ee0e40522de0b21ad3bc90ab8983be3bf3cec3d349c80d76c8bb1a4beb"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d4763b0b9195b72132a4e7de8e5a9bf1f05542f442a9115aa27cfc2a8004f581"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:209649da10c9d4a93d8a4d100ecbf9cc3b0252169426bec3e8b4ad7e57d600cf"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:68813aa333c1604a2df4a495b2a6ed065d7c8aebf26cc7e7abb5a6835d08353c"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:370a23ec775ad14e9d1e71474d56f381224dcf3e72b15d8ca7b4ad7dd9cd5853"}, + {file = "google_re2-1.1-5-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:14664a66a3ddf6bc9e56f401bf029db2d169982c53eff3f5876399104df0e9a6"}, + {file = "google_re2-1.1-5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea3722cc4932cbcebd553b69dce1b4a73572823cff4e6a244f1c855da21d511"}, + {file = "google_re2-1.1-5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e14bb264c40fd7c627ef5678e295370cd6ba95ca71d835798b6e37502fc4c690"}, + {file = "google_re2-1.1-5-cp310-cp310-win32.whl", hash = "sha256:39512cd0151ea4b3969c992579c79b423018b464624ae955be685fc07d94556c"}, + {file = "google_re2-1.1-5-cp310-cp310-win_amd64.whl", hash = "sha256:ac66537aa3bc5504320d922b73156909e3c2b6da19739c866502f7827b3f9fdf"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5b5ea68d54890c9edb1b930dcb2658819354e5d3f2201f811798bbc0a142c2b4"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:33443511b6b83c35242370908efe2e8e1e7cae749c766b2b247bf30e8616066c"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:413d77bdd5ba0bfcada428b4c146e87707452ec50a4091ec8e8ba1413d7e0619"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:5171686e43304996a34baa2abcee6f28b169806d0e583c16d55e5656b092a414"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b284db130283771558e31a02d8eb8fb756156ab98ce80035ae2e9e3a5f307c4"}, + {file = "google_re2-1.1-5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:296e6aed0b169648dc4b870ff47bd34c702a32600adb9926154569ef51033f47"}, + {file = "google_re2-1.1-5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38d50e68ead374160b1e656bbb5d101f0b95fb4cc57f4a5c12100155001480c5"}, + {file = "google_re2-1.1-5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a0416a35921e5041758948bcb882456916f22845f66a93bc25070ef7262b72a"}, + {file = "google_re2-1.1-5-cp311-cp311-win32.whl", hash = "sha256:a1d59568bbb5de5dd56dd6cdc79907db26cce63eb4429260300c65f43469e3e7"}, + {file = "google_re2-1.1-5-cp311-cp311-win_amd64.whl", hash = "sha256:72f5a2f179648b8358737b2b493549370debd7d389884a54d331619b285514e3"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cbc72c45937b1dc5acac3560eb1720007dccca7c9879138ff874c7f6baf96005"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:5fadd1417fbef7235fa9453dba4eb102e6e7d94b1e4c99d5fa3dd4e288d0d2ae"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:040f85c63cc02696485b59b187a5ef044abe2f99b92b4fb399de40b7d2904ccc"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:64e3b975ee6d9bbb2420494e41f929c1a0de4bcc16d86619ab7a87f6ea80d6bd"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8ee370413e00f4d828eaed0e83b8af84d7a72e8ee4f4bd5d3078bc741dfc430a"}, + {file = "google_re2-1.1-5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:5b89383001079323f693ba592d7aad789d7a02e75adb5d3368d92b300f5963fd"}, + {file = "google_re2-1.1-5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cb4fdfbbda16ae31b41a6388ea621510db82feb8217a74bf36552ecfcd50ad"}, + {file = "google_re2-1.1-5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ebedd84ae8be10b7a71a16162376fd67a2386fe6361ef88c622dcf7fd679daf"}, + {file = "google_re2-1.1-5-cp312-cp312-win32.whl", hash = "sha256:c8e22d1692bc2c81173330c721aff53e47ffd3c4403ff0cd9d91adfd255dd150"}, + {file = "google_re2-1.1-5-cp312-cp312-win_amd64.whl", hash = "sha256:5197a6af438bb8c4abda0bbe9c4fbd6c27c159855b211098b29d51b73e4cbcf6"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b6727e0b98417e114b92688ad2aa256102ece51f29b743db3d831df53faf1ce3"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:711e2b6417eb579c61a4951029d844f6b95b9b373b213232efd413659889a363"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_13_0_arm64.whl", hash = "sha256:71ae8b3df22c5c154c8af0f0e99d234a450ef1644393bc2d7f53fc8c0a1e111c"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:94a04e214bc521a3807c217d50cf099bbdd0c0a80d2d996c0741dbb995b5f49f"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_14_0_arm64.whl", hash = "sha256:a770f75358508a9110c81a1257721f70c15d9bb592a2fb5c25ecbd13566e52a5"}, + {file = "google_re2-1.1-5-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:07c9133357f7e0b17c6694d5dcb82e0371f695d7c25faef2ff8117ef375343ff"}, + {file = "google_re2-1.1-5-cp38-cp38-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:204ca6b1cf2021548f4a9c29ac015e0a4ab0a7b6582bf2183d838132b60c8fda"}, + {file = "google_re2-1.1-5-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b95857c2c654f419ca684ec38c9c3325c24e6ba7d11910a5110775a557bb18"}, + {file = "google_re2-1.1-5-cp38-cp38-win32.whl", hash = "sha256:347ac770e091a0364e822220f8d26ab53e6fdcdeaec635052000845c5a3fb869"}, + {file = "google_re2-1.1-5-cp38-cp38-win_amd64.whl", hash = "sha256:ec32bb6de7ffb112a07d210cf9f797b7600645c2d5910703fa07f456dd2150e0"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:eb5adf89060f81c5ff26c28e261e6b4997530a923a6093c9726b8dec02a9a326"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:a22630c9dd9ceb41ca4316bccba2643a8b1d5c198f21c00ed5b50a94313aaf10"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:544dc17fcc2d43ec05f317366375796351dec44058e1164e03c3f7d050284d58"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:19710af5ea88751c7768575b23765ce0dfef7324d2539de576f75cdc319d6654"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:f82995a205e08ad896f4bd5ce4847c834fab877e1772a44e5f262a647d8a1dec"}, + {file = "google_re2-1.1-5-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:63533c4d58da9dc4bc040250f1f52b089911699f0368e0e6e15f996387a984ed"}, + {file = "google_re2-1.1-5-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79e00fcf0cb04ea35a22b9014712d448725ce4ddc9f08cc818322566176ca4b0"}, + {file = "google_re2-1.1-5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc41afcefee2da6c4ed883a93d7f527c4b960cd1d26bbb0020a7b8c2d341a60a"}, + {file = "google_re2-1.1-5-cp39-cp39-win32.whl", hash = "sha256:486730b5e1f1c31b0abc6d80abe174ce4f1188fe17d1b50698f2bf79dc6e44be"}, + {file = "google_re2-1.1-5-cp39-cp39-win_amd64.whl", hash = "sha256:4de637ca328f1d23209e80967d1b987d6b352cd01b3a52a84b4d742c69c3da6c"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:621e9c199d1ff0fdb2a068ad450111a84b3bf14f96dfe5a8a7a0deae5f3f4cce"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:220acd31e7dde95373f97c3d1f3b3bd2532b38936af28b1917ee265d25bebbf4"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:db34e1098d164f76251a6ece30e8f0ddfd65bb658619f48613ce71acb3f9cbdb"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:5152bac41d8073977582f06257219541d0fc46ad99b0bbf30e8f60198a43b08c"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6191294799e373ee1735af91f55abd23b786bdfd270768a690d9d55af9ea1b0d"}, + {file = "google_re2-1.1-6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:070cbafbb4fecbb02e98feb28a1eb292fb880f434d531f38cc33ee314b521f1f"}, + {file = "google_re2-1.1-6-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8437d078b405a59a576cbed544490fe041140f64411f2d91012e8ec05ab8bf86"}, + {file = "google_re2-1.1-6-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f00f9a9af8896040e37896d9b9fc409ad4979f1ddd85bb188694a7d95ddd1164"}, + {file = "google_re2-1.1-6-cp310-cp310-win32.whl", hash = "sha256:df26345f229a898b4fd3cafd5f82259869388cee6268fc35af16a8e2293dd4e5"}, + {file = "google_re2-1.1-6-cp310-cp310-win_amd64.whl", hash = "sha256:3665d08262c57c9b28a5bdeb88632ad792c4e5f417e5645901695ab2624f5059"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b26b869d8aa1d8fe67c42836bf3416bb72f444528ee2431cfb59c0d3e02c6ce3"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:41fd4486c57dea4f222a6bb7f1ff79accf76676a73bdb8da0fcbd5ba73f8da71"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:0ee378e2e74e25960070c338c28192377c4dd41e7f4608f2688064bd2badc41e"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a00cdbf662693367b36d075b29feb649fd7ee1b617cf84f85f2deebeda25fc64"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c09455014217a41499432b8c8f792f25f3df0ea2982203c3a8c8ca0e7895e69"}, + {file = "google_re2-1.1-6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6501717909185327935c7945e23bb5aa8fc7b6f237b45fe3647fa36148662158"}, + {file = "google_re2-1.1-6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3510b04790355f199e7861c29234081900e1e1cbf2d1484da48aa0ba6d7356ab"}, + {file = "google_re2-1.1-6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c0e64c187ca406764f9e9ad6e750d62e69ed8f75bf2e865d0bfbc03b642361c"}, + {file = "google_re2-1.1-6-cp311-cp311-win32.whl", hash = "sha256:2a199132350542b0de0f31acbb3ca87c3a90895d1d6e5235f7792bb0af02e523"}, + {file = "google_re2-1.1-6-cp311-cp311-win_amd64.whl", hash = "sha256:83bdac8ceaece8a6db082ea3a8ba6a99a2a1ee7e9f01a9d6d50f79c6f251a01d"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:81985ff894cd45ab5a73025922ac28c0707759db8171dd2f2cc7a0e856b6b5ad"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:5635af26065e6b45456ccbea08674ae2ab62494008d9202df628df3b267bc095"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:813b6f04de79f4a8fdfe05e2cb33e0ccb40fe75d30ba441d519168f9d958bd54"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:5ec2f5332ad4fd232c3f2d6748c2c7845ccb66156a87df73abcc07f895d62ead"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5a687b3b32a6cbb731647393b7c4e3fde244aa557f647df124ff83fb9b93e170"}, + {file = "google_re2-1.1-6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:39a62f9b3db5d3021a09a47f5b91708b64a0580193e5352751eb0c689e4ad3d7"}, + {file = "google_re2-1.1-6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0f0b45d4a1709cbf5d21f355e5809ac238f1ee594625a1e5ffa9ff7a09eb2b"}, + {file = "google_re2-1.1-6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64b3796a7a616c7861247bd061c9a836b5caf0d5963e5ea8022125601cf7b09"}, + {file = "google_re2-1.1-6-cp312-cp312-win32.whl", hash = "sha256:32783b9cb88469ba4cd9472d459fe4865280a6b1acdad4480a7b5081144c4eb7"}, + {file = "google_re2-1.1-6-cp312-cp312-win_amd64.whl", hash = "sha256:259ff3fd2d39035b9cbcbf375995f83fa5d9e6a0c5b94406ff1cc168ed41d6c6"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:e4711bcffe190acd29104d8ecfea0c0e42b754837de3fb8aad96e6cc3c613cdc"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:4d081cce43f39c2e813fe5990e1e378cbdb579d3f66ded5bade96130269ffd75"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_13_0_arm64.whl", hash = "sha256:4f123b54d48450d2d6b14d8fad38e930fb65b5b84f1b022c10f2913bd956f5b5"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:e1928b304a2b591a28eb3175f9db7f17c40c12cf2d4ec2a85fdf1cc9c073ff91"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_14_0_arm64.whl", hash = "sha256:3a69f76146166aec1173003c1f547931bdf288c6b135fda0020468492ac4149f"}, + {file = "google_re2-1.1-6-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:fc08c388f4ebbbca345e84a0c56362180d33d11cbe9ccfae663e4db88e13751e"}, + {file = "google_re2-1.1-6-cp38-cp38-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b057adf38ce4e616486922f2f47fc7d19c827ba0a7f69d540a3664eba2269325"}, + {file = "google_re2-1.1-6-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4138c0b933ab099e96f5d8defce4486f7dfd480ecaf7f221f2409f28022ccbc5"}, + {file = "google_re2-1.1-6-cp38-cp38-win32.whl", hash = "sha256:9693e45b37b504634b1abbf1ee979471ac6a70a0035954592af616306ab05dd6"}, + {file = "google_re2-1.1-6-cp38-cp38-win_amd64.whl", hash = "sha256:5674d437baba0ea287a5a7f8f81f24265d6ae8f8c09384e2ef7b6f84b40a7826"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:7783137cb2e04f458a530c6d0ee9ef114815c1d48b9102f023998c371a3b060e"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:a49b7153935e7a303675f4deb5f5d02ab1305adefc436071348706d147c889e0"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:a96a8bb309182090704593c60bdb369a2756b38fe358bbf0d40ddeb99c71769f"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:dff3d4be9f27ef8ec3705eed54f19ef4ab096f5876c15fe011628c69ba3b561c"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:40f818b0b39e26811fa677978112a8108269977fdab2ba0453ac4363c35d9e66"}, + {file = "google_re2-1.1-6-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:8a7e53538cdb40ef4296017acfbb05cab0c19998be7552db1cfb85ba40b171b9"}, + {file = "google_re2-1.1-6-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ee18e7569fb714e5bb8c42809bf8160738637a5e71ed5a4797757a1fb4dc4de"}, + {file = "google_re2-1.1-6-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cda4f6d1a7d5b43ea92bc395f23853fba0caf8b1e1efa6e8c48685f912fcb89"}, + {file = "google_re2-1.1-6-cp39-cp39-win32.whl", hash = "sha256:6a9cdbdc36a2bf24f897be6a6c85125876dc26fea9eb4247234aec0decbdccfd"}, + {file = "google_re2-1.1-6-cp39-cp39-win_amd64.whl", hash = "sha256:73f646cecfad7cc5b4330b4192c25f2e29730a3b8408e089ffd2078094208196"}, ] [[package]] @@ -10518,4 +10618,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.13" -content-hash = "c0607d05ab37a1a6addf3ae7264bf5972cb6ce6e46df1dcdc2da3cff72e5008e" +content-hash = "1bf3deccd929c083b880c1a82be0983430ab49f7ade247b1c5573bb8c70d9ff5" diff --git a/pyproject.toml b/pyproject.toml index 7377b03fde..f736fc65ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ requests = ">=2.26.0" pendulum = ">=2.1.2" simplejson = ">=3.17.5" PyYAML = ">=5.4.1" -semver = ">=2.13.0" +semver = ">=3.0.0" hexbytes = ">=0.2.2" tzdata = ">=2022.1" tomlkit = ">=0.11.3" From 77d8ab6ee23518213fe9da60c4275784450f98fa Mon Sep 17 00:00:00 2001 From: David Scharf Date: Wed, 11 Dec 2024 00:43:32 +0100 Subject: [PATCH 06/23] leverage ibis expression for getting readablerelations (#2046) * add ibis dataset in own class for now * make error clearer * fix some linting and fix broken test * make most destinations work with selecting the right db and catalog, transpiling sql via postgres in some cases and selecting the right dialect in others * add missing motherduck and sqlalchemy mappings * casefold identifiers for ibis wrapper calss * re-organize existing dataset code to prepare ibis relation integration * integrate ibis relation into existing code * re-order tests * fall back to default dataset if table not in schema * make dataset type selectable * add dataset type selection test and fix bug in tests * update docs for ibis expressions use * ensure a bunch of ibis operations continue working * add some more tests and typings * fix typing (with brute force get_attr typing..) * move ibis to dependency group * move ibis stuff to helpers * post devel merge, put in change from dataset, update lockfile * add ibis to sqlalchemy tests * improve docs a bit * fix ibis dep group * fix dataset snippets * fix ibis version * add support for column schema in certion query cases --------- Co-authored-by: Marcin Rudolf --- .github/workflows/test_destination_athena.yml | 2 +- .../test_destination_athena_iceberg.yml | 2 +- .../workflows/test_destination_bigquery.yml | 2 +- .../workflows/test_destination_clickhouse.yml | 2 +- .../workflows/test_destination_databricks.yml | 2 +- .github/workflows/test_destination_dremio.yml | 2 +- .../workflows/test_destination_motherduck.yml | 2 +- .github/workflows/test_destination_mssql.yml | 2 +- .../workflows/test_destination_snowflake.yml | 2 +- .../workflows/test_destination_synapse.yml | 2 +- .github/workflows/test_destinations.yml | 2 +- .github/workflows/test_local_destinations.yml | 2 +- .../test_sqlalchemy_destinations.yml | 2 +- dlt/common/destination/reference.py | 10 +- dlt/destinations/dataset.py | 412 ------------------ dlt/destinations/dataset/__init__.py | 19 + dlt/destinations/dataset/dataset.py | 142 ++++++ dlt/destinations/dataset/exceptions.py | 22 + dlt/destinations/dataset/factory.py | 22 + dlt/destinations/dataset/ibis_relation.py | 224 ++++++++++ dlt/destinations/dataset/relation.py | 207 +++++++++ dlt/destinations/dataset/utils.py | 95 ++++ .../impl/sqlalchemy/db_api_client.py | 4 +- dlt/{common/libs => helpers}/ibis.py | 58 ++- dlt/pipeline/pipeline.py | 12 +- .../general-usage/dataset-access/dataset.md | 58 +++ poetry.lock | 105 ++--- pyproject.toml | 7 +- .../test_readable_dbapi_dataset.py | 30 +- tests/load/pipeline/test_duckdb.py | 8 +- tests/load/test_read_interfaces.py | 363 ++++++++++++--- 31 files changed, 1245 insertions(+), 579 deletions(-) delete mode 100644 dlt/destinations/dataset.py create mode 100644 dlt/destinations/dataset/__init__.py create mode 100644 dlt/destinations/dataset/dataset.py create mode 100644 dlt/destinations/dataset/exceptions.py create mode 100644 dlt/destinations/dataset/factory.py create mode 100644 dlt/destinations/dataset/ibis_relation.py create mode 100644 dlt/destinations/dataset/relation.py create mode 100644 dlt/destinations/dataset/utils.py rename dlt/{common/libs => helpers}/ibis.py (74%) diff --git a/.github/workflows/test_destination_athena.yml b/.github/workflows/test_destination_athena.yml index 1169fab0de..03eb7f9434 100644 --- a/.github/workflows/test_destination_athena.yml +++ b/.github/workflows/test_destination_athena.yml @@ -67,7 +67,7 @@ jobs: - name: Install dependencies # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction -E athena --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E athena --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_athena_iceberg.yml b/.github/workflows/test_destination_athena_iceberg.yml index 7ccefcc055..3412e789e3 100644 --- a/.github/workflows/test_destination_athena_iceberg.yml +++ b/.github/workflows/test_destination_athena_iceberg.yml @@ -67,7 +67,7 @@ jobs: - name: Install dependencies # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction -E athena --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E athena --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_bigquery.yml b/.github/workflows/test_destination_bigquery.yml index 7afc9b8a00..eb8b63f757 100644 --- a/.github/workflows/test_destination_bigquery.yml +++ b/.github/workflows/test_destination_bigquery.yml @@ -66,7 +66,7 @@ jobs: - name: Install dependencies # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction -E bigquery --with providers -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E bigquery --with providers -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_clickhouse.yml b/.github/workflows/test_destination_clickhouse.yml index 7f297db971..46464ea462 100644 --- a/.github/workflows/test_destination_clickhouse.yml +++ b/.github/workflows/test_destination_clickhouse.yml @@ -61,7 +61,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E clickhouse --with providers -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E clickhouse --with providers -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_databricks.yml b/.github/workflows/test_destination_databricks.yml index 1656fe27f4..c1609de863 100644 --- a/.github/workflows/test_destination_databricks.yml +++ b/.github/workflows/test_destination_databricks.yml @@ -64,7 +64,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E databricks -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E databricks -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_dremio.yml b/.github/workflows/test_destination_dremio.yml index 45c6d17db1..4bc48c54db 100644 --- a/.github/workflows/test_destination_dremio.yml +++ b/.github/workflows/test_destination_dremio.yml @@ -65,7 +65,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline,ibis - run: | poetry run pytest tests/load --ignore tests/load/sources diff --git a/.github/workflows/test_destination_motherduck.yml b/.github/workflows/test_destination_motherduck.yml index 0014b17655..db81131266 100644 --- a/.github/workflows/test_destination_motherduck.yml +++ b/.github/workflows/test_destination_motherduck.yml @@ -64,7 +64,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-motherduck - name: Install dependencies - run: poetry install --no-interaction -E motherduck -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E motherduck -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_mssql.yml b/.github/workflows/test_destination_mssql.yml index 8b899e7da2..6fdd7a5bc5 100644 --- a/.github/workflows/test_destination_mssql.yml +++ b/.github/workflows/test_destination_mssql.yml @@ -69,7 +69,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E mssql -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E mssql -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_snowflake.yml b/.github/workflows/test_destination_snowflake.yml index a720c479bd..73a2a8f6e7 100644 --- a/.github/workflows/test_destination_snowflake.yml +++ b/.github/workflows/test_destination_snowflake.yml @@ -64,7 +64,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E snowflake -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E snowflake -E s3 -E gs -E az -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destination_synapse.yml b/.github/workflows/test_destination_synapse.yml index be1b493916..8f6bf1eb29 100644 --- a/.github/workflows/test_destination_synapse.yml +++ b/.github/workflows/test_destination_synapse.yml @@ -67,7 +67,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-gcp - name: Install dependencies - run: poetry install --no-interaction -E synapse -E parquet --with sentry-sdk --with pipeline + run: poetry install --no-interaction -E synapse -E parquet --with sentry-sdk --with pipeline,ibis - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_destinations.yml b/.github/workflows/test_destinations.yml index 933248d994..cfd0a3bd56 100644 --- a/.github/workflows/test_destinations.yml +++ b/.github/workflows/test_destinations.yml @@ -78,7 +78,7 @@ jobs: - name: Install dependencies # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction -E redshift -E postgis -E postgres -E gs -E s3 -E az -E parquet -E duckdb -E cli -E filesystem --with sentry-sdk --with pipeline -E deltalake + run: poetry install --no-interaction -E redshift -E postgis -E postgres -E gs -E s3 -E az -E parquet -E duckdb -E cli -E filesystem --with sentry-sdk --with pipeline,ibis -E deltalake - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_local_destinations.yml b/.github/workflows/test_local_destinations.yml index 4947a46a3b..6f44e5fd5a 100644 --- a/.github/workflows/test_local_destinations.yml +++ b/.github/workflows/test_local_destinations.yml @@ -95,7 +95,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-local-destinations - name: Install dependencies - run: poetry install --no-interaction -E postgres -E postgis -E duckdb -E parquet -E filesystem -E cli -E weaviate -E qdrant -E sftp --with sentry-sdk --with pipeline -E deltalake + run: poetry install --no-interaction -E postgres -E postgis -E duckdb -E parquet -E filesystem -E cli -E weaviate -E qdrant -E sftp --with sentry-sdk --with pipeline,ibis -E deltalake - name: Start SFTP server run: docker compose -f "tests/load/filesystem_sftp/docker-compose.yml" up -d diff --git a/.github/workflows/test_sqlalchemy_destinations.yml b/.github/workflows/test_sqlalchemy_destinations.yml index c2572b322d..1f00373674 100644 --- a/.github/workflows/test_sqlalchemy_destinations.yml +++ b/.github/workflows/test_sqlalchemy_destinations.yml @@ -86,7 +86,7 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-local-destinations - name: Install dependencies - run: poetry install --no-interaction -E parquet -E filesystem -E sqlalchemy -E cli --with sentry-sdk --with pipeline && poetry run pip install mysqlclient && poetry run pip install "sqlalchemy==${{ matrix.sqlalchemy }}" + run: poetry install --no-interaction -E parquet -E filesystem -E sqlalchemy -E cli --with sentry-sdk --with pipeline,ibis && poetry run pip install mysqlclient && poetry run pip install "sqlalchemy==${{ matrix.sqlalchemy }}" - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/dlt/common/destination/reference.py b/dlt/common/destination/reference.py index e27f99cde7..048fe2186f 100644 --- a/dlt/common/destination/reference.py +++ b/dlt/common/destination/reference.py @@ -67,7 +67,7 @@ TDestinationConfig = TypeVar("TDestinationConfig", bound="DestinationClientConfiguration") TDestinationClient = TypeVar("TDestinationClient", bound="JobClientBase") TDestinationDwhClient = TypeVar("TDestinationDwhClient", bound="DestinationClientDwhConfiguration") -TDatasetType = Literal["dbapi", "ibis"] +TDatasetType = Literal["auto", "default", "ibis"] DEFAULT_FILE_LAYOUT = "{table_name}/{load_id}.{file_id}.{ext}" @@ -76,7 +76,7 @@ try: from dlt.common.libs.pandas import DataFrame from dlt.common.libs.pyarrow import Table as ArrowTable - from dlt.common.libs.ibis import BaseBackend as IbisBackend + from dlt.helpers.ibis import BaseBackend as IbisBackend except MissingDependencyException: DataFrame = Any ArrowTable = Any @@ -535,7 +535,7 @@ def fetchone(self) -> Optional[Tuple[Any, ...]]: ... # modifying access parameters - def limit(self, limit: int) -> "SupportsReadableRelation": + def limit(self, limit: int, **kwargs: Any) -> "SupportsReadableRelation": """limit the result to 'limit' items""" ... @@ -557,6 +557,10 @@ def __getitem__(self, columns: Union[str, Sequence[str]]) -> "SupportsReadableRe """set which columns will be selected""" ... + def __getattr__(self, attr: str) -> Any: + """get an attribute of the relation""" + ... + def __copy__(self) -> "SupportsReadableRelation": """create a copy of the relation object""" ... diff --git a/dlt/destinations/dataset.py b/dlt/destinations/dataset.py deleted file mode 100644 index 27a7f5a7af..0000000000 --- a/dlt/destinations/dataset.py +++ /dev/null @@ -1,412 +0,0 @@ -from typing import Any, Generator, Sequence, Union, TYPE_CHECKING, Tuple - -from contextlib import contextmanager - -from dlt import version -from dlt.common.json import json -from dlt.common.exceptions import MissingDependencyException -from dlt.common.destination import AnyDestination -from dlt.common.destination.reference import ( - SupportsReadableRelation, - SupportsReadableDataset, - TDatasetType, - TDestinationReferenceArg, - Destination, - JobClientBase, - WithStateSync, - DestinationClientDwhConfiguration, - DestinationClientStagingConfiguration, - DestinationClientConfiguration, - DestinationClientDwhWithStagingConfiguration, -) - -from dlt.common.schema.typing import TTableSchemaColumns -from dlt.destinations.sql_client import SqlClientBase, WithSqlClient -from dlt.common.schema import Schema -from dlt.common.exceptions import DltException - -if TYPE_CHECKING: - try: - from dlt.common.libs.ibis import BaseBackend as IbisBackend - except MissingDependencyException: - IbisBackend = Any -else: - IbisBackend = Any - - -class DatasetException(DltException): - pass - - -class ReadableRelationHasQueryException(DatasetException): - def __init__(self, attempted_change: str) -> None: - msg = ( - "This readable relation was created with a provided sql query. You cannot change" - f" {attempted_change}. Please change the orignal sql query." - ) - super().__init__(msg) - - -class ReadableRelationUnknownColumnException(DatasetException): - def __init__(self, column_name: str) -> None: - msg = ( - f"The selected column {column_name} is not known in the dlt schema for this releation." - ) - super().__init__(msg) - - -class ReadableDBAPIRelation(SupportsReadableRelation): - def __init__( - self, - *, - readable_dataset: "ReadableDBAPIDataset", - provided_query: Any = None, - table_name: str = None, - limit: int = None, - selected_columns: Sequence[str] = None, - ) -> None: - """Create a lazy evaluated relation to for the dataset of a destination""" - - # NOTE: we can keep an assertion here, this class will not be created by the user - assert bool(table_name) != bool( - provided_query - ), "Please provide either an sql query OR a table_name" - - self._dataset = readable_dataset - - self._provided_query = provided_query - self._table_name = table_name - self._limit = limit - self._selected_columns = selected_columns - - # wire protocol functions - self.df = self._wrap_func("df") # type: ignore - self.arrow = self._wrap_func("arrow") # type: ignore - self.fetchall = self._wrap_func("fetchall") # type: ignore - self.fetchmany = self._wrap_func("fetchmany") # type: ignore - self.fetchone = self._wrap_func("fetchone") # type: ignore - - self.iter_df = self._wrap_iter("iter_df") # type: ignore - self.iter_arrow = self._wrap_iter("iter_arrow") # type: ignore - self.iter_fetch = self._wrap_iter("iter_fetch") # type: ignore - - @property - def sql_client(self) -> SqlClientBase[Any]: - return self._dataset.sql_client - - @property - def schema(self) -> Schema: - return self._dataset.schema - - @property - def query(self) -> Any: - """build the query""" - if self._provided_query: - return self._provided_query - - table_name = self.sql_client.make_qualified_table_name( - self.schema.naming.normalize_tables_path(self._table_name) - ) - - maybe_limit_clause_1 = "" - maybe_limit_clause_2 = "" - if self._limit: - maybe_limit_clause_1, maybe_limit_clause_2 = self.sql_client._limit_clause_sql( - self._limit - ) - - selector = "*" - if self._selected_columns: - selector = ",".join( - [ - self.sql_client.escape_column_name(self.schema.naming.normalize_path(c)) - for c in self._selected_columns - ] - ) - - return f"SELECT {maybe_limit_clause_1} {selector} FROM {table_name} {maybe_limit_clause_2}" - - @property - def columns_schema(self) -> TTableSchemaColumns: - return self.compute_columns_schema() - - @columns_schema.setter - def columns_schema(self, new_value: TTableSchemaColumns) -> None: - raise NotImplementedError("columns schema in ReadableDBAPIRelation can only be computed") - - def compute_columns_schema(self) -> TTableSchemaColumns: - """provide schema columns for the cursor, may be filtered by selected columns""" - - columns_schema = ( - self.schema.tables.get(self._table_name, {}).get("columns", {}) if self.schema else {} - ) - - if not columns_schema: - return None - if not self._selected_columns: - return columns_schema - - filtered_columns: TTableSchemaColumns = {} - for sc in self._selected_columns: - sc = self.schema.naming.normalize_path(sc) - if sc not in columns_schema.keys(): - raise ReadableRelationUnknownColumnException(sc) - filtered_columns[sc] = columns_schema[sc] - - return filtered_columns - - @contextmanager - def cursor(self) -> Generator[SupportsReadableRelation, Any, Any]: - """Gets a DBApiCursor for the current relation""" - with self.sql_client as client: - # this hacky code is needed for mssql to disable autocommit, read iterators - # will not work otherwise. in the future we should be able to create a readony - # client which will do this automatically - if hasattr(self.sql_client, "_conn") and hasattr(self.sql_client._conn, "autocommit"): - self.sql_client._conn.autocommit = False - with client.execute_query(self.query) as cursor: - if columns_schema := self.columns_schema: - cursor.columns_schema = columns_schema - yield cursor - - def _wrap_iter(self, func_name: str) -> Any: - """wrap SupportsReadableRelation generators in cursor context""" - - def _wrap(*args: Any, **kwargs: Any) -> Any: - with self.cursor() as cursor: - yield from getattr(cursor, func_name)(*args, **kwargs) - - return _wrap - - def _wrap_func(self, func_name: str) -> Any: - """wrap SupportsReadableRelation functions in cursor context""" - - def _wrap(*args: Any, **kwargs: Any) -> Any: - with self.cursor() as cursor: - return getattr(cursor, func_name)(*args, **kwargs) - - return _wrap - - def __copy__(self) -> "ReadableDBAPIRelation": - return self.__class__( - readable_dataset=self._dataset, - provided_query=self._provided_query, - table_name=self._table_name, - limit=self._limit, - selected_columns=self._selected_columns, - ) - - def limit(self, limit: int) -> "ReadableDBAPIRelation": - if self._provided_query: - raise ReadableRelationHasQueryException("limit") - rel = self.__copy__() - rel._limit = limit - return rel - - def select(self, *columns: str) -> "ReadableDBAPIRelation": - if self._provided_query: - raise ReadableRelationHasQueryException("select") - rel = self.__copy__() - rel._selected_columns = columns - # NOTE: the line below will ensure that no unknown columns are selected if - # schema is known - rel.compute_columns_schema() - return rel - - def __getitem__(self, columns: Union[str, Sequence[str]]) -> "SupportsReadableRelation": - if isinstance(columns, str): - return self.select(columns) - elif isinstance(columns, Sequence): - return self.select(*columns) - else: - raise TypeError(f"Invalid argument type: {type(columns).__name__}") - - def head(self, limit: int = 5) -> "ReadableDBAPIRelation": - return self.limit(limit) - - -class ReadableDBAPIDataset(SupportsReadableDataset): - """Access to dataframes and arrowtables in the destination dataset via dbapi""" - - def __init__( - self, - destination: TDestinationReferenceArg, - dataset_name: str, - schema: Union[Schema, str, None] = None, - ) -> None: - self._destination = Destination.from_reference(destination) - self._provided_schema = schema - self._dataset_name = dataset_name - self._sql_client: SqlClientBase[Any] = None - self._schema: Schema = None - - def ibis(self) -> IbisBackend: - """return a connected ibis backend""" - from dlt.common.libs.ibis import create_ibis_backend - - self._ensure_client_and_schema() - return create_ibis_backend( - self._destination, - self._destination_client(self.schema), - ) - - @property - def schema(self) -> Schema: - self._ensure_client_and_schema() - return self._schema - - @property - def sql_client(self) -> SqlClientBase[Any]: - self._ensure_client_and_schema() - return self._sql_client - - def _destination_client(self, schema: Schema) -> JobClientBase: - return get_destination_clients( - schema, destination=self._destination, destination_dataset_name=self._dataset_name - )[0] - - def _ensure_client_and_schema(self) -> None: - """Lazy load schema and client""" - - # full schema given, nothing to do - if not self._schema and isinstance(self._provided_schema, Schema): - self._schema = self._provided_schema - - # schema name given, resolve it from destination by name - elif not self._schema and isinstance(self._provided_schema, str): - with self._destination_client(Schema(self._provided_schema)) as client: - if isinstance(client, WithStateSync): - stored_schema = client.get_stored_schema(self._provided_schema) - if stored_schema: - self._schema = Schema.from_stored_schema(json.loads(stored_schema.schema)) - else: - self._schema = Schema(self._provided_schema) - - # no schema name given, load newest schema from destination - elif not self._schema: - with self._destination_client(Schema(self._dataset_name)) as client: - if isinstance(client, WithStateSync): - stored_schema = client.get_stored_schema() - if stored_schema: - self._schema = Schema.from_stored_schema(json.loads(stored_schema.schema)) - - # default to empty schema with dataset name - if not self._schema: - self._schema = Schema(self._dataset_name) - - # here we create the client bound to the resolved schema - if not self._sql_client: - destination_client = self._destination_client(self._schema) - if isinstance(destination_client, WithSqlClient): - self._sql_client = destination_client.sql_client - else: - raise Exception( - f"Destination {destination_client.config.destination_type} does not support" - " SqlClient." - ) - - def __call__(self, query: Any) -> ReadableDBAPIRelation: - return ReadableDBAPIRelation(readable_dataset=self, provided_query=query) # type: ignore[abstract] - - def table(self, table_name: str) -> SupportsReadableRelation: - return ReadableDBAPIRelation( - readable_dataset=self, - table_name=table_name, - ) # type: ignore[abstract] - - def __getitem__(self, table_name: str) -> SupportsReadableRelation: - """access of table via dict notation""" - return self.table(table_name) - - def __getattr__(self, table_name: str) -> SupportsReadableRelation: - """access of table via property notation""" - return self.table(table_name) - - -def dataset( - destination: TDestinationReferenceArg, - dataset_name: str, - schema: Union[Schema, str, None] = None, - dataset_type: TDatasetType = "dbapi", -) -> SupportsReadableDataset: - if dataset_type == "dbapi": - return ReadableDBAPIDataset(destination, dataset_name, schema) - raise NotImplementedError(f"Dataset of type {dataset_type} not implemented") - - -# helpers -def get_destination_client_initial_config( - destination: AnyDestination, - default_schema_name: str, - dataset_name: str, - as_staging: bool = False, -) -> DestinationClientConfiguration: - client_spec = destination.spec - - # this client supports many schemas and datasets - if issubclass(client_spec, DestinationClientDwhConfiguration): - if issubclass(client_spec, DestinationClientStagingConfiguration): - spec: DestinationClientDwhConfiguration = client_spec(as_staging_destination=as_staging) - else: - spec = client_spec() - - spec._bind_dataset_name(dataset_name, default_schema_name) - return spec - - return client_spec() - - -def get_destination_clients( - schema: Schema, - destination: AnyDestination = None, - destination_dataset_name: str = None, - destination_initial_config: DestinationClientConfiguration = None, - staging: AnyDestination = None, - staging_dataset_name: str = None, - staging_initial_config: DestinationClientConfiguration = None, - # pipeline specific settings - default_schema_name: str = None, -) -> Tuple[JobClientBase, JobClientBase]: - destination = Destination.from_reference(destination) if destination else None - staging = Destination.from_reference(staging) if staging else None - - try: - # resolve staging config in order to pass it to destination client config - staging_client = None - if staging: - if not staging_initial_config: - # this is just initial config - without user configuration injected - staging_initial_config = get_destination_client_initial_config( - staging, - dataset_name=staging_dataset_name, - default_schema_name=default_schema_name, - as_staging=True, - ) - # create the client - that will also resolve the config - staging_client = staging.client(schema, staging_initial_config) - - if not destination_initial_config: - # config is not provided then get it with injected credentials - initial_config = get_destination_client_initial_config( - destination, - dataset_name=destination_dataset_name, - default_schema_name=default_schema_name, - ) - - # attach the staging client config to destination client config - if its type supports it - if ( - staging_client - and isinstance(initial_config, DestinationClientDwhWithStagingConfiguration) - and isinstance(staging_client.config, DestinationClientStagingConfiguration) - ): - initial_config.staging_config = staging_client.config - # create instance with initial_config properly set - client = destination.client(schema, initial_config) - return client, staging_client - except ModuleNotFoundError: - client_spec = destination.spec() - raise MissingDependencyException( - f"{client_spec.destination_type} destination", - [f"{version.DLT_PKG_NAME}[{client_spec.destination_type}]"], - "Dependencies for specific destinations are available as extras of dlt", - ) diff --git a/dlt/destinations/dataset/__init__.py b/dlt/destinations/dataset/__init__.py new file mode 100644 index 0000000000..e0eef681b8 --- /dev/null +++ b/dlt/destinations/dataset/__init__.py @@ -0,0 +1,19 @@ +from dlt.destinations.dataset.factory import ( + dataset, +) +from dlt.destinations.dataset.dataset import ( + ReadableDBAPIDataset, + get_destination_clients, +) +from dlt.destinations.dataset.utils import ( + get_destination_clients, + get_destination_client_initial_config, +) + + +__all__ = [ + "dataset", + "ReadableDBAPIDataset", + "get_destination_client_initial_config", + "get_destination_clients", +] diff --git a/dlt/destinations/dataset/dataset.py b/dlt/destinations/dataset/dataset.py new file mode 100644 index 0000000000..e443045e49 --- /dev/null +++ b/dlt/destinations/dataset/dataset.py @@ -0,0 +1,142 @@ +from typing import Any, Union, TYPE_CHECKING + +from dlt.common.json import json + +from dlt.common.exceptions import MissingDependencyException + +from dlt.common.destination.reference import ( + SupportsReadableRelation, + SupportsReadableDataset, + TDestinationReferenceArg, + Destination, + JobClientBase, + WithStateSync, +) + +from dlt.destinations.sql_client import SqlClientBase, WithSqlClient +from dlt.common.schema import Schema +from dlt.destinations.dataset.relation import ReadableDBAPIRelation +from dlt.destinations.dataset.utils import get_destination_clients +from dlt.common.destination.reference import TDatasetType + +if TYPE_CHECKING: + try: + from dlt.helpers.ibis import BaseBackend as IbisBackend + except MissingDependencyException: + IbisBackend = Any +else: + IbisBackend = Any + + +class ReadableDBAPIDataset(SupportsReadableDataset): + """Access to dataframes and arrowtables in the destination dataset via dbapi""" + + def __init__( + self, + destination: TDestinationReferenceArg, + dataset_name: str, + schema: Union[Schema, str, None] = None, + dataset_type: TDatasetType = "auto", + ) -> None: + self._destination = Destination.from_reference(destination) + self._provided_schema = schema + self._dataset_name = dataset_name + self._sql_client: SqlClientBase[Any] = None + self._schema: Schema = None + self._dataset_type = dataset_type + + def ibis(self) -> IbisBackend: + """return a connected ibis backend""" + from dlt.helpers.ibis import create_ibis_backend + + self._ensure_client_and_schema() + return create_ibis_backend( + self._destination, + self._destination_client(self.schema), + ) + + @property + def schema(self) -> Schema: + self._ensure_client_and_schema() + return self._schema + + @property + def sql_client(self) -> SqlClientBase[Any]: + self._ensure_client_and_schema() + return self._sql_client + + def _destination_client(self, schema: Schema) -> JobClientBase: + return get_destination_clients( + schema, destination=self._destination, destination_dataset_name=self._dataset_name + )[0] + + def _ensure_client_and_schema(self) -> None: + """Lazy load schema and client""" + + # full schema given, nothing to do + if not self._schema and isinstance(self._provided_schema, Schema): + self._schema = self._provided_schema + + # schema name given, resolve it from destination by name + elif not self._schema and isinstance(self._provided_schema, str): + with self._destination_client(Schema(self._provided_schema)) as client: + if isinstance(client, WithStateSync): + stored_schema = client.get_stored_schema(self._provided_schema) + if stored_schema: + self._schema = Schema.from_stored_schema(json.loads(stored_schema.schema)) + else: + self._schema = Schema(self._provided_schema) + + # no schema name given, load newest schema from destination + elif not self._schema: + with self._destination_client(Schema(self._dataset_name)) as client: + if isinstance(client, WithStateSync): + stored_schema = client.get_stored_schema() + if stored_schema: + self._schema = Schema.from_stored_schema(json.loads(stored_schema.schema)) + + # default to empty schema with dataset name + if not self._schema: + self._schema = Schema(self._dataset_name) + + # here we create the client bound to the resolved schema + if not self._sql_client: + destination_client = self._destination_client(self._schema) + if isinstance(destination_client, WithSqlClient): + self._sql_client = destination_client.sql_client + else: + raise Exception( + f"Destination {destination_client.config.destination_type} does not support" + " SqlClient." + ) + + def __call__(self, query: Any) -> ReadableDBAPIRelation: + return ReadableDBAPIRelation(readable_dataset=self, provided_query=query) # type: ignore[abstract] + + def table(self, table_name: str) -> SupportsReadableRelation: + # we can create an ibis powered relation if ibis is available + if table_name in self.schema.tables and self._dataset_type in ("auto", "ibis"): + try: + from dlt.helpers.ibis import create_unbound_ibis_table + from dlt.destinations.dataset.ibis_relation import ReadableIbisRelation + + unbound_table = create_unbound_ibis_table(self.sql_client, self.schema, table_name) + return ReadableIbisRelation(readable_dataset=self, ibis_object=unbound_table, columns_schema=self.schema.tables[table_name]["columns"]) # type: ignore[abstract] + except MissingDependencyException: + # if ibis is explicitly requested, reraise + if self._dataset_type == "ibis": + raise + + # fallback to the standard dbapi relation + return ReadableDBAPIRelation( + readable_dataset=self, + table_name=table_name, + ) # type: ignore[abstract] + + def __getitem__(self, table_name: str) -> SupportsReadableRelation: + """access of table via dict notation""" + return self.table(table_name) + + def __getattr__(self, table_name: str) -> SupportsReadableRelation: + """access of table via property notation""" + return self.table(table_name) diff --git a/dlt/destinations/dataset/exceptions.py b/dlt/destinations/dataset/exceptions.py new file mode 100644 index 0000000000..17e8f6b563 --- /dev/null +++ b/dlt/destinations/dataset/exceptions.py @@ -0,0 +1,22 @@ +from dlt.common.exceptions import DltException + + +class DatasetException(DltException): + pass + + +class ReadableRelationHasQueryException(DatasetException): + def __init__(self, attempted_change: str) -> None: + msg = ( + "This readable relation was created with a provided sql query. You cannot change" + f" {attempted_change}. Please change the orignal sql query." + ) + super().__init__(msg) + + +class ReadableRelationUnknownColumnException(DatasetException): + def __init__(self, column_name: str) -> None: + msg = ( + f"The selected column {column_name} is not known in the dlt schema for this releation." + ) + super().__init__(msg) diff --git a/dlt/destinations/dataset/factory.py b/dlt/destinations/dataset/factory.py new file mode 100644 index 0000000000..8ea0ddf7a1 --- /dev/null +++ b/dlt/destinations/dataset/factory.py @@ -0,0 +1,22 @@ +from typing import Union + + +from dlt.common.destination import AnyDestination +from dlt.common.destination.reference import ( + SupportsReadableDataset, + TDatasetType, + TDestinationReferenceArg, +) + +from dlt.common.schema import Schema + +from dlt.destinations.dataset.dataset import ReadableDBAPIDataset + + +def dataset( + destination: TDestinationReferenceArg, + dataset_name: str, + schema: Union[Schema, str, None] = None, + dataset_type: TDatasetType = "auto", +) -> SupportsReadableDataset: + return ReadableDBAPIDataset(destination, dataset_name, schema, dataset_type) diff --git a/dlt/destinations/dataset/ibis_relation.py b/dlt/destinations/dataset/ibis_relation.py new file mode 100644 index 0000000000..632298ad56 --- /dev/null +++ b/dlt/destinations/dataset/ibis_relation.py @@ -0,0 +1,224 @@ +from typing import TYPE_CHECKING, Any, Union, Sequence + +from functools import partial + +from dlt.common.exceptions import MissingDependencyException +from dlt.destinations.dataset.relation import BaseReadableDBAPIRelation +from dlt.common.schema.typing import TTableSchemaColumns + + +if TYPE_CHECKING: + from dlt.destinations.dataset.dataset import ReadableDBAPIDataset +else: + ReadableDBAPIDataset = Any + +try: + from dlt.helpers.ibis import Expr +except MissingDependencyException: + Expr = Any + +# map dlt destination to sqlglot dialect +DIALECT_MAP = { + "dlt.destinations.duckdb": "duckdb", # works + "dlt.destinations.motherduck": "duckdb", # works + "dlt.destinations.clickhouse": "clickhouse", # works + "dlt.destinations.databricks": "databricks", # works + "dlt.destinations.bigquery": "bigquery", # works + "dlt.destinations.postgres": "postgres", # works + "dlt.destinations.redshift": "redshift", # works + "dlt.destinations.snowflake": "snowflake", # works + "dlt.destinations.mssql": "tsql", # works + "dlt.destinations.synapse": "tsql", # works + "dlt.destinations.athena": "trino", # works + "dlt.destinations.filesystem": "duckdb", # works + "dlt.destinations.dremio": "presto", # works + # NOTE: can we discover the current dialect in sqlalchemy? + "dlt.destinations.sqlalchemy": "mysql", # may work +} + +# NOTE: some dialects are not supported by ibis, but by sqlglot, these need to +# be transpiled with a intermediary step +TRANSPILE_VIA_MAP = { + "tsql": "postgres", + "databricks": "postgres", + "clickhouse": "postgres", + "redshift": "postgres", + "presto": "postgres", +} + + +class ReadableIbisRelation(BaseReadableDBAPIRelation): + def __init__( + self, + *, + readable_dataset: ReadableDBAPIDataset, + ibis_object: Any = None, + columns_schema: TTableSchemaColumns = None, + ) -> None: + """Create a lazy evaluated relation to for the dataset of a destination""" + super().__init__(readable_dataset=readable_dataset) + self._ibis_object = ibis_object + self._columns_schema = columns_schema + + @property + def query(self) -> Any: + """build the query""" + + from dlt.helpers.ibis import ibis, sqlglot + + destination_type = self._dataset._destination.destination_type + target_dialect = DIALECT_MAP[destination_type] + + # render sql directly if possible + if target_dialect not in TRANSPILE_VIA_MAP: + return ibis.to_sql(self._ibis_object, dialect=target_dialect) + + # here we need to transpile first + transpile_via = TRANSPILE_VIA_MAP[target_dialect] + sql = ibis.to_sql(self._ibis_object, dialect=transpile_via) + sql = sqlglot.transpile(sql, read=transpile_via, write=target_dialect)[0] + return sql + + @property + def columns_schema(self) -> TTableSchemaColumns: + return self.compute_columns_schema() + + @columns_schema.setter + def columns_schema(self, new_value: TTableSchemaColumns) -> None: + raise NotImplementedError("columns schema in ReadableDBAPIRelation can only be computed") + + def compute_columns_schema(self) -> TTableSchemaColumns: + """provide schema columns for the cursor, may be filtered by selected columns""" + # TODO: provide column lineage tracing with sqlglot lineage + return self._columns_schema + + def _proxy_expression_method(self, method_name: str, *args: Any, **kwargs: Any) -> Any: + """Proxy method calls to the underlying ibis expression, allowing to wrap the resulting expression in a new relation""" + + # Get the method from the expression + method = getattr(self._ibis_object, method_name) + + # unwrap args and kwargs if they are relations + args = tuple( + arg._ibis_object if isinstance(arg, ReadableIbisRelation) else arg for arg in args + ) + kwargs = { + k: v._ibis_object if isinstance(v, ReadableIbisRelation) else v + for k, v in kwargs.items() + } + + # casefold string params, we assume these are column names + args = tuple( + self.sql_client.capabilities.casefold_identifier(arg) if isinstance(arg, str) else arg + for arg in args + ) + kwargs = { + k: self.sql_client.capabilities.casefold_identifier(v) if isinstance(v, str) else v + for k, v in kwargs.items() + } + + # Call it with provided args + result = method(*args, **kwargs) + + # calculate columns schema for the result, some operations we know will not change the schema + # and select will just reduce the amount of column + columns_schema = None + if method_name == "select": + columns_schema = self._get_filtered_columns_schema(args) + elif method_name in ["filter", "limit", "order_by", "head"]: + columns_schema = self._columns_schema + + # If result is an ibis expression, wrap it in a new relation else return raw result + return self.__class__( + readable_dataset=self._dataset, ibis_object=result, columns_schema=columns_schema + ) + + def __getattr__(self, name: str) -> Any: + """Wrap all callable attributes of the expression""" + + attr = getattr(self._ibis_object, name, None) + + # try casefolded name for ibis columns access + if attr is None: + name = self.sql_client.capabilities.casefold_identifier(name) + attr = getattr(self._ibis_object, name, None) + + if attr is None: + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + + if not callable(attr): + # NOTE: we don't need to forward columns schema for non-callable attributes, these are usually columns + return self.__class__(readable_dataset=self._dataset, ibis_object=attr) + + return partial(self._proxy_expression_method, name) + + def __getitem__(self, columns: Union[str, Sequence[str]]) -> "ReadableIbisRelation": + # casefold column-names + columns = [columns] if isinstance(columns, str) else columns + columns = [self.sql_client.capabilities.casefold_identifier(col) for col in columns] + expr = self._ibis_object[columns] + return self.__class__( + readable_dataset=self._dataset, + ibis_object=expr, + columns_schema=self._get_filtered_columns_schema(columns), + ) + + def _get_filtered_columns_schema(self, columns: Sequence[str]) -> TTableSchemaColumns: + if not self._columns_schema: + return None + try: + return {col: self._columns_schema[col] for col in columns} + except KeyError: + # NOTE: select statements can contain new columns not present in the original schema + # here we just break the column schema inheritance chain + return None + + # forward ibis methods defined on interface + def limit(self, limit: int, **kwargs: Any) -> "ReadableIbisRelation": + """limit the result to 'limit' items""" + return self._proxy_expression_method("limit", limit, **kwargs) # type: ignore + + def head(self, limit: int = 5) -> "ReadableIbisRelation": + """limit the result to 5 items by default""" + return self._proxy_expression_method("head", limit) # type: ignore + + def select(self, *columns: str) -> "ReadableIbisRelation": + """set which columns will be selected""" + return self._proxy_expression_method("select", *columns) # type: ignore + + # forward ibis comparison and math operators + def __lt__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__lt__", other) # type: ignore + + def __gt__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__gt__", other) # type: ignore + + def __ge__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__ge__", other) # type: ignore + + def __le__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__le__", other) # type: ignore + + def __eq__(self, other: Any) -> bool: + return self._proxy_expression_method("__eq__", other) # type: ignore + + def __ne__(self, other: Any) -> bool: + return self._proxy_expression_method("__ne__", other) # type: ignore + + def __and__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__and__", other) # type: ignore + + def __or__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__or__", other) # type: ignore + + def __mul__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__mul__", other) # type: ignore + + def __div__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__div__", other) # type: ignore + + def __add__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__add__", other) # type: ignore + + def __sub__(self, other: Any) -> "ReadableIbisRelation": + return self._proxy_expression_method("__sub__", other) # type: ignore diff --git a/dlt/destinations/dataset/relation.py b/dlt/destinations/dataset/relation.py new file mode 100644 index 0000000000..2cdb7640df --- /dev/null +++ b/dlt/destinations/dataset/relation.py @@ -0,0 +1,207 @@ +from typing import Any, Generator, Sequence, Union, TYPE_CHECKING + +from contextlib import contextmanager + + +from dlt.common.destination.reference import ( + SupportsReadableRelation, +) + +from dlt.destinations.dataset.exceptions import ( + ReadableRelationHasQueryException, + ReadableRelationUnknownColumnException, +) + +from dlt.common.schema.typing import TTableSchemaColumns +from dlt.destinations.sql_client import SqlClientBase +from dlt.common.schema import Schema + +if TYPE_CHECKING: + from dlt.destinations.dataset.dataset import ReadableDBAPIDataset +else: + ReadableDBAPIDataset = Any + + +class BaseReadableDBAPIRelation(SupportsReadableRelation): + def __init__( + self, + *, + readable_dataset: "ReadableDBAPIDataset", + ) -> None: + """Create a lazy evaluated relation to for the dataset of a destination""" + + self._dataset = readable_dataset + + # wire protocol functions + self.df = self._wrap_func("df") # type: ignore + self.arrow = self._wrap_func("arrow") # type: ignore + self.fetchall = self._wrap_func("fetchall") # type: ignore + self.fetchmany = self._wrap_func("fetchmany") # type: ignore + self.fetchone = self._wrap_func("fetchone") # type: ignore + + self.iter_df = self._wrap_iter("iter_df") # type: ignore + self.iter_arrow = self._wrap_iter("iter_arrow") # type: ignore + self.iter_fetch = self._wrap_iter("iter_fetch") # type: ignore + + @property + def sql_client(self) -> SqlClientBase[Any]: + return self._dataset.sql_client + + @property + def schema(self) -> Schema: + return self._dataset.schema + + @property + def query(self) -> Any: + raise NotImplementedError("No query in ReadableDBAPIRelation") + + @contextmanager + def cursor(self) -> Generator[SupportsReadableRelation, Any, Any]: + """Gets a DBApiCursor for the current relation""" + with self.sql_client as client: + # this hacky code is needed for mssql to disable autocommit, read iterators + # will not work otherwise. in the future we should be able to create a readony + # client which will do this automatically + if hasattr(self.sql_client, "_conn") and hasattr(self.sql_client._conn, "autocommit"): + self.sql_client._conn.autocommit = False + with client.execute_query(self.query) as cursor: + if columns_schema := self.columns_schema: + cursor.columns_schema = columns_schema + yield cursor + + def _wrap_iter(self, func_name: str) -> Any: + """wrap SupportsReadableRelation generators in cursor context""" + + def _wrap(*args: Any, **kwargs: Any) -> Any: + with self.cursor() as cursor: + yield from getattr(cursor, func_name)(*args, **kwargs) + + return _wrap + + def _wrap_func(self, func_name: str) -> Any: + """wrap SupportsReadableRelation functions in cursor context""" + + def _wrap(*args: Any, **kwargs: Any) -> Any: + with self.cursor() as cursor: + return getattr(cursor, func_name)(*args, **kwargs) + + return _wrap + + +class ReadableDBAPIRelation(BaseReadableDBAPIRelation): + def __init__( + self, + *, + readable_dataset: "ReadableDBAPIDataset", + provided_query: Any = None, + table_name: str = None, + limit: int = None, + selected_columns: Sequence[str] = None, + ) -> None: + """Create a lazy evaluated relation to for the dataset of a destination""" + + # NOTE: we can keep an assertion here, this class will not be created by the user + assert bool(table_name) != bool( + provided_query + ), "Please provide either an sql query OR a table_name" + + super().__init__(readable_dataset=readable_dataset) + + self._provided_query = provided_query + self._table_name = table_name + self._limit = limit + self._selected_columns = selected_columns + + @property + def query(self) -> Any: + """build the query""" + if self._provided_query: + return self._provided_query + + table_name = self.sql_client.make_qualified_table_name( + self.schema.naming.normalize_path(self._table_name) + ) + + maybe_limit_clause_1 = "" + maybe_limit_clause_2 = "" + if self._limit: + maybe_limit_clause_1, maybe_limit_clause_2 = self.sql_client._limit_clause_sql( + self._limit + ) + + selector = "*" + if self._selected_columns: + selector = ",".join( + [ + self.sql_client.escape_column_name(self.schema.naming.normalize_tables_path(c)) + for c in self._selected_columns + ] + ) + + return f"SELECT {maybe_limit_clause_1} {selector} FROM {table_name} {maybe_limit_clause_2}" + + @property + def columns_schema(self) -> TTableSchemaColumns: + return self.compute_columns_schema() + + @columns_schema.setter + def columns_schema(self, new_value: TTableSchemaColumns) -> None: + raise NotImplementedError("columns schema in ReadableDBAPIRelation can only be computed") + + def compute_columns_schema(self) -> TTableSchemaColumns: + """provide schema columns for the cursor, may be filtered by selected columns""" + + columns_schema = ( + self.schema.tables.get(self._table_name, {}).get("columns", {}) if self.schema else {} + ) + + if not columns_schema: + return None + if not self._selected_columns: + return columns_schema + + filtered_columns: TTableSchemaColumns = {} + for sc in self._selected_columns: + sc = self.schema.naming.normalize_path(sc) + if sc not in columns_schema.keys(): + raise ReadableRelationUnknownColumnException(sc) + filtered_columns[sc] = columns_schema[sc] + + return filtered_columns + + def __copy__(self) -> "ReadableDBAPIRelation": + return self.__class__( + readable_dataset=self._dataset, + provided_query=self._provided_query, + table_name=self._table_name, + limit=self._limit, + selected_columns=self._selected_columns, + ) + + def limit(self, limit: int, **kwargs: Any) -> "ReadableDBAPIRelation": + if self._provided_query: + raise ReadableRelationHasQueryException("limit") + rel = self.__copy__() + rel._limit = limit + return rel + + def select(self, *columns: str) -> "ReadableDBAPIRelation": + if self._provided_query: + raise ReadableRelationHasQueryException("select") + rel = self.__copy__() + rel._selected_columns = columns + # NOTE: the line below will ensure that no unknown columns are selected if + # schema is known + rel.compute_columns_schema() + return rel + + def __getitem__(self, columns: Union[str, Sequence[str]]) -> "SupportsReadableRelation": + if isinstance(columns, str): + return self.select(columns) + elif isinstance(columns, Sequence): + return self.select(*columns) + else: + raise TypeError(f"Invalid argument type: {type(columns).__name__}") + + def head(self, limit: int = 5) -> "ReadableDBAPIRelation": + return self.limit(limit) diff --git a/dlt/destinations/dataset/utils.py b/dlt/destinations/dataset/utils.py new file mode 100644 index 0000000000..766fbc13ea --- /dev/null +++ b/dlt/destinations/dataset/utils.py @@ -0,0 +1,95 @@ +from typing import Tuple + +from dlt import version + +from dlt.common.exceptions import MissingDependencyException + +from dlt.common.destination import AnyDestination +from dlt.common.destination.reference import ( + Destination, + JobClientBase, + DestinationClientDwhConfiguration, + DestinationClientStagingConfiguration, + DestinationClientConfiguration, + DestinationClientDwhWithStagingConfiguration, +) + +from dlt.common.schema import Schema + + +# helpers +def get_destination_client_initial_config( + destination: AnyDestination, + default_schema_name: str, + dataset_name: str, + as_staging: bool = False, +) -> DestinationClientConfiguration: + client_spec = destination.spec + + # this client supports many schemas and datasets + if issubclass(client_spec, DestinationClientDwhConfiguration): + if issubclass(client_spec, DestinationClientStagingConfiguration): + spec: DestinationClientDwhConfiguration = client_spec(as_staging_destination=as_staging) + else: + spec = client_spec() + + spec._bind_dataset_name(dataset_name, default_schema_name) + return spec + + return client_spec() + + +def get_destination_clients( + schema: Schema, + destination: AnyDestination = None, + destination_dataset_name: str = None, + destination_initial_config: DestinationClientConfiguration = None, + staging: AnyDestination = None, + staging_dataset_name: str = None, + staging_initial_config: DestinationClientConfiguration = None, + # pipeline specific settings + default_schema_name: str = None, +) -> Tuple[JobClientBase, JobClientBase]: + destination = Destination.from_reference(destination) if destination else None + staging = Destination.from_reference(staging) if staging else None + + try: + # resolve staging config in order to pass it to destination client config + staging_client = None + if staging: + if not staging_initial_config: + # this is just initial config - without user configuration injected + staging_initial_config = get_destination_client_initial_config( + staging, + dataset_name=staging_dataset_name, + default_schema_name=default_schema_name, + as_staging=True, + ) + # create the client - that will also resolve the config + staging_client = staging.client(schema, staging_initial_config) + + if not destination_initial_config: + # config is not provided then get it with injected credentials + initial_config = get_destination_client_initial_config( + destination, + dataset_name=destination_dataset_name, + default_schema_name=default_schema_name, + ) + + # attach the staging client config to destination client config - if its type supports it + if ( + staging_client + and isinstance(initial_config, DestinationClientDwhWithStagingConfiguration) + and isinstance(staging_client.config, DestinationClientStagingConfiguration) + ): + initial_config.staging_config = staging_client.config + # create instance with initial_config properly set + client = destination.client(schema, initial_config) + return client, staging_client + except ModuleNotFoundError: + client_spec = destination.spec() + raise MissingDependencyException( + f"{client_spec.destination_type} destination", + [f"{version.DLT_PKG_NAME}[{client_spec.destination_type}]"], + "Dependencies for specific destinations are available as extras of dlt", + ) diff --git a/dlt/destinations/impl/sqlalchemy/db_api_client.py b/dlt/destinations/impl/sqlalchemy/db_api_client.py index 6f3ff065bf..27c4f2f1f9 100644 --- a/dlt/destinations/impl/sqlalchemy/db_api_client.py +++ b/dlt/destinations/impl/sqlalchemy/db_api_client.py @@ -84,7 +84,7 @@ def __init__(self, curr: sa.engine.CursorResult) -> None: def _get_columns(self) -> List[str]: try: - return list(self.native_cursor.keys()) # type: ignore[attr-defined] + return list(self.native_cursor.keys()) except ResourceClosedError: # this happens if now rows are returned return [] @@ -314,7 +314,7 @@ def execute_sql( self, sql: Union[AnyStr, sa.sql.Executable], *args: Any, **kwargs: Any ) -> Optional[Sequence[Sequence[Any]]]: with self.execute_query(sql, *args, **kwargs) as cursor: - if cursor.returns_rows: # type: ignore[attr-defined] + if cursor.returns_rows: return cursor.fetchall() return None diff --git a/dlt/common/libs/ibis.py b/dlt/helpers/ibis.py similarity index 74% rename from dlt/common/libs/ibis.py rename to dlt/helpers/ibis.py index ba6f363e66..ed4264dac7 100644 --- a/dlt/common/libs/ibis.py +++ b/dlt/helpers/ibis.py @@ -1,12 +1,14 @@ -from typing import cast +from typing import cast, Any from dlt.common.exceptions import MissingDependencyException - from dlt.common.destination.reference import TDestinationReferenceArg, Destination, JobClientBase +from dlt.common.schema import Schema +from dlt.destinations.sql_client import SqlClientBase try: import ibis # type: ignore - from ibis import BaseBackend + import sqlglot + from ibis import BaseBackend, Expr except ModuleNotFoundError: raise MissingDependencyException("dlt ibis Helpers", ["ibis"]) @@ -29,6 +31,22 @@ ] +# Map dlt data types to ibis data types +DATA_TYPE_MAP = { + "text": "string", + "double": "float64", + "bool": "boolean", + "timestamp": "timestamp", + "bigint": "int64", + "binary": "binary", + "json": "string", # Store JSON as string in ibis + "decimal": "decimal", + "wei": "int64", # Wei is a large integer + "date": "date", + "time": "time", +} + + def create_ibis_backend( destination: TDestinationReferenceArg, client: JobClientBase ) -> BaseBackend: @@ -119,3 +137,37 @@ def create_ibis_backend( con = ibis.duckdb.from_connection(duck) return con + + +def create_unbound_ibis_table( + sql_client: SqlClientBase[Any], schema: Schema, table_name: str +) -> Expr: + """Create an unbound ibis table from a dlt schema""" + + if table_name not in schema.tables: + raise Exception( + f"Table {table_name} not found in schema. Available tables: {schema.tables.keys()}" + ) + table_schema = schema.tables[table_name] + + # Convert dlt table schema columns to ibis schema + ibis_schema = { + sql_client.capabilities.casefold_identifier(col_name): DATA_TYPE_MAP[ + col_info.get("data_type", "string") + ] + for col_name, col_info in table_schema.get("columns", {}).items() + } + + # normalize table name + table_path = sql_client.make_qualified_table_name_path(table_name, escape=False) + + catalog = None + if len(table_path) == 3: + catalog, database, table = table_path + else: + database, table = table_path + + # create unbound ibis table and return in dlt wrapper + unbound_table = ibis.table(schema=ibis_schema, name=table, database=database, catalog=catalog) + + return unbound_table diff --git a/dlt/pipeline/pipeline.py b/dlt/pipeline/pipeline.py index 70d160ea67..9bd2d6911f 100644 --- a/dlt/pipeline/pipeline.py +++ b/dlt/pipeline/pipeline.py @@ -1751,9 +1751,17 @@ def __getstate__(self) -> Any: return {"pipeline_name": self.pipeline_name} def _dataset( - self, schema: Union[Schema, str, None] = None, dataset_type: TDatasetType = "dbapi" + self, schema: Union[Schema, str, None] = None, dataset_type: TDatasetType = "auto" ) -> SupportsReadableDataset: - """Access helper to dataset""" + """Returns a dataset object for querying the destination data. + + Args: + schema: Schema name or Schema object to use. If None, uses the default schema if set. + dataset_type: Type of dataset interface to return. Defaults to 'auto' which will select ibis if available + otherwise it will fallback to the standard dbapi interface. + Returns: + A dataset object that supports querying the destination data. + """ if schema is None: schema = self.default_schema if self.default_schema_name else None return dataset( diff --git a/docs/website/docs/general-usage/dataset-access/dataset.md b/docs/website/docs/general-usage/dataset-access/dataset.md index 68635383c5..b2e3f03d4d 100644 --- a/docs/website/docs/general-usage/dataset-access/dataset.md +++ b/docs/website/docs/general-usage/dataset-access/dataset.md @@ -156,6 +156,64 @@ You can combine `select`, `limit`, and other methods. arrow_table = items_relation.select("col1", "col2").limit(50).arrow() ``` +## Modifying queries with ibis expressions + +If you install the amazing [ibis](https://ibis-project.org/) library, you can use ibis expressions to modify your queries. + +```sh +pip install ibis-framework +``` + +dlt will then wrap an `ibis.UnboundTable` with a `ReadableIbisRelation` object under the hood that will allow you to modify the query of a reltaion using ibis expressions: + +```py +# now that ibis is installed, we can get a dataset with ibis relations +dataset = pipeline._dataset() + +# get two relations +items_relation = dataset["items"] +order_relation = dataset["orders"] + +# join them using an ibis expression +joined_relation = items_relation.join(order_relation, items_relation.id == order_relation.item_id) + +# now we can use the ibis expression to filter the data +filtered_relation = joined_relation.filter(order_relation.status == "completed") + +# we can inspect the query that will be used to read the data +print(filtered_relation.query) + +# and finally fetch the data as a pandas dataframe, the same way we would do with a normal relation +df = filtered_relation.df() + +# a few more examples + +# filter for rows where the id is in the list of ids +items_relation.filter(items_relation.id.isin([1, 2, 3])).df() + +# limit and offset +items_relation.limit(10, offset=5).arrow() + +# mutate columns by adding a new colums that always is 10 times the value of the id column +items_relation.mutate(new_id=items_relation.id * 10).df() + +# sort asc and desc +import ibis +items_relation.order_by(ibis.desc("id"), ibis.asc("price")).limit(10) + +# group by and aggregate +items_relation.group_by("item_group").having(items_table.count() >= 1000).aggregate(sum_id=items_table.id.sum()).df() + +# subqueries +items_relation.filter(items_table.category.isin(beverage_categories.name)).df() +``` + +You can learn more about the available expressions on the [ibis for sql users](https://ibis-project.org/tutorials/ibis-for-sql-users) page. + +:::note +Keep in mind that you can use only methods that modify the executed query and none of the methods ibis provides for fetching data. This is done with the same methods defined on the regular relations explained above. If you need full native ibis integration, please read the ibis section in the advanced part further down. Additionally, not all ibis expressions may be supported by all destinations and sql dialects. +::: + ## Supported destinations All SQL and filesystem destinations supported by `dlt` can utilize this data access interface. For filesystem destinations, `dlt` [uses **DuckDB** under the hood](./sql-client.md#the-filesystem-sql-client) to create views from Parquet or JSONL files dynamically. This allows you to query data stored in files using the same interface as you would with SQL databases. If you plan on accessing data in buckets or the filesystem a lot this way, it is advised to load data as Parquet instead of JSONL, as **DuckDB** is able to only load the parts of the data actually needed for the query to work. diff --git a/poetry.lock b/poetry.lock index 6232b383c8..749979439d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "about-time" @@ -776,7 +776,7 @@ files = [ name = "atpublic" version = "5.0" description = "Keep all y'all's __all__'s in sync" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "atpublic-5.0-py3-none-any.whl", hash = "sha256:b651dcd886666b1042d1e38158a22a4f2c267748f4e97fde94bc492a4a28a3f3"}, @@ -1755,7 +1755,7 @@ PyYAML = ">=3.11" name = "clickhouse-connect" version = "0.7.8" description = "ClickHouse Database Core Driver for Python, Pandas, and Superset" -optional = true +optional = false python-versions = "~=3.8" files = [ {file = "clickhouse-connect-0.7.8.tar.gz", hash = "sha256:dad10ba90eabfe215dfb1fef59f2821a95c752988e66f1093ca8590a51539b8f"}, @@ -2242,7 +2242,7 @@ urllib3 = ">=1.0" name = "db-dtypes" version = "1.3.0" description = "Pandas Data Types for SQL systems (BigQuery, Spanner)" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "db_dtypes-1.3.0-py2.py3-none-any.whl", hash = "sha256:7e65c59f849ccbe6f7bc4d0253edcc212a7907662906921caba3e4aadd0bc277"}, @@ -3526,7 +3526,7 @@ tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] name = "google-cloud-bigquery-storage" version = "2.27.0" description = "Google Cloud Bigquery Storage API client library" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "google_cloud_bigquery_storage-2.27.0-py2.py3-none-any.whl", hash = "sha256:3bfa8f74a61ceaffd3bfe90be5bbef440ad81c1c19ac9075188cccab34bffc2b"}, @@ -4504,63 +4504,64 @@ files = [ [[package]] name = "ibis-framework" -version = "10.0.0.dev256" +version = "9.5.0" description = "The portable Python dataframe library" -optional = true +optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "ibis_framework-10.0.0.dev256-py3-none-any.whl", hash = "sha256:d6f21278e6fd78920bbe986df2c871921142635cc4f7d5d2048cae26e307a3df"}, - {file = "ibis_framework-10.0.0.dev256.tar.gz", hash = "sha256:e9f97d8177fd88f4a3578be20519c1da79a6a7ffac678b46b790bfde67405930"}, + {file = "ibis_framework-9.5.0-py3-none-any.whl", hash = "sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0"}, + {file = "ibis_framework-9.5.0.tar.gz", hash = "sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84"}, ] [package.dependencies] atpublic = ">=2.3,<6" clickhouse-connect = {version = ">=0.5.23,<1", extras = ["arrow", "numpy", "pandas"], optional = true, markers = "extra == \"clickhouse\""} db-dtypes = {version = ">=0.3,<2", optional = true, markers = "extra == \"bigquery\""} -duckdb = {version = ">=0.10,<1.2", optional = true, markers = "extra == \"duckdb\""} +duckdb = {version = ">=0.8.1,<1.2", optional = true, markers = "extra == \"duckdb\""} google-cloud-bigquery = {version = ">=3,<4", optional = true, markers = "extra == \"bigquery\""} google-cloud-bigquery-storage = {version = ">=2,<3", optional = true, markers = "extra == \"bigquery\""} -numpy = {version = ">=1.23.2,<3", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"databricks\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} -packaging = {version = ">=21.3,<25", optional = true, markers = "extra == \"duckdb\" or extra == \"oracle\" or extra == \"polars\" or extra == \"pyspark\""} -pandas = {version = ">=1.5.3,<3", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"databricks\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} +numpy = {version = ">=1.23.2,<3", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"dask\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} +packaging = {version = ">=21.3,<25", optional = true, markers = "extra == \"dask\" or extra == \"duckdb\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"pyspark\""} +pandas = {version = ">=1.5.3,<3", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"dask\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} parsy = ">=2,<3" psycopg2 = {version = ">=2.8.4,<3", optional = true, markers = "extra == \"postgres\" or extra == \"risingwave\""} -pyarrow = {version = ">=10.0.1,<19", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"databricks\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} -pyarrow-hotfix = {version = ">=0.4,<1", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"databricks\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} +pyarrow = {version = ">=10.0.1,<18", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"dask\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} +pyarrow-hotfix = {version = ">=0.4,<1", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"dask\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} pydata-google-auth = {version = ">=1.4.0,<2", optional = true, markers = "extra == \"bigquery\""} pyodbc = {version = ">=4.0.39,<6", optional = true, markers = "extra == \"mssql\""} python-dateutil = ">=2.8.2,<3" pytz = ">=2022.7" -rich = {version = ">=12.4.4,<14", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"databricks\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} +rich = {version = ">=12.4.4,<14", optional = true, markers = "extra == \"bigquery\" or extra == \"clickhouse\" or extra == \"dask\" or extra == \"datafusion\" or extra == \"druid\" or extra == \"duckdb\" or extra == \"exasol\" or extra == \"flink\" or extra == \"impala\" or extra == \"mssql\" or extra == \"mysql\" or extra == \"oracle\" or extra == \"pandas\" or extra == \"polars\" or extra == \"postgres\" or extra == \"pyspark\" or extra == \"snowflake\" or extra == \"sqlite\" or extra == \"risingwave\" or extra == \"trino\""} snowflake-connector-python = {version = ">=3.0.2,<3.3.0b1 || >3.3.0b1,<4", optional = true, markers = "extra == \"snowflake\""} -sqlglot = ">=23.4,<25.30" -toolz = ">=0.11,<2" +sqlglot = ">=23.4,<25.21" +toolz = ">=0.11,<1" typing-extensions = ">=4.3.0,<5" [package.extras] -bigquery = ["db-dtypes (>=0.3,<2)", "google-cloud-bigquery (>=3,<4)", "google-cloud-bigquery-storage (>=2,<3)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "pydata-google-auth (>=1.4.0,<2)", "rich (>=12.4.4,<14)"] -clickhouse = ["clickhouse-connect[arrow,numpy,pandas] (>=0.5.23,<1)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -databricks = ["databricks-sql-connector-core (>=4,<5)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -datafusion = ["datafusion (>=0.6,<43)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +bigquery = ["db-dtypes (>=0.3,<2)", "google-cloud-bigquery (>=3,<4)", "google-cloud-bigquery-storage (>=2,<3)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pydata-google-auth (>=1.4.0,<2)", "rich (>=12.4.4,<14)"] +clickhouse = ["clickhouse-connect[arrow,numpy,pandas] (>=0.5.23,<1)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +dask = ["dask[array,dataframe] (>=2022.9.1,<2024.3.0)", "numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "regex (>=2021.7.6)", "rich (>=12.4.4,<14)"] +datafusion = ["datafusion (>=0.6,<41)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] decompiler = ["black (>=22.1.0,<25)"] deltalake = ["deltalake (>=0.9.0,<1)"] -druid = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "pydruid (>=0.6.7,<1)", "rich (>=12.4.4,<14)"] -duckdb = ["duckdb (>=0.10,<1.2)", "numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +druid = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pydruid (>=0.6.7,<1)", "rich (>=12.4.4,<14)"] +duckdb = ["duckdb (>=0.8.1,<1.2)", "numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] examples = ["pins[gcs] (>=0.8.3,<1)"] -exasol = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "pyexasol[pandas] (>=0.25.2,<1)", "rich (>=12.4.4,<14)"] -flink = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +exasol = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pyexasol[pandas] (>=0.25.2,<1)", "rich (>=12.4.4,<14)"] +flink = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] geospatial = ["geoarrow-types (>=0.2,<1)", "geopandas (>=0.6,<2)", "pyproj (>=3.3.0,<4)", "shapely (>=2,<3)"] -impala = ["impyla (>=0.17,<1)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -mssql = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "pyodbc (>=4.0.39,<6)", "rich (>=12.4.4,<14)"] -mysql = ["mysqlclient (>=2.2.4,<3)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -oracle = ["numpy (>=1.23.2,<3)", "oracledb (>=1.3.1,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -polars = ["numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "polars (>=1,<2)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -postgres = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "psycopg2 (>=2.8.4,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -pyspark = ["numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "pyspark (>=3.3.3,<4)", "rich (>=12.4.4,<14)"] -risingwave = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "psycopg2 (>=2.8.4,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] -snowflake = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)", "snowflake-connector-python (>=3.0.2,!=3.3.0b1,<4)"] -sqlite = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "regex (>=2021.7.6)", "rich (>=12.4.4,<14)"] -trino = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<19)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)", "trino (>=0.321,<1)"] +impala = ["impyla (>=0.17,<1)", "numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +mssql = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pyodbc (>=4.0.39,<6)", "rich (>=12.4.4,<14)"] +mysql = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pymysql (>=1,<2)", "rich (>=12.4.4,<14)"] +oracle = ["numpy (>=1.23.2,<3)", "oracledb (>=1.3.1,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +pandas = ["numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "regex (>=2021.7.6)", "rich (>=12.4.4,<14)"] +polars = ["numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "polars (>=1,<2)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +postgres = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "psycopg2 (>=2.8.4,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +pyspark = ["numpy (>=1.23.2,<3)", "packaging (>=21.3,<25)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "pyspark (>=3.3.3,<4)", "rich (>=12.4.4,<14)"] +risingwave = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "psycopg2 (>=2.8.4,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)"] +snowflake = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)", "snowflake-connector-python (>=3.0.2,!=3.3.0b1,<4)"] +sqlite = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "regex (>=2021.7.6)", "rich (>=12.4.4,<14)"] +trino = ["numpy (>=1.23.2,<3)", "pandas (>=1.5.3,<3)", "pyarrow (>=10.0.1,<18)", "pyarrow-hotfix (>=0.4,<1)", "rich (>=12.4.4,<14)", "trino (>=0.321,<1)"] visualization = ["graphviz (>=0.16,<1)"] [[package]] @@ -5212,7 +5213,7 @@ source = ["Cython (>=0.29.35)"] name = "lz4" version = "4.3.3" description = "LZ4 Bindings for Python" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "lz4-4.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b891880c187e96339474af2a3b2bfb11a8e4732ff5034be919aa9029484cd201"}, @@ -6645,7 +6646,7 @@ future = "*" name = "parsy" version = "2.1" description = "Easy-to-use parser combinators, for parsing in pure Python" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "parsy-2.1-py3-none-any.whl", hash = "sha256:8f18e7b11985e7802e7e3ecbd8291c6ca243d29820b1186e4c84605db4efffa0"}, @@ -7080,7 +7081,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "psycopg2" version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716"}, @@ -7243,7 +7244,7 @@ test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] name = "pyarrow-hotfix" version = "0.6" description = "" -optional = true +optional = false python-versions = ">=3.5" files = [ {file = "pyarrow_hotfix-0.6-py3-none-any.whl", hash = "sha256:dcc9ae2d220dff0083be6a9aa8e0cdee5182ad358d4931fce825c545e5c89178"}, @@ -7456,7 +7457,7 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" name = "pydata-google-auth" version = "1.9.0" description = "PyData helpers for authenticating to Google APIs" -optional = true +optional = false python-versions = ">=3.9" files = [ {file = "pydata-google-auth-1.9.0.tar.gz", hash = "sha256:2f546e88f007dfdb050087556eb46d6008e351386a7b368096797fae5df374f2"}, @@ -9265,13 +9266,13 @@ typing-extensions = "*" [[package]] name = "sqlglot" -version = "25.24.5" +version = "25.20.2" description = "An easily customizable SQL parser and transpiler" optional = false python-versions = ">=3.7" files = [ - {file = "sqlglot-25.24.5-py3-none-any.whl", hash = "sha256:f8a8870d1f5cdd2e2dc5c39a5030a0c7b0a91264fb8972caead3dac8e8438873"}, - {file = "sqlglot-25.24.5.tar.gz", hash = "sha256:6d3d604034301ca3b614d6b4148646b4033317b7a93d1801e9661495eb4b4fcf"}, + {file = "sqlglot-25.20.2-py3-none-any.whl", hash = "sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567"}, + {file = "sqlglot-25.20.2.tar.gz", hash = "sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab"}, ] [package.extras] @@ -9648,13 +9649,13 @@ files = [ [[package]] name = "toolz" -version = "1.0.0" +version = "0.12.1" description = "List processing tools and functional utilities" -optional = true -python-versions = ">=3.8" +optional = false +python-versions = ">=3.7" files = [ - {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, - {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, + {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, + {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, ] [[package]] @@ -10529,7 +10530,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p name = "zstandard" version = "0.22.0" description = "Zstandard bindings for Python" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "zstandard-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:275df437ab03f8c033b8a2c181e51716c32d831082d93ce48002a5227ec93019"}, @@ -10618,4 +10619,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.13" -content-hash = "1bf3deccd929c083b880c1a82be0983430ab49f7ade247b1c5573bb8c70d9ff5" +content-hash = "a7cd6b599326d80b5beb8d4a3d3e3b4074eda6dc53daa5c296ef8d54002c5f78" diff --git a/pyproject.toml b/pyproject.toml index f736fc65ad..0fb7f94e36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,7 +167,6 @@ pytest-mock = "^3.14.0" types-regex = "^2024.5.15.20240519" flake8-print = "^5.0.0" mimesis = "^7.0.0" -ibis-framework = { version = ">=9.0.0", markers = "python_version >= '3.10'", optional = true, extras = ["duckdb", "postgres", "bigquery", "snowflake", "mssql", "clickhouse"]} shapely = ">=2.0.6" [tool.poetry.group.sources] @@ -205,6 +204,12 @@ optional = true [tool.poetry.group.airflow.dependencies] apache-airflow = {version = "^2.8.0", markers = "python_version < '3.12'"} +[tool.poetry.group.ibis] +optional = true + +[tool.poetry.group.ibis.dependencies] +ibis-framework = { version = ">=9.0.0,<10.0.0", markers = "python_version >= '3.10'", extras = ["duckdb", "postgres", "bigquery", "snowflake", "mssql", "clickhouse"]} + [tool.poetry.group.providers] optional = true diff --git a/tests/destinations/test_readable_dbapi_dataset.py b/tests/destinations/test_readable_dbapi_dataset.py index 4745735371..bc58a18fa0 100644 --- a/tests/destinations/test_readable_dbapi_dataset.py +++ b/tests/destinations/test_readable_dbapi_dataset.py @@ -2,7 +2,7 @@ import dlt import pytest -from dlt.destinations.dataset import ( +from dlt.destinations.dataset.exceptions import ( ReadableRelationHasQueryException, ReadableRelationUnknownColumnException, ) @@ -12,44 +12,44 @@ def test_query_builder() -> None: dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline")._dataset() # default query for a table - assert dataset.my_table.query.strip() == 'SELECT * FROM "pipeline_dataset"."my_table"' # type: ignore[attr-defined] + assert dataset.my_table.query.strip() == 'SELECT * FROM "pipeline_dataset"."my_table"' # head query assert ( - dataset.my_table.head().query.strip() # type: ignore[attr-defined] + dataset.my_table.head().query.strip() == 'SELECT * FROM "pipeline_dataset"."my_table" LIMIT 5' ) # limit query assert ( - dataset.my_table.limit(24).query.strip() # type: ignore[attr-defined] + dataset.my_table.limit(24).query.strip() == 'SELECT * FROM "pipeline_dataset"."my_table" LIMIT 24' ) # select columns assert ( - dataset.my_table.select("col1", "col2").query.strip() # type: ignore[attr-defined] + dataset.my_table.select("col1", "col2").query.strip() == 'SELECT "col1","col2" FROM "pipeline_dataset"."my_table"' ) # also indexer notation assert ( - dataset.my_table[["col1", "col2"]].query.strip() # type: ignore[attr-defined] + dataset.my_table[["col1", "col2"]].query.strip() == 'SELECT "col1","col2" FROM "pipeline_dataset"."my_table"' ) # identifiers are normalized assert ( - dataset["MY_TABLE"].select("CoL1", "cOl2").query.strip() # type: ignore[attr-defined] + dataset["MY_TABLE"].select("CoL1", "cOl2").query.strip() == 'SELECT "co_l1","c_ol2" FROM "pipeline_dataset"."my_table"' ) assert ( - dataset["MY__TABLE"].select("Co__L1", "cOl2").query.strip() # type: ignore[attr-defined] + dataset["MY__TABLE"].select("Co__L1", "cOl2").query.strip() == 'SELECT "co__l1","c_ol2" FROM "pipeline_dataset"."my__table"' ) # limit and select chained assert ( - dataset.my_table.select("col1", "col2").limit(24).query.strip() # type: ignore[attr-defined] + dataset.my_table.select("col1", "col2").limit(24).query.strip() == 'SELECT "col1","col2" FROM "pipeline_dataset"."my_table" LIMIT 24' ) @@ -65,18 +65,18 @@ def test_copy_and_chaining() -> None: relation2 = relation.__copy__() assert relation != relation2 - assert relation._limit == relation2._limit # type: ignore[attr-defined] - assert relation._table_name == relation2._table_name # type: ignore[attr-defined] - assert relation._provided_query == relation2._provided_query # type: ignore[attr-defined] - assert relation._selected_columns == relation2._selected_columns # type: ignore[attr-defined] + assert relation._limit == relation2._limit + assert relation._table_name == relation2._table_name + assert relation._provided_query == relation2._provided_query + assert relation._selected_columns == relation2._selected_columns # test copy while chaining limit relation3 = relation2.limit(22) assert relation2 != relation3 - assert relation2._limit != relation3._limit # type: ignore[attr-defined] + assert relation2._limit != relation3._limit # test last setting prevails chaining - assert relation.limit(23).limit(67).limit(11)._limit == 11 # type: ignore[attr-defined] + assert relation.limit(23).limit(67).limit(11)._limit == 11 def test_computed_schema_columns() -> None: diff --git a/tests/load/pipeline/test_duckdb.py b/tests/load/pipeline/test_duckdb.py index 98642bb263..a7aa4d36e4 100644 --- a/tests/load/pipeline/test_duckdb.py +++ b/tests/load/pipeline/test_duckdb.py @@ -283,8 +283,8 @@ def test_duckdb_credentials_separation( print(p1_dataset.p1_data.fetchall()) print(p2_dataset.p2_data.fetchall()) - assert "p1" in p1_dataset.sql_client.credentials._conn_str() # type: ignore[attr-defined] - assert "p2" in p2_dataset.sql_client.credentials._conn_str() # type: ignore[attr-defined] + assert "p1" in p1_dataset.sql_client.credentials._conn_str() + assert "p2" in p2_dataset.sql_client.credentials._conn_str() - assert p1_dataset.sql_client.credentials.bound_to_pipeline is p1 # type: ignore[attr-defined] - assert p2_dataset.sql_client.credentials.bound_to_pipeline is p2 # type: ignore[attr-defined] + assert p1_dataset.sql_client.credentials.bound_to_pipeline is p1 + assert p2_dataset.sql_client.credentials.bound_to_pipeline is p2 diff --git a/tests/load/test_read_interfaces.py b/tests/load/test_read_interfaces.py index 1a9c8a383b..d2f5f7951e 100644 --- a/tests/load/test_read_interfaces.py +++ b/tests/load/test_read_interfaces.py @@ -1,5 +1,5 @@ -from typing import Any, cast - +from typing import Any, cast, Tuple, List +import re import pytest import dlt import os @@ -20,8 +20,10 @@ ) from dlt.destinations import filesystem from tests.utils import TEST_STORAGE_ROOT, clean_test_storage -from dlt.common.destination.reference import TDestinationReferenceArg -from dlt.destinations.dataset import ReadableDBAPIDataset, ReadableRelationUnknownColumnException +from dlt.destinations.dataset.dataset import ReadableDBAPIDataset +from dlt.destinations.dataset.exceptions import ( + ReadableRelationUnknownColumnException, +) from tests.load.utils import drop_pipeline_data EXPECTED_COLUMNS = ["id", "decimal", "other_decimal", "_dlt_load_id", "_dlt_id"] @@ -58,6 +60,7 @@ def autouse_test_storage() -> FileStorage: @pytest.fixture(scope="session") def populated_pipeline(request, autouse_test_storage) -> Any: """fixture that returns a pipeline object populated with the example data""" + destination_config = cast(DestinationTestConfiguration, request.param) if ( @@ -104,6 +107,7 @@ def items(): columns={ "id": {"data_type": "bigint"}, "double_id": {"data_type": "bigint"}, + "di_decimal": {"data_type": "decimal", "precision": 7, "scale": 3}, }, ) def double_items(): @@ -111,6 +115,7 @@ def double_items(): { "id": i, "double_id": i * 2, + "di_decimal": Decimal("10.433"), } for i in range(total_records) ] @@ -151,6 +156,24 @@ def double_items(): ) +@pytest.mark.no_load +@pytest.mark.essential +@pytest.mark.parametrize( + "populated_pipeline", + configs, + indirect=True, + ids=lambda x: x.name, +) +def test_explicit_dataset_type_selection(populated_pipeline: Pipeline): + from dlt.destinations.dataset.dataset import ReadableDBAPIRelation + from dlt.destinations.dataset.ibis_relation import ReadableIbisRelation + + assert isinstance( + populated_pipeline._dataset(dataset_type="default").items, ReadableDBAPIRelation + ) + assert isinstance(populated_pipeline._dataset(dataset_type="ibis").items, ReadableIbisRelation) + + @pytest.mark.no_load @pytest.mark.essential @pytest.mark.parametrize( @@ -258,71 +281,6 @@ def test_db_cursor_access(populated_pipeline: Pipeline) -> None: assert set(ids) == set(range(total_records)) -@pytest.mark.no_load -@pytest.mark.essential -@pytest.mark.parametrize( - "populated_pipeline", - configs, - indirect=True, - ids=lambda x: x.name, -) -def test_ibis_dataset_access(populated_pipeline: Pipeline) -> None: - # NOTE: we could generalize this with a context for certain deps - import subprocess - - subprocess.check_call( - ["pip", "install", "ibis-framework[duckdb,postgres,bigquery,snowflake,mssql,clickhouse]"] - ) - - from dlt.common.libs.ibis import SUPPORTED_DESTINATIONS - - # check correct error if not supported - if populated_pipeline.destination.destination_type not in SUPPORTED_DESTINATIONS: - with pytest.raises(NotImplementedError): - populated_pipeline._dataset().ibis() - return - - total_records = _total_records(populated_pipeline) - ibis_connection = populated_pipeline._dataset().ibis() - - map_i = lambda x: x - if populated_pipeline.destination.destination_type == "dlt.destinations.snowflake": - map_i = lambda x: x.upper() - - dataset_name = map_i(populated_pipeline.dataset_name) - table_like_statement = None - table_name_prefix = "" - addtional_tables = [] - - # clickhouse has no datasets, but table prefixes and a sentinel table - if populated_pipeline.destination.destination_type == "dlt.destinations.clickhouse": - table_like_statement = dataset_name + "." - table_name_prefix = dataset_name + "___" - dataset_name = None - addtional_tables = ["dlt_sentinel_table"] - - add_table_prefix = lambda x: table_name_prefix + x - - # just do a basic check to see wether ibis can connect - assert set(ibis_connection.list_tables(database=dataset_name, like=table_like_statement)) == { - add_table_prefix(map_i(x)) - for x in ( - [ - "_dlt_loads", - "_dlt_pipeline_state", - "_dlt_version", - "double_items", - "items", - "items__children", - ] - + addtional_tables - ) - } - - items_table = ibis_connection.table(add_table_prefix(map_i("items")), database=dataset_name) - assert items_table.count().to_pandas() == total_records - - @pytest.mark.no_load @pytest.mark.essential @pytest.mark.parametrize( @@ -332,7 +290,8 @@ def test_ibis_dataset_access(populated_pipeline: Pipeline) -> None: ids=lambda x: x.name, ) def test_hint_preservation(populated_pipeline: Pipeline) -> None: - table_relationship = populated_pipeline._dataset().items + # NOTE: for now hints are only preserved for the default dataset + table_relationship = populated_pipeline._dataset(dataset_type="default").items # check that hints are carried over to arrow table expected_decimal_precision = 10 expected_decimal_precision_2 = 12 @@ -425,8 +384,7 @@ def test_limit_and_head(populated_pipeline: Pipeline) -> None: ids=lambda x: x.name, ) def test_column_selection(populated_pipeline: Pipeline) -> None: - table_relationship = populated_pipeline._dataset().items - + table_relationship = populated_pipeline._dataset(dataset_type="default").items columns = ["_dlt_load_id", "other_decimal"] data_frame = table_relationship.select(*columns).head().df() assert [v.lower() for v in data_frame.columns.values] == columns @@ -479,6 +437,266 @@ def test_schema_arg(populated_pipeline: Pipeline) -> None: assert "items" in dataset.schema.tables +@pytest.mark.no_load +@pytest.mark.essential +@pytest.mark.parametrize( + "populated_pipeline", + configs, + indirect=True, + ids=lambda x: x.name, +) +def test_ibis_expression_relation(populated_pipeline: Pipeline) -> None: + # NOTE: we could generalize this with a context for certain deps + import ibis # type: ignore + + # now we should get the more powerful ibis relation + dataset = populated_pipeline._dataset() + total_records = _total_records(populated_pipeline) + + items_table = dataset["items"] + double_items_table = dataset["double_items"] + + # check full table access + df = items_table.df() + assert len(df.index) == total_records + + df = double_items_table.df() + assert len(df.index) == total_records + + # check limit + df = items_table.limit(5).df() + assert len(df.index) == 5 + + # check chained expression with join, column selection, order by and limit + joined_table = ( + items_table.join(double_items_table, items_table.id == double_items_table.id)[ + ["id", "double_id"] + ] + .order_by("id") + .limit(20) + ) + table = joined_table.fetchall() + assert len(table) == 20 + assert list(table[0]) == [0, 0] + assert list(table[5]) == [5, 10] + assert list(table[10]) == [10, 20] + + # check aggregate of first 20 items + agg_table = items_table.order_by("id").limit(20).aggregate(sum_id=items_table.id.sum()) + assert agg_table.fetchone()[0] == reduce(lambda a, b: a + b, range(20)) + + # check filtering + filtered_table = items_table.filter(items_table.id < 10) + assert len(filtered_table.fetchall()) == 10 + + if populated_pipeline.destination.destination_type != "dlt.destinations.duckdb": + return + + # we check a bunch of expressions without executing them to see that they produce correct sql + # also we return the keys of the disovered schema columns + def sql_from_expr(expr: Any) -> Tuple[str, List[str]]: + query = str(expr.query).replace(populated_pipeline.dataset_name, "dataset") + columns = list(expr.columns_schema.keys()) if expr.columns_schema else None + return re.sub(r"\s+", " ", query), columns + + # test all functions discussed here: https://ibis-project.org/tutorials/ibis-for-sql-users + ALL_COLUMNS = ["id", "decimal", "other_decimal", "_dlt_load_id", "_dlt_id"] + + # selecting two columns + assert sql_from_expr(items_table.select("id", "decimal")) == ( + 'SELECT "t0"."id", "t0"."decimal" FROM "dataset"."items" AS "t0"', + ["id", "decimal"], + ) + + # selecting all columns + assert sql_from_expr(items_table) == ('SELECT * FROM "dataset"."items"', ALL_COLUMNS) + + # selecting two other columns via item getter + assert sql_from_expr(items_table["id", "decimal"]) == ( + 'SELECT "t0"."id", "t0"."decimal" FROM "dataset"."items" AS "t0"', + ["id", "decimal"], + ) + + # adding a new columns + new_col = (items_table.id * 2).name("new_col") + assert sql_from_expr(items_table.select("id", "decimal", new_col)) == ( + ( + 'SELECT "t0"."id", "t0"."decimal", "t0"."id" * 2 AS "new_col" FROM' + ' "dataset"."items" AS "t0"' + ), + None, + ) + + # mutating table (add a new column computed from existing columns) + assert sql_from_expr( + items_table.mutate(double_id=items_table.id * 2).select("id", "double_id") + ) == ( + 'SELECT "t0"."id", "t0"."id" * 2 AS "double_id" FROM "dataset"."items" AS "t0"', + None, + ) + + # mutating table add new static column + assert sql_from_expr( + items_table.mutate(new_col=ibis.literal("static_value")).select("id", "new_col") + ) == ('SELECT "t0"."id", \'static_value\' AS "new_col" FROM "dataset"."items" AS "t0"', None) + + # check filtering (preserves all columns) + assert sql_from_expr(items_table.filter(items_table.id < 10)) == ( + 'SELECT * FROM "dataset"."items" AS "t0" WHERE "t0"."id" < 10', + ALL_COLUMNS, + ) + + # filtering and selecting a single column + assert sql_from_expr(items_table.filter(items_table.id < 10).select("id")) == ( + 'SELECT "t0"."id" FROM "dataset"."items" AS "t0" WHERE "t0"."id" < 10', + ["id"], + ) + + # check filter "and" condition + assert sql_from_expr(items_table.filter(items_table.id < 10).filter(items_table.id > 5)) == ( + 'SELECT * FROM "dataset"."items" AS "t0" WHERE "t0"."id" < 10 AND "t0"."id" > 5', + ALL_COLUMNS, + ) + + # check filter "or" condition + assert sql_from_expr(items_table.filter((items_table.id < 10) | (items_table.id > 5))) == ( + 'SELECT * FROM "dataset"."items" AS "t0" WHERE ( "t0"."id" < 10 ) OR ( "t0"."id" > 5 )', + ALL_COLUMNS, + ) + + # check group by and aggregate + assert sql_from_expr( + items_table.group_by("id") + .having(items_table.count() >= 1000) + .aggregate(sum_id=items_table.id.sum()) + ) == ( + ( + 'SELECT "t1"."id", "t1"."sum_id" FROM ( SELECT "t0"."id", SUM("t0"."id") AS "sum_id",' + ' COUNT(*) AS "CountStar(items)" FROM "dataset"."items" AS "t0" GROUP BY 1 ) AS "t1"' + ' WHERE "t1"."CountStar(items)" >= 1000' + ), + None, + ) + + # sorting and ordering + assert sql_from_expr(items_table.order_by("id", "decimal").limit(10)) == ( + ( + 'SELECT * FROM "dataset"."items" AS "t0" ORDER BY "t0"."id" ASC, "t0"."decimal" ASC' + " LIMIT 10" + ), + ALL_COLUMNS, + ) + + # sort desc and asc + assert sql_from_expr(items_table.order_by(ibis.desc("id"), ibis.asc("decimal")).limit(10)) == ( + ( + 'SELECT * FROM "dataset"."items" AS "t0" ORDER BY "t0"."id" DESC, "t0"."decimal" ASC' + " LIMIT 10" + ), + ALL_COLUMNS, + ) + + # offset and limit + assert sql_from_expr(items_table.order_by("id").limit(10, offset=5)) == ( + 'SELECT * FROM "dataset"."items" AS "t0" ORDER BY "t0"."id" ASC LIMIT 10 OFFSET 5', + ALL_COLUMNS, + ) + + # join + assert sql_from_expr( + items_table.join(double_items_table, items_table.id == double_items_table.id)[ + ["id", "double_id"] + ] + ) == ( + ( + 'SELECT "t2"."id", "t3"."double_id" FROM "dataset"."items" AS "t2" INNER JOIN' + ' "dataset"."double_items" AS "t3" ON "t2"."id" = "t3"."id"' + ), + None, + ) + + # subqueries + assert sql_from_expr( + items_table.filter(items_table.decimal.isin(double_items_table.di_decimal)) + ) == ( + ( + 'SELECT * FROM "dataset"."items" AS "t0" WHERE "t0"."decimal" IN ( SELECT' + ' "t1"."di_decimal" FROM "dataset"."double_items" AS "t1" )' + ), + ALL_COLUMNS, + ) + + # topk + assert sql_from_expr(items_table.decimal.topk(10)) == ( + ( + 'SELECT * FROM ( SELECT "t0"."decimal", COUNT(*) AS "CountStar(items)" FROM' + ' "dataset"."items" AS "t0" GROUP BY 1 ) AS "t1" ORDER BY "t1"."CountStar(items)" DESC' + " LIMIT 10" + ), + None, + ) + + +@pytest.mark.no_load +@pytest.mark.essential +@pytest.mark.parametrize( + "populated_pipeline", + configs, + indirect=True, + ids=lambda x: x.name, +) +def test_ibis_dataset_access(populated_pipeline: Pipeline) -> None: + # NOTE: we could generalize this with a context for certain deps + + from dlt.helpers.ibis import SUPPORTED_DESTINATIONS + + # check correct error if not supported + if populated_pipeline.destination.destination_type not in SUPPORTED_DESTINATIONS: + with pytest.raises(NotImplementedError): + populated_pipeline._dataset().ibis() + return + + total_records = _total_records(populated_pipeline) + ibis_connection = populated_pipeline._dataset().ibis() + + map_i = lambda x: x + if populated_pipeline.destination.destination_type == "dlt.destinations.snowflake": + map_i = lambda x: x.upper() + + dataset_name = map_i(populated_pipeline.dataset_name) + table_like_statement = None + table_name_prefix = "" + addtional_tables = [] + + # clickhouse has no datasets, but table prefixes and a sentinel table + if populated_pipeline.destination.destination_type == "dlt.destinations.clickhouse": + table_like_statement = dataset_name + "." + table_name_prefix = dataset_name + "___" + dataset_name = None + addtional_tables = ["dlt_sentinel_table"] + + add_table_prefix = lambda x: table_name_prefix + x + + # just do a basic check to see wether ibis can connect + assert set(ibis_connection.list_tables(database=dataset_name, like=table_like_statement)) == { + add_table_prefix(map_i(x)) + for x in ( + [ + "_dlt_loads", + "_dlt_pipeline_state", + "_dlt_version", + "double_items", + "items", + "items__children", + ] + + addtional_tables + ) + } + + items_table = ibis_connection.table(add_table_prefix(map_i("items")), database=dataset_name) + assert items_table.count().to_pandas() == total_records + + @pytest.mark.no_load @pytest.mark.essential @pytest.mark.parametrize( @@ -546,6 +764,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: assert dataset.schema.name == "unknown_dataset" assert "items" not in dataset.schema.tables + # NOTE: this breaks the following test, it will need to be fixed somehow # create a newer schema with different name and see wether this is loaded from dlt.common.schema import Schema from dlt.common.schema import utils From 4e5a2405e23c7dfae89903327569ae31fb535d4b Mon Sep 17 00:00:00 2001 From: Jorrit Sandbrink <47451109+jorritsandbrink@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:35:59 +0400 Subject: [PATCH 07/23] `iceberg` table format support for `filesystem` destination (#2067) * add pyiceberg dependency and upgrade mypy - mypy upgrade needed to solve this issue: https://github.com/apache/iceberg-python/issues/768 - uses <1.13.0 requirement on mypy because 1.13.0 gives error - new lint errors arising due to version upgrade are simply ignored * extend pyiceberg dependencies * remove redundant delta annotation * add basic local filesystem iceberg support * add active table format setting * disable merge tests for iceberg table format * restore non-redundant extra info * refactor to in-memory iceberg catalog * add s3 support for iceberg table format * add schema evolution support for iceberg table format * extract _register_table function * add partition support for iceberg table format * update docstring * enable child table test for iceberg table format * enable empty source test for iceberg table format * make iceberg catalog namespace configurable and default to dataset name * add optional typing * fix typo * improve typing * extract logic into dedicated function * add iceberg read support to filesystem sql client * remove unused import * add todo * extract logic into separate functions * add azure support for iceberg table format * generalize delta table format tests * enable get tables function test for iceberg table format * remove ignores * undo table directory management change * enable test_read_interfaces tests for iceberg * fix active table format filter * use mixin for object store rs credentials * generalize catalog typing * extract pyiceberg scheme mapping into separate function * generalize credentials mixin test setup * remove unused import * add centralized fallback to append when merge is not supported * Revert "add centralized fallback to append when merge is not supported" This reverts commit 54cd0bcebffad15d522e734da321c602f4bd7461. * fall back to append if merge is not supported on filesystem * fix test for s3-compatible storage * remove obsolete code path * exclude gcs read interface tests for iceberg * add gcs support for iceberg table format * switch to UnsupportedAuthenticationMethodException * add iceberg table format docs * use shorter pipeline name to prevent too long sql identifiers * add iceberg catalog note to docs * black format * use shorter pipeline name to prevent too long sql identifiers * correct max id length for sqlalchemy mysql dialect * Revert "use shorter pipeline name to prevent too long sql identifiers" This reverts commit 6cce03b77111825b0714597e6d494df97145f0f2. * Revert "use shorter pipeline name to prevent too long sql identifiers" This reverts commit ef29aa7c2fdba79441573850c7d15b83526c011a. * replace show with execute to prevent useless print output * add abfss scheme to test * remove az support for iceberg table format * remove iceberg bucket test exclusion * add note to docs on azure scheme support for iceberg table format * exclude iceberg from duckdb s3-compatibility test * disable pyiceberg info logs for tests * extend table format docs and move into own page * upgrade adlfs to enable account_host attribute * Merge branch 'devel' of https://github.com/dlt-hub/dlt into feat/1996-iceberg-filesystem * fix lint errors * re-add pyiceberg dependency * enabled iceberg in dbt-duckdb * upgrade pyiceberg version * remove pyiceberg mypy errors across python version * does not install airflow group for dev * fixes gcp oauth iceberg credentials handling * fixes ca cert bundle duckdb azure on ci * allow for airflow dep to be present during type check --------- Co-authored-by: Marcin Rudolf --- .github/workflows/test_destinations.yml | 9 +- .github/workflows/test_local_destinations.yml | 5 +- Makefile | 2 +- dlt/cli/source_detection.py | 3 +- .../configuration/specs/aws_credentials.py | 15 +- .../configuration/specs/azure_credentials.py | 22 +- .../configuration/specs/base_configuration.py | 2 +- .../specs/config_providers_context.py | 7 +- dlt/common/configuration/specs/exceptions.py | 4 + .../configuration/specs/gcp_credentials.py | 36 +- dlt/common/configuration/specs/mixins.py | 24 ++ dlt/common/data_writers/buffered.py | 2 +- dlt/common/destination/utils.py | 2 +- dlt/common/libs/deltalake.py | 6 +- dlt/common/libs/pyiceberg.py | 192 +++++++++ dlt/common/logger.py | 2 +- dlt/common/metrics.py | 2 +- dlt/common/reflection/utils.py | 14 +- dlt/common/schema/schema.py | 2 +- dlt/common/typing.py | 2 +- dlt/destinations/impl/filesystem/factory.py | 4 +- .../impl/filesystem/filesystem.py | 86 +++- .../impl/filesystem/sql_client.py | 27 +- dlt/destinations/impl/sqlalchemy/factory.py | 3 + dlt/extract/incremental/lag.py | 2 +- dlt/helpers/airflow_helper.py | 4 +- dlt/helpers/dbt/profiles.yml | 1 + .../destinations/delta-iceberg.md | 168 ++++++++ .../dlt-ecosystem/destinations/filesystem.md | 113 +---- .../dlt-ecosystem/table-formats/iceberg.md | 2 +- .../dataset-access/ibis-backend.md | 3 +- docs/website/sidebars.js | 1 + mypy.ini | 6 + poetry.lock | 154 +++++-- pyproject.toml | 11 +- tests/conftest.py | 3 + tests/libs/test_csv_writer.py | 4 +- ...dentials.py => test_credentials_mixins.py} | 169 +++++--- tests/load/filesystem/test_sql_client.py | 18 +- .../load/pipeline/test_filesystem_pipeline.py | 393 +++++++++++------- .../sql_database/test_sql_database_source.py | 5 +- tests/load/utils.py | 33 +- tests/pipeline/utils.py | 13 + .../helpers/rest_client/test_client.py | 2 +- tests/utils.py | 7 + 45 files changed, 1163 insertions(+), 422 deletions(-) create mode 100644 dlt/common/configuration/specs/mixins.py create mode 100644 dlt/common/libs/pyiceberg.py create mode 100644 docs/website/docs/dlt-ecosystem/destinations/delta-iceberg.md rename tests/load/filesystem/{test_object_store_rs_credentials.py => test_credentials_mixins.py} (50%) diff --git a/.github/workflows/test_destinations.yml b/.github/workflows/test_destinations.yml index cfd0a3bd56..84a8f95d71 100644 --- a/.github/workflows/test_destinations.yml +++ b/.github/workflows/test_destinations.yml @@ -77,8 +77,13 @@ jobs: # key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-redshift - name: Install dependencies - # if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction -E redshift -E postgis -E postgres -E gs -E s3 -E az -E parquet -E duckdb -E cli -E filesystem --with sentry-sdk --with pipeline,ibis -E deltalake + run: poetry install --no-interaction -E redshift -E postgis -E postgres -E gs -E s3 -E az -E parquet -E duckdb -E cli -E filesystem --with sentry-sdk --with pipeline,ibis -E deltalake -E pyiceberg + + - name: enable certificates for azure and duckdb + run: sudo mkdir -p /etc/pki/tls/certs && sudo ln -s /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt + + - name: Upgrade sqlalchemy + run: poetry run pip install sqlalchemy==2.0.18 # minimum version required by `pyiceberg` - name: create secrets.toml run: pwd && echo "$DLT_SECRETS_TOML" > tests/.dlt/secrets.toml diff --git a/.github/workflows/test_local_destinations.yml b/.github/workflows/test_local_destinations.yml index 6f44e5fd5a..706bae1b0c 100644 --- a/.github/workflows/test_local_destinations.yml +++ b/.github/workflows/test_local_destinations.yml @@ -95,7 +95,10 @@ jobs: key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-local-destinations - name: Install dependencies - run: poetry install --no-interaction -E postgres -E postgis -E duckdb -E parquet -E filesystem -E cli -E weaviate -E qdrant -E sftp --with sentry-sdk --with pipeline,ibis -E deltalake + run: poetry install --no-interaction -E postgres -E postgis -E duckdb -E parquet -E filesystem -E cli -E weaviate -E qdrant -E sftp --with sentry-sdk --with pipeline,ibis -E deltalake -E pyiceberg + + - name: Upgrade sqlalchemy + run: poetry run pip install sqlalchemy==2.0.18 # minimum version required by `pyiceberg` - name: Start SFTP server run: docker compose -f "tests/load/filesystem_sftp/docker-compose.yml" up -d diff --git a/Makefile b/Makefile index 2a7f6dac0a..0ca8a2e0c3 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ has-poetry: poetry --version dev: has-poetry - poetry install --all-extras --with docs,providers,pipeline,sources,sentry-sdk,airflow + poetry install --all-extras --with docs,providers,pipeline,sources,sentry-sdk lint: ./tools/check-package.sh diff --git a/dlt/cli/source_detection.py b/dlt/cli/source_detection.py index 7067f8b896..0769605d01 100644 --- a/dlt/cli/source_detection.py +++ b/dlt/cli/source_detection.py @@ -29,8 +29,7 @@ def find_call_arguments_to_replace( if not isinstance(dn_node, ast.Constant) or not isinstance(dn_node.value, str): raise CliCommandInnerException( "init", - f"The pipeline script {init_script_name} must pass the {t_arg_name} as" - f" string to '{arg_name}' function in line {dn_node.lineno}", + f"The pipeline script {init_script_name} must pass the {t_arg_name} as string to '{arg_name}' function in line {dn_node.lineno}", # type: ignore[attr-defined] ) else: transformed_nodes.append((dn_node, ast.Constant(value=t_value, kind=None))) diff --git a/dlt/common/configuration/specs/aws_credentials.py b/dlt/common/configuration/specs/aws_credentials.py index 5f69be6a33..a75cd85225 100644 --- a/dlt/common/configuration/specs/aws_credentials.py +++ b/dlt/common/configuration/specs/aws_credentials.py @@ -8,6 +8,7 @@ CredentialsWithDefault, configspec, ) +from dlt.common.configuration.specs.mixins import WithObjectStoreRsCredentials, WithPyicebergConfig from dlt.common.configuration.specs.exceptions import ( InvalidBoto3Session, ObjectStoreRsCredentialsException, @@ -16,7 +17,9 @@ @configspec -class AwsCredentialsWithoutDefaults(CredentialsConfiguration): +class AwsCredentialsWithoutDefaults( + CredentialsConfiguration, WithObjectStoreRsCredentials, WithPyicebergConfig +): # credentials without boto implementation aws_access_key_id: str = None aws_secret_access_key: TSecretStrValue = None @@ -77,6 +80,16 @@ def to_object_store_rs_credentials(self) -> Dict[str, str]: return creds + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + return { + "s3.access-key-id": self.aws_access_key_id, + "s3.secret-access-key": self.aws_secret_access_key, + "s3.session-token": self.aws_session_token, + "s3.region": self.region_name, + "s3.endpoint": self.endpoint_url, + "s3.connect-timeout": 300, + } + @configspec class AwsCredentials(AwsCredentialsWithoutDefaults, CredentialsWithDefault): diff --git a/dlt/common/configuration/specs/azure_credentials.py b/dlt/common/configuration/specs/azure_credentials.py index cf6ec493de..aabd0b471a 100644 --- a/dlt/common/configuration/specs/azure_credentials.py +++ b/dlt/common/configuration/specs/azure_credentials.py @@ -8,6 +8,7 @@ CredentialsWithDefault, configspec, ) +from dlt.common.configuration.specs.mixins import WithObjectStoreRsCredentials, WithPyicebergConfig from dlt import version from dlt.common.utils import without_none @@ -15,7 +16,7 @@ @configspec -class AzureCredentialsBase(CredentialsConfiguration): +class AzureCredentialsBase(CredentialsConfiguration, WithObjectStoreRsCredentials): azure_storage_account_name: str = None azure_account_host: Optional[str] = None """Alternative host when accessing blob storage endpoint ie. my_account.dfs.core.windows.net""" @@ -32,7 +33,7 @@ def to_object_store_rs_credentials(self) -> Dict[str, str]: @configspec -class AzureCredentialsWithoutDefaults(AzureCredentialsBase): +class AzureCredentialsWithoutDefaults(AzureCredentialsBase, WithPyicebergConfig): """Credentials for Azure Blob Storage, compatible with adlfs""" azure_storage_account_key: Optional[TSecretStrValue] = None @@ -49,6 +50,13 @@ def to_adlfs_credentials(self) -> Dict[str, Any]: account_host=self.azure_account_host, ) + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + return { + "adlfs.account-name": self.azure_storage_account_name, + "adlfs.account-key": self.azure_storage_account_key, + "adlfs.sas-token": self.azure_storage_sas_token, + } + def create_sas_token(self) -> None: try: from azure.storage.blob import generate_account_sas, ResourceTypes @@ -72,7 +80,7 @@ def on_partial(self) -> None: @configspec -class AzureServicePrincipalCredentialsWithoutDefaults(AzureCredentialsBase): +class AzureServicePrincipalCredentialsWithoutDefaults(AzureCredentialsBase, WithPyicebergConfig): azure_tenant_id: str = None azure_client_id: str = None azure_client_secret: TSecretStrValue = None @@ -86,6 +94,14 @@ def to_adlfs_credentials(self) -> Dict[str, Any]: client_secret=self.azure_client_secret, ) + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + return { + "adlfs.account-name": self.azure_storage_account_name, + "adlfs.tenant-id": self.azure_tenant_id, + "adlfs.client-id": self.azure_client_id, + "adlfs.client-secret": self.azure_client_secret, + } + @configspec class AzureCredentials(AzureCredentialsWithoutDefaults, CredentialsWithDefault): diff --git a/dlt/common/configuration/specs/base_configuration.py b/dlt/common/configuration/specs/base_configuration.py index 8d913d0542..41d1d7a0ca 100644 --- a/dlt/common/configuration/specs/base_configuration.py +++ b/dlt/common/configuration/specs/base_configuration.py @@ -359,7 +359,7 @@ def _get_resolvable_dataclass_fields(cls) -> Iterator[TDtcField]: def get_resolvable_fields(cls) -> Dict[str, type]: """Returns a mapping of fields to their type hints. Dunders should not be resolved and are not returned""" return { - f.name: eval(f.type) if isinstance(f.type, str) else f.type # type: ignore[arg-type] + f.name: eval(f.type) if isinstance(f.type, str) else f.type for f in cls._get_resolvable_dataclass_fields() } diff --git a/dlt/common/configuration/specs/config_providers_context.py b/dlt/common/configuration/specs/config_providers_context.py index 5d1a5b7f26..a244ab571f 100644 --- a/dlt/common/configuration/specs/config_providers_context.py +++ b/dlt/common/configuration/specs/config_providers_context.py @@ -1,5 +1,4 @@ import contextlib -import dataclasses import io from typing import ClassVar, List @@ -8,10 +7,6 @@ ConfigProvider, ContextProvider, ) -from dlt.common.configuration.specs.base_configuration import ( - ContainerInjectableContext, - NotResolved, -) from dlt.common.configuration.specs import ( GcpServiceAccountCredentials, BaseConfiguration, @@ -137,7 +132,7 @@ def _airflow_providers() -> List[ConfigProvider]: # check if we are in task context and provide more info from airflow.operators.python import get_current_context # noqa - ti: TaskInstance = get_current_context()["ti"] # type: ignore + ti: TaskInstance = get_current_context()["ti"] # type: ignore[assignment,unused-ignore] # log outside of stderr/out redirect if secrets_toml_var is None: diff --git a/dlt/common/configuration/specs/exceptions.py b/dlt/common/configuration/specs/exceptions.py index 928e46a8a0..fe87ef24d7 100644 --- a/dlt/common/configuration/specs/exceptions.py +++ b/dlt/common/configuration/specs/exceptions.py @@ -72,3 +72,7 @@ def __init__(self, spec: Type[Any], native_value: Any): class ObjectStoreRsCredentialsException(ConfigurationException): pass + + +class UnsupportedAuthenticationMethodException(ConfigurationException): + pass diff --git a/dlt/common/configuration/specs/gcp_credentials.py b/dlt/common/configuration/specs/gcp_credentials.py index 60ab1d4b56..17519b032a 100644 --- a/dlt/common/configuration/specs/gcp_credentials.py +++ b/dlt/common/configuration/specs/gcp_credentials.py @@ -11,7 +11,9 @@ InvalidGoogleServicesJson, NativeValueError, OAuth2ScopesRequired, + UnsupportedAuthenticationMethodException, ) +from dlt.common.configuration.specs.mixins import WithObjectStoreRsCredentials, WithPyicebergConfig from dlt.common.exceptions import MissingDependencyException from dlt.common.typing import DictStrAny, TSecretStrValue, StrAny from dlt.common.configuration.specs.base_configuration import ( @@ -23,7 +25,7 @@ @configspec -class GcpCredentials(CredentialsConfiguration): +class GcpCredentials(CredentialsConfiguration, WithObjectStoreRsCredentials, WithPyicebergConfig): token_uri: Final[str] = dataclasses.field( default="https://oauth2.googleapis.com/token", init=False, repr=False, compare=False ) @@ -126,6 +128,12 @@ def to_native_credentials(self) -> Any: else: return ServiceAccountCredentials.from_service_account_info(self) + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + raise UnsupportedAuthenticationMethodException( + "Service Account authentication not supported with `iceberg` table format. Use OAuth" + " authentication instead." + ) + def __str__(self) -> str: return f"{self.client_email}@{self.project_id}" @@ -176,11 +184,19 @@ def to_native_representation(self) -> str: return json.dumps(self._info_dict()) def to_object_store_rs_credentials(self) -> Dict[str, str]: - raise NotImplementedError( - "`object_store` Rust crate does not support OAuth for GCP credentials. Reference:" - " https://docs.rs/object_store/latest/object_store/gcp." + raise UnsupportedAuthenticationMethodException( + "OAuth authentication not supported with `delta` table format. Use Service Account or" + " Application Default Credentials authentication instead." ) + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + self.auth() + return { + "gcs.project-id": self.project_id, + "gcs.oauth2.token": self.token, + "gcs.oauth2.token-expires-at": (pendulum.now().timestamp() + 60) * 1000, + } + def auth(self, scopes: Union[str, List[str]] = None, redirect_url: str = None) -> None: if not self.refresh_token: self.add_scopes(scopes) @@ -313,6 +329,12 @@ def to_native_credentials(self) -> Any: else: return super().to_native_credentials() + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + raise UnsupportedAuthenticationMethodException( + "Application Default Credentials authentication not supported with `iceberg` table" + " format. Use OAuth authentication instead." + ) + @configspec class GcpServiceAccountCredentials( @@ -334,3 +356,9 @@ def parse_native_representation(self, native_value: Any) -> None: except NativeValueError: pass GcpOAuthCredentialsWithoutDefaults.parse_native_representation(self, native_value) + + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + if self.has_default_credentials(): + return GcpDefaultCredentials.to_pyiceberg_fileio_config(self) + else: + return GcpOAuthCredentialsWithoutDefaults.to_pyiceberg_fileio_config(self) diff --git a/dlt/common/configuration/specs/mixins.py b/dlt/common/configuration/specs/mixins.py new file mode 100644 index 0000000000..2f843aee5b --- /dev/null +++ b/dlt/common/configuration/specs/mixins.py @@ -0,0 +1,24 @@ +from typing import Dict, Any +from abc import abstractmethod, ABC + + +class WithObjectStoreRsCredentials(ABC): + @abstractmethod + def to_object_store_rs_credentials(self) -> Dict[str, Any]: + """Returns credentials dictionary for object_store Rust crate. + + Can be used for libraries that build on top of the object_store crate, such as `deltalake`. + + https://docs.rs/object_store/latest/object_store/ + """ + pass + + +class WithPyicebergConfig(ABC): + @abstractmethod + def to_pyiceberg_fileio_config(self) -> Dict[str, Any]: + """Returns `pyiceberg` FileIO configuration dictionary. + + https://py.iceberg.apache.org/configuration/#fileio + """ + pass diff --git a/dlt/common/data_writers/buffered.py b/dlt/common/data_writers/buffered.py index e2b6c9a442..6ef431a4d0 100644 --- a/dlt/common/data_writers/buffered.py +++ b/dlt/common/data_writers/buffered.py @@ -242,7 +242,7 @@ def _flush_items(self, allow_empty_file: bool = False) -> None: if self.writer_spec.is_binary_format: self._file = self.open(self._file_name, "wb") # type: ignore else: - self._file = self.open(self._file_name, "wt", encoding="utf-8", newline="") # type: ignore + self._file = self.open(self._file_name, "wt", encoding="utf-8", newline="") self._writer = self.writer_cls(self._file, caps=self._caps) # type: ignore[assignment] self._writer.write_header(self._current_columns) # write buffer diff --git a/dlt/common/destination/utils.py b/dlt/common/destination/utils.py index 0bad5b152e..c98344b687 100644 --- a/dlt/common/destination/utils.py +++ b/dlt/common/destination/utils.py @@ -38,7 +38,7 @@ def verify_schema_capabilities( exception_log: List[Exception] = [] # combined casing function case_identifier = lambda ident: capabilities.casefold_identifier( - (str if capabilities.has_case_sensitive_identifiers else str.casefold)(ident) # type: ignore + (str if capabilities.has_case_sensitive_identifiers else str.casefold)(ident) ) table_name_lookup: DictStrStr = {} # name collision explanation diff --git a/dlt/common/libs/deltalake.py b/dlt/common/libs/deltalake.py index 4047bc3a1a..0f938e7102 100644 --- a/dlt/common/libs/deltalake.py +++ b/dlt/common/libs/deltalake.py @@ -10,6 +10,7 @@ from dlt.common.exceptions import MissingDependencyException from dlt.common.storages import FilesystemConfiguration from dlt.common.utils import assert_min_pkg_version +from dlt.common.configuration.specs.mixins import WithObjectStoreRsCredentials from dlt.destinations.impl.filesystem.filesystem import FilesystemClient try: @@ -191,10 +192,9 @@ def get_delta_tables( def _deltalake_storage_options(config: FilesystemConfiguration) -> Dict[str, str]: """Returns dict that can be passed as `storage_options` in `deltalake` library.""" - creds = {} # type: ignore + creds = {} extra_options = {} - # TODO: create a mixin with to_object_store_rs_credentials for a proper discovery - if hasattr(config.credentials, "to_object_store_rs_credentials"): + if isinstance(config.credentials, WithObjectStoreRsCredentials): creds = config.credentials.to_object_store_rs_credentials() if config.deltalake_storage_options is not None: extra_options = config.deltalake_storage_options diff --git a/dlt/common/libs/pyiceberg.py b/dlt/common/libs/pyiceberg.py new file mode 100644 index 0000000000..19ce9abbf2 --- /dev/null +++ b/dlt/common/libs/pyiceberg.py @@ -0,0 +1,192 @@ +from typing import Dict, Any, List, Optional + +from dlt import version, Pipeline +from dlt.common.libs.pyarrow import cast_arrow_schema_types +from dlt.common.schema.typing import TWriteDisposition +from dlt.common.utils import assert_min_pkg_version +from dlt.common.exceptions import MissingDependencyException +from dlt.common.storages.configuration import FileSystemCredentials +from dlt.common.configuration.specs import CredentialsConfiguration +from dlt.common.configuration.specs.mixins import WithPyicebergConfig +from dlt.destinations.impl.filesystem.filesystem import FilesystemClient + + +try: + from pyiceberg.table import Table as IcebergTable + from pyiceberg.catalog import MetastoreCatalog + import pyarrow as pa +except ModuleNotFoundError: + raise MissingDependencyException( + "dlt pyiceberg helpers", + [f"{version.DLT_PKG_NAME}[pyiceberg]"], + "Install `pyiceberg` so dlt can create Iceberg tables in the `filesystem` destination.", + ) + + +def ensure_iceberg_compatible_arrow_schema(schema: pa.Schema) -> pa.Schema: + ARROW_TO_ICEBERG_COMPATIBLE_ARROW_TYPE_MAP = { + pa.types.is_time: pa.string(), + pa.types.is_decimal256: pa.string(), # pyarrow does not allow downcasting to decimal128 + } + return cast_arrow_schema_types(schema, ARROW_TO_ICEBERG_COMPATIBLE_ARROW_TYPE_MAP) + + +def ensure_iceberg_compatible_arrow_data(data: pa.Table) -> pa.Table: + schema = ensure_iceberg_compatible_arrow_schema(data.schema) + return data.cast(schema) + + +def write_iceberg_table( + table: IcebergTable, + data: pa.Table, + write_disposition: TWriteDisposition, +) -> None: + if write_disposition == "append": + table.append(ensure_iceberg_compatible_arrow_data(data)) + elif write_disposition == "replace": + table.overwrite(ensure_iceberg_compatible_arrow_data(data)) + + +def get_sql_catalog(credentials: FileSystemCredentials) -> "SqlCatalog": # type: ignore[name-defined] # noqa: F821 + assert_min_pkg_version( + pkg_name="sqlalchemy", + version="2.0.18", + msg=( + "`sqlalchemy>=2.0.18` is needed for `iceberg` table format on `filesystem` destination." + ), + ) + + from pyiceberg.catalog.sql import SqlCatalog + + return SqlCatalog( + "default", + uri="sqlite:///:memory:", + **_get_fileio_config(credentials), + ) + + +def create_or_evolve_table( + catalog: MetastoreCatalog, + client: FilesystemClient, + table_name: str, + namespace_name: Optional[str] = None, + schema: Optional[pa.Schema] = None, + partition_columns: Optional[List[str]] = None, +) -> MetastoreCatalog: + # add table to catalog + table_id = f"{namespace_name}.{table_name}" + table_path = f"{client.dataset_path}/{table_name}" + metadata_path = f"{table_path}/metadata" + if client.fs_client.exists(metadata_path): + # found metadata; register existing table + table = _register_table(table_id, metadata_path, catalog, client) + + # evolve schema + if schema is not None: + with table.update_schema() as update: + update.union_by_name(ensure_iceberg_compatible_arrow_schema(schema)) + else: + # found no metadata; create new table + assert schema is not None + with catalog.create_table_transaction( + table_id, + schema=ensure_iceberg_compatible_arrow_schema(schema), + location=_make_path(table_path, client), + ) as txn: + # add partitioning + with txn.update_spec() as update_spec: + for col in partition_columns: + update_spec.add_identity(col) + + return catalog + + +def get_catalog( + client: FilesystemClient, + table_name: str, + namespace_name: Optional[str] = None, + schema: Optional[pa.Schema] = None, + partition_columns: Optional[List[str]] = None, +) -> MetastoreCatalog: + """Returns single-table, ephemeral, in-memory Iceberg catalog.""" + + # create in-memory catalog + catalog: MetastoreCatalog = get_sql_catalog(client.config.credentials) + + # create namespace + if namespace_name is None: + namespace_name = client.dataset_name + catalog.create_namespace(namespace_name) + + # add table to catalog + catalog = create_or_evolve_table( + catalog=catalog, + client=client, + table_name=table_name, + namespace_name=namespace_name, + schema=schema, + partition_columns=partition_columns, + ) + + return catalog + + +def get_iceberg_tables( + pipeline: Pipeline, *tables: str, schema_name: Optional[str] = None +) -> Dict[str, IcebergTable]: + from dlt.common.schema.utils import get_table_format + + with pipeline.destination_client(schema_name=schema_name) as client: + assert isinstance( + client, FilesystemClient + ), "The `get_iceberg_tables` function requires a `filesystem` destination." + + schema_iceberg_tables = [ + t["name"] + for t in client.schema.tables.values() + if get_table_format(client.schema.tables, t["name"]) == "iceberg" + ] + if len(tables) > 0: + invalid_tables = set(tables) - set(schema_iceberg_tables) + if len(invalid_tables) > 0: + available_schemas = "" + if len(pipeline.schema_names) > 1: + available_schemas = f" Available schemas are {pipeline.schema_names}" + raise ValueError( + f"Schema {client.schema.name} does not contain Iceberg tables with these names:" + f" {', '.join(invalid_tables)}.{available_schemas}" + ) + schema_iceberg_tables = [t for t in schema_iceberg_tables if t in tables] + + return { + name: get_catalog(client, name).load_table(f"{pipeline.dataset_name}.{name}") + for name in schema_iceberg_tables + } + + +def _get_fileio_config(credentials: CredentialsConfiguration) -> Dict[str, Any]: + if isinstance(credentials, WithPyicebergConfig): + return credentials.to_pyiceberg_fileio_config() + return {} + + +def _get_last_metadata_file(metadata_path: str, client: FilesystemClient) -> str: + # TODO: implement faster way to obtain `last_metadata_file` (listing is slow) + metadata_files = [f for f in client.fs_client.ls(metadata_path) if f.endswith(".json")] + return _make_path(sorted(metadata_files)[-1], client) + + +def _register_table( + identifier: str, + metadata_path: str, + catalog: MetastoreCatalog, + client: FilesystemClient, +) -> IcebergTable: + last_metadata_file = _get_last_metadata_file(metadata_path, client) + return catalog.register_table(identifier, last_metadata_file) + + +def _make_path(path: str, client: FilesystemClient) -> str: + # don't use file protocol for local files because duckdb does not support it + # https://github.com/duckdb/duckdb/issues/13669 + return path if client.is_local_filesystem else client.config.make_url(path) diff --git a/dlt/common/logger.py b/dlt/common/logger.py index b163c15672..634e305805 100644 --- a/dlt/common/logger.py +++ b/dlt/common/logger.py @@ -47,7 +47,7 @@ def is_logging() -> bool: def log_level() -> str: if not LOGGER: raise RuntimeError("Logger not initialized") - return logging.getLevelName(LOGGER.level) # type: ignore + return logging.getLevelName(LOGGER.level) def is_json_logging(log_format: str) -> bool: diff --git a/dlt/common/metrics.py b/dlt/common/metrics.py index d6acf19d0d..2f9f574dd0 100644 --- a/dlt/common/metrics.py +++ b/dlt/common/metrics.py @@ -9,7 +9,7 @@ class DataWriterMetrics(NamedTuple): created: float last_modified: float - def __add__(self, other: Tuple[object, ...], /) -> Tuple[object, ...]: + def __add__(self, other: Tuple[object, ...], /) -> Tuple[object, ...]: # type: ignore[override] if isinstance(other, DataWriterMetrics): return DataWriterMetrics( self.file_path if self.file_path == other.file_path else "", diff --git a/dlt/common/reflection/utils.py b/dlt/common/reflection/utils.py index c612c5a4f1..27c7bd8758 100644 --- a/dlt/common/reflection/utils.py +++ b/dlt/common/reflection/utils.py @@ -90,24 +90,24 @@ def rewrite_python_script( last_line = -1 last_offset = -1 # sort transformed nodes by line and offset - for node, t_value in sorted(transformed_nodes, key=lambda n: (n[0].lineno, n[0].col_offset)): + for node, t_value in sorted(transformed_nodes, key=lambda n: (n[0].lineno, n[0].col_offset)): # type: ignore[attr-defined] # do we have a line changed - if last_line != node.lineno - 1: + if last_line != node.lineno - 1: # type: ignore[attr-defined] # add remainder from the previous line if last_offset >= 0: script_lines.append(source_script_lines[last_line][last_offset:]) # add all new lines from previous line to current - script_lines.extend(source_script_lines[last_line + 1 : node.lineno - 1]) + script_lines.extend(source_script_lines[last_line + 1 : node.lineno - 1]) # type: ignore[attr-defined] # add trailing characters until node in current line starts - script_lines.append(source_script_lines[node.lineno - 1][: node.col_offset]) + script_lines.append(source_script_lines[node.lineno - 1][: node.col_offset]) # type: ignore[attr-defined] elif last_offset >= 0: # no line change, add the characters from the end of previous node to the current - script_lines.append(source_script_lines[last_line][last_offset : node.col_offset]) + script_lines.append(source_script_lines[last_line][last_offset : node.col_offset]) # type: ignore[attr-defined] # replace node value script_lines.append(ast_unparse(t_value).strip()) - last_line = node.end_lineno - 1 - last_offset = node.end_col_offset + last_line = node.end_lineno - 1 # type: ignore[attr-defined] + last_offset = node.end_col_offset # type: ignore[attr-defined] # add all that was missing if last_offset >= 0: diff --git a/dlt/common/schema/schema.py b/dlt/common/schema/schema.py index d6031a08fa..276bbe9c09 100644 --- a/dlt/common/schema/schema.py +++ b/dlt/common/schema/schema.py @@ -525,7 +525,7 @@ def get_new_table_columns( Typically they come from the destination schema. Columns that are in `existing_columns` and not in `table_name` columns are ignored. Optionally includes incomplete columns (without data type)""" - casefold_f: Callable[[str], str] = str.casefold if not case_sensitive else str # type: ignore[assignment] + casefold_f: Callable[[str], str] = str.casefold if not case_sensitive else str casefold_existing = { casefold_f(col_name): col for col_name, col in existing_columns.items() } diff --git a/dlt/common/typing.py b/dlt/common/typing.py index a3364d1b07..8986d753f3 100644 --- a/dlt/common/typing.py +++ b/dlt/common/typing.py @@ -446,7 +446,7 @@ def get_generic_type_argument_from_instance( if cls_: orig_param_type = get_args(cls_)[0] if orig_param_type in (Any, CallableAny) and sample_value is not None: - orig_param_type = type(sample_value) + orig_param_type = type(sample_value) # type: ignore[assignment] return orig_param_type # type: ignore diff --git a/dlt/destinations/impl/filesystem/factory.py b/dlt/destinations/impl/filesystem/factory.py index 2463da58fa..906bd157e4 100644 --- a/dlt/destinations/impl/filesystem/factory.py +++ b/dlt/destinations/impl/filesystem/factory.py @@ -19,7 +19,7 @@ def filesystem_loader_file_format_selector( *, table_schema: TTableSchema, ) -> t.Tuple[TLoaderFileFormat, t.Sequence[TLoaderFileFormat]]: - if table_schema.get("table_format") == "delta": + if table_schema.get("table_format") in ("delta", "iceberg"): return ("parquet", ["parquet"]) return (preferred_loader_file_format, supported_loader_file_formats) @@ -43,7 +43,7 @@ def _raw_capabilities(self) -> DestinationCapabilitiesContext: caps = DestinationCapabilitiesContext.generic_capabilities( preferred_loader_file_format="jsonl", loader_file_format_selector=filesystem_loader_file_format_selector, - supported_table_formats=["delta"], + supported_table_formats=["delta", "iceberg"], supported_merge_strategies=["upsert"], merge_strategies_selector=filesystem_merge_strategies_selector, ) diff --git a/dlt/destinations/impl/filesystem/filesystem.py b/dlt/destinations/impl/filesystem/filesystem.py index 1739c87fb3..ccf764811b 100644 --- a/dlt/destinations/impl/filesystem/filesystem.py +++ b/dlt/destinations/impl/filesystem/filesystem.py @@ -119,16 +119,27 @@ def metrics(self) -> Optional[LoadJobMetrics]: return m._replace(remote_url=self.make_remote_url()) -class DeltaLoadFilesystemJob(FilesystemLoadJob): +class TableFormatLoadFilesystemJob(FilesystemLoadJob): def __init__(self, file_path: str) -> None: super().__init__(file_path=file_path) self.file_paths = ReferenceFollowupJobRequest.resolve_references(self._file_path) def make_remote_path(self) -> str: - # remote path is table dir - delta will create its file structure inside it return self._job_client.get_table_dir(self.load_table_name) + @property + def arrow_dataset(self) -> Any: + from dlt.common.libs.pyarrow import pyarrow + + return pyarrow.dataset.dataset(self.file_paths) + + @property + def _partition_columns(self) -> List[str]: + return get_columns_names_with_prop(self._load_table, "partition") + + +class DeltaLoadFilesystemJob(TableFormatLoadFilesystemJob): def run(self) -> None: # create Arrow dataset from Parquet files from dlt.common.libs.pyarrow import pyarrow as pa @@ -138,7 +149,7 @@ def run(self) -> None: f"Will copy file(s) {self.file_paths} to delta table {self.make_remote_url()} [arrow" f" buffer: {pa.total_allocated_bytes()}]" ) - source_ds = pa.dataset.dataset(self.file_paths) + source_ds = self.arrow_dataset delta_table = self._delta_table() # explicitly check if there is data @@ -148,9 +159,6 @@ def run(self) -> None: else: with source_ds.scanner().to_reader() as arrow_rbr: # RecordBatchReader if self._load_table["write_disposition"] == "merge" and delta_table is not None: - self._load_table["x-merge-strategy"] = resolve_merge_strategy( # type: ignore[typeddict-unknown-key] - self._schema.tables, self._load_table, self._job_client.capabilities - ) merge_delta_table( table=delta_table, data=arrow_rbr, @@ -188,10 +196,6 @@ def _delta_table(self) -> Optional["DeltaTable"]: # type: ignore[name-defined] else: return None - @property - def _partition_columns(self) -> List[str]: - return get_columns_names_with_prop(self._load_table, "partition") - def _create_or_evolve_delta_table(self, arrow_ds: "Dataset", delta_table: "DeltaTable") -> "DeltaTable": # type: ignore[name-defined] # noqa: F821 from dlt.common.libs.deltalake import ( DeltaTable, @@ -211,13 +215,36 @@ def _create_or_evolve_delta_table(self, arrow_ds: "Dataset", delta_table: "Delta return _evolve_delta_table_schema(delta_table, arrow_ds.schema) +class IcebergLoadFilesystemJob(TableFormatLoadFilesystemJob): + def run(self) -> None: + from dlt.common.libs.pyiceberg import write_iceberg_table + + write_iceberg_table( + table=self._iceberg_table(), + data=self.arrow_dataset.to_table(), + write_disposition=self._load_table["write_disposition"], + ) + + def _iceberg_table(self) -> "pyiceberg.table.Table": # type: ignore[name-defined] # noqa: F821 + from dlt.common.libs.pyiceberg import get_catalog + + catalog = get_catalog( + client=self._job_client, + table_name=self.load_table_name, + schema=self.arrow_dataset.schema, + partition_columns=self._partition_columns, + ) + return catalog.load_table(self.table_identifier) + + @property + def table_identifier(self) -> str: + return f"{self._job_client.dataset_name}.{self.load_table_name}" + + class FilesystemLoadJobWithFollowup(HasFollowupJobs, FilesystemLoadJob): def create_followup_jobs(self, final_state: TLoadJobState) -> List[FollowupJobRequest]: jobs = super().create_followup_jobs(final_state) - if self._load_table.get("table_format") == "delta": - # delta table jobs only require table chain followup jobs - pass - elif final_state == "completed": + if final_state == "completed": ref_job = ReferenceFollowupJobRequest( original_file_name=self.file_name(), remote_paths=[self._job_client.make_remote_url(self.make_remote_path())], @@ -394,6 +421,13 @@ def prepare_load_table(self, table_name: str) -> PreparedTableSchema: if table["write_disposition"] == "merge": table["write_disposition"] = "append" table.pop("table_format", None) + merge_strategy = resolve_merge_strategy(self.schema.tables, table, self.capabilities) + if table["write_disposition"] == "merge": + if merge_strategy is None: + # no supported merge strategies, fall back to append + table["write_disposition"] = "append" + else: + table["x-merge-strategy"] = merge_strategy # type: ignore[typeddict-unknown-key] return table def get_table_dir(self, table_name: str, remote: bool = False) -> str: @@ -458,12 +492,20 @@ def create_load_job( # where we want to load the state the regular way if table["name"] == self.schema.state_table_name and not self.config.as_staging_destination: return FinalizedLoadJob(file_path) - if table.get("table_format") == "delta": - import dlt.common.libs.deltalake # assert dependencies are installed + table_format = table.get("table_format") + if table_format in ("delta", "iceberg"): # a reference job for a delta table indicates a table chain followup job if ReferenceFollowupJobRequest.is_reference_job(file_path): - return DeltaLoadFilesystemJob(file_path) + if table_format == "delta": + import dlt.common.libs.deltalake + + return DeltaLoadFilesystemJob(file_path) + elif table_format == "iceberg": + import dlt.common.libs.pyiceberg + + return IcebergLoadFilesystemJob(file_path) + # otherwise just continue return FinalizedLoadJobWithFollowupJobs(file_path) @@ -494,10 +536,10 @@ def should_load_data_to_staging_dataset(self, table_name: str) -> bool: def should_truncate_table_before_load(self, table_name: str) -> bool: table = self.prepare_load_table(table_name) - return ( - table["write_disposition"] == "replace" - and not table.get("table_format") == "delta" # Delta can do a logical replace - ) + return table["write_disposition"] == "replace" and not table.get("table_format") in ( + "delta", + "iceberg", + ) # Delta/Iceberg can do a logical replace # # state stuff @@ -718,7 +760,7 @@ def create_table_chain_completed_followup_jobs( jobs = super().create_table_chain_completed_followup_jobs( table_chain, completed_table_chain_jobs ) - if table_chain[0].get("table_format") == "delta": + if table_chain[0].get("table_format") in ("delta", "iceberg"): for table in table_chain: table_job_paths = [ job.file_path diff --git a/dlt/destinations/impl/filesystem/sql_client.py b/dlt/destinations/impl/filesystem/sql_client.py index d03a00b418..d39f4c3431 100644 --- a/dlt/destinations/impl/filesystem/sql_client.py +++ b/dlt/destinations/impl/filesystem/sql_client.py @@ -13,6 +13,7 @@ from dlt.common.destination.reference import DBApiCursor +from dlt.common.storages.fsspec_filesystem import AZURE_BLOB_STORAGE_PROTOCOLS from dlt.destinations.sql_client import raise_database_error from dlt.destinations.impl.duckdb.sql_client import DuckDbSqlClient @@ -169,8 +170,9 @@ def create_authentication(self, persistent: bool = False, secret_name: str = Non # native google storage implementation is not supported.. elif self.fs_client.config.protocol in ["gs", "gcs"]: logger.warn( - "For gs/gcs access via duckdb please use the gs/gcs s3 compatibility layer. Falling" - " back to fsspec." + "For gs/gcs access via duckdb please use the gs/gcs s3 compatibility layer if" + " possible (not supported when using `iceberg` table format). Falling back to" + " fsspec." ) self._conn.register_filesystem(self.fs_client.fs_client) @@ -192,7 +194,7 @@ def open_connection(self) -> duckdb.DuckDBPyConnection: # the line below solves problems with certificate path lookup on linux # see duckdb docs - if self.fs_client.config.protocol in ["az", "abfss"]: + if self.fs_client.config.protocol in AZURE_BLOB_STORAGE_PROTOCOLS: self._conn.sql("SET azure_transport_option_type = 'curl';") return self._conn @@ -258,6 +260,13 @@ def create_views_for_tables(self, tables: Dict[str, str]) -> None: from_statement = "" if schema_table.get("table_format") == "delta": from_statement = f"delta_scan('{resolved_folder}')" + elif schema_table.get("table_format") == "iceberg": + from dlt.common.libs.pyiceberg import _get_last_metadata_file + + self._setup_iceberg(self._conn) + metadata_path = f"{resolved_folder}/metadata" + last_metadata_file = _get_last_metadata_file(metadata_path, self.fs_client) + from_statement = f"iceberg_scan('{last_metadata_file}')" elif first_file_type == "parquet": from_statement = f"read_parquet([{resolved_files_string}])" elif first_file_type == "jsonl": @@ -267,7 +276,7 @@ def create_views_for_tables(self, tables: Dict[str, str]) -> None: else: raise NotImplementedError( f"Unknown filetype {first_file_type} for table {table_name}. Currently only" - " jsonl and parquet files as well as delta tables are supported." + " jsonl and parquet files as well as delta and iceberg tables are supported." ) # create table @@ -299,6 +308,16 @@ def execute_query(self, query: AnyStr, *args: Any, **kwargs: Any) -> Iterator[DB with super().execute_query(query, *args, **kwargs) as cursor: yield cursor + @staticmethod + def _setup_iceberg(conn: duckdb.DuckDBPyConnection) -> None: + # needed to make persistent secrets work in new connection + # https://github.com/duckdb/duckdb_iceberg/issues/83 + conn.execute("FROM duckdb_secrets();") + + # `duckdb_iceberg` extension does not support autoloading + # https://github.com/duckdb/duckdb_iceberg/issues/71 + conn.execute("INSTALL iceberg; LOAD iceberg;") + def __del__(self) -> None: if self.memory_db: self.memory_db.close() diff --git a/dlt/destinations/impl/sqlalchemy/factory.py b/dlt/destinations/impl/sqlalchemy/factory.py index edd827ed00..e61ac1fb6a 100644 --- a/dlt/destinations/impl/sqlalchemy/factory.py +++ b/dlt/destinations/impl/sqlalchemy/factory.py @@ -81,6 +81,9 @@ def adjust_capabilities( caps.max_column_identifier_length = dialect.max_identifier_length caps.supports_native_boolean = dialect.supports_native_boolean if dialect.name == "mysql": + # correct max identifier length + # dialect uses 255 (max length for aliases) instead of 64 (max length of identifiers) + caps.max_identifier_length = 64 caps.format_datetime_literal = _format_mysql_datetime_literal return caps diff --git a/dlt/extract/incremental/lag.py b/dlt/extract/incremental/lag.py index ee102a9961..dfafa2cd11 100644 --- a/dlt/extract/incremental/lag.py +++ b/dlt/extract/incremental/lag.py @@ -20,7 +20,7 @@ def _apply_lag_to_value( parsed_value = ensure_pendulum_datetime(value) if is_str else value if isinstance(parsed_value, (datetime, date)): - parsed_value = _apply_lag_to_datetime(lag, parsed_value, last_value_func, is_str_date) + parsed_value = _apply_lag_to_datetime(lag, parsed_value, last_value_func, is_str_date) # type: ignore[assignment] # go back to string or pass exact type value = parsed_value.strftime(value_format) if value_format else parsed_value # type: ignore[assignment] diff --git a/dlt/helpers/airflow_helper.py b/dlt/helpers/airflow_helper.py index 99458a3949..aaa19ea97d 100644 --- a/dlt/helpers/airflow_helper.py +++ b/dlt/helpers/airflow_helper.py @@ -18,7 +18,7 @@ from airflow.configuration import conf from airflow.models import TaskInstance from airflow.utils.task_group import TaskGroup - from airflow.operators.dummy import DummyOperator # type: ignore + from airflow.operators.dummy import DummyOperator from airflow.operators.python import PythonOperator, get_current_context except ModuleNotFoundError: raise MissingDependencyException("Airflow", ["apache-airflow>=2.5"]) @@ -255,7 +255,7 @@ def _run( # use task logger if self.use_task_logger: - ti: TaskInstance = get_current_context()["ti"] # type: ignore + ti: TaskInstance = get_current_context()["ti"] # type: ignore[assignment,unused-ignore] logger.LOGGER = ti.log # set global number of buffered items diff --git a/dlt/helpers/dbt/profiles.yml b/dlt/helpers/dbt/profiles.yml index a2a0014e4e..fd114478fb 100644 --- a/dlt/helpers/dbt/profiles.yml +++ b/dlt/helpers/dbt/profiles.yml @@ -83,6 +83,7 @@ duckdb: extensions: - httpfs - parquet + - iceberg # TODO: emit the config of duck db motherduck: diff --git a/docs/website/docs/dlt-ecosystem/destinations/delta-iceberg.md b/docs/website/docs/dlt-ecosystem/destinations/delta-iceberg.md new file mode 100644 index 0000000000..7a056d6b40 --- /dev/null +++ b/docs/website/docs/dlt-ecosystem/destinations/delta-iceberg.md @@ -0,0 +1,168 @@ +--- +title: Delta / Iceberg +description: Delta / Iceberg `dlt` destination +keywords: [delta, iceberg, destination, data warehouse] +--- + +# Delta and Iceberg table formats +`dlt` supports writing [Delta](https://delta.io/) and [Iceberg](https://iceberg.apache.org/) tables when using the [filesystem](./filesystem.md) destination. + +## How it works +`dlt` uses the [deltalake](https://pypi.org/project/deltalake/) and [pyiceberg](https://pypi.org/project/pyiceberg/) libraries to write Delta and Iceberg tables, respectively. One or multiple Parquet files are prepared during the extract and normalize steps. In the load step, these Parquet files are exposed as an Arrow data structure and fed into `deltalake` or `pyiceberg`. + +## Iceberg single-user ephemeral catalog +`dlt` uses single-table, ephemeral, in-memory, sqlite-based [Iceberg catalog](https://iceberg.apache.org/concepts/catalog/)s. These catalogs are created "on demand" when a pipeline is run, and do not persist afterwards. If a table already exists in the filesystem, it gets registered into the catalog using its latest metadata file. This allows for a serverless setup. It is currently not possible to connect your own Iceberg catalog. + +:::caution +While ephemeral catalogs make it easy to get started with Iceberg, it comes with limitations: +- concurrent writes are not handled and may lead to corrupt table state +- we cannot guarantee that reads concurrent with writes are clean +- the latest manifest file needs to be searched for using file listing—this can become slow with large tables, especially in cloud object stores +::: + +## Delta dependencies + +You need the `deltalake` package to use this format: + +```sh +pip install "dlt[deltalake]" +``` + +You also need `pyarrow>=17.0.0`: + +```sh +pip install 'pyarrow>=17.0.0' +``` + +## Iceberg dependencies + +You need Python version 3.9 or higher and the `pyiceberg` package to use this format: + +```sh +pip install "dlt[pyiceberg]" +``` + +You also need `sqlalchemy>=2.0.18`: + +```sh +pip install 'sqlalchemy>=2.0.18' +``` + +## Set table format + +Set the `table_format` argument to `delta` or `iceberg` when defining your resource: + +```py +@dlt.resource(table_format="delta") +def my_delta_resource(): + ... +``` + +or when calling `run` on your pipeline: + +```py +pipeline.run(my_resource, table_format="delta") +``` + +:::note +`dlt` always uses Parquet as `loader_file_format` when using the `delta` or `iceberg` table format. Any setting of `loader_file_format` is disregarded. +::: + + +## Table format partitioning +Both `delta` and `iceberg` tables can be partitioned by specifying one or more `partition` column hints. This example partitions a Delta table by the `foo` column: + +```py +@dlt.resource( + table_format="delta", + columns={"foo": {"partition": True}} +) +def my_delta_resource(): + ... +``` + +:::note +Delta uses [Hive-style partitioning](https://delta.io/blog/pros-cons-hive-style-partionining/), while Iceberg uses [hidden partioning](https://iceberg.apache.org/docs/latest/partitioning/). +::: + +:::caution +Partition evolution (changing partition columns after a table has been created) is not supported. +::: + +## Table access helper functions +You can use the `get_delta_tables` and `get_iceberg_tables` helper functions to acccess native table objects. For `delta` these are `deltalake` [DeltaTable](https://delta-io.github.io/delta-rs/api/delta_table/) objects, for `iceberg` these are `pyiceberg` [Table](https://py.iceberg.apache.org/reference/pyiceberg/table/#pyiceberg.table.Table) objects. + +```py +from dlt.common.libs.deltalake import get_delta_tables +# from dlt.common.libs.pyiceberg import get_iceberg_tables + +... + +# get dictionary of DeltaTable objects +delta_tables = get_delta_tables(pipeline) + +# execute operations on DeltaTable objects +delta_tables["my_delta_table"].optimize.compact() +delta_tables["another_delta_table"].optimize.z_order(["col_a", "col_b"]) +# delta_tables["my_delta_table"].vacuum() +# etc. +``` + +## Table format Google Cloud Storage authentication + +Note that not all authentication methods are supported when using table formats on Google Cloud Storage: + +| Authentication method | `delta` | `iceberg` | +| -- | -- | -- | +| [Service Account](bigquery.md#setup-guide) | ✅ | ❌ | +| [OAuth](../destinations/bigquery.md#oauth-20-authentication) | ❌ | ✅ | +| [Application Default Credentials](bigquery.md#using-default-credentials) | ✅ | ❌ | + +:::note +The [S3-compatible](#using-s3-compatible-storage) interface for Google Cloud Storage is not supported when using `iceberg`. +::: + +## Iceberg Azure scheme +The `az` [scheme](#supported-schemes) is not supported when using the `iceberg` table format. Please use the `abfss` scheme. This is because `pyiceberg`, which `dlt` used under the hood, currently does not support `az`. + +## Table format `merge` support (**experimental**) +The [`upsert`](../../general-usage/incremental-loading.md#upsert-strategy) merge strategy is supported for `delta`. For `iceberg`, the `merge` write disposition is not supported and falls back to `append`. + +:::caution +The `upsert` merge strategy for the filesystem destination with Delta table format is **experimental**. +::: + +```py +@dlt.resource( + write_disposition={"disposition": "merge", "strategy": "upsert"}, + primary_key="my_primary_key", + table_format="delta" +) +def my_upsert_resource(): + ... +... +``` + +### Known limitations +- `hard_delete` hint not supported +- Deleting records from nested tables not supported + - This means updates to JSON columns that involve element removals are not propagated. For example, if you first load `{"key": 1, "nested": [1, 2]}` and then load `{"key": 1, "nested": [1]}`, then the record for element `2` will not be deleted from the nested table. + +## Delta table format storage options +You can pass storage options by configuring `destination.filesystem.deltalake_storage_options`: + +```toml +[destination.filesystem] +deltalake_storage_options = '{"AWS_S3_LOCKING_PROVIDER": "dynamodb", "DELTA_DYNAMO_TABLE_NAME": "custom_table_name"}' +``` + +`dlt` passes these options to the `storage_options` argument of the `write_deltalake` method in the `deltalake` library. Look at their [documentation](https://delta-io.github.io/delta-rs/api/delta_writer/#deltalake.write_deltalake) to see which options can be used. + +You don't need to specify credentials here. `dlt` merges the required credentials with the options you provided before passing it as `storage_options`. + +>❗When using `s3`, you need to specify storage options to [configure](https://delta-io.github.io/delta-rs/usage/writing/writing-to-s3-with-locking-provider/) locking behavior. + +## Delta table format memory usage +:::caution +Beware that when loading a large amount of data for one table, the underlying rust implementation will consume a lot of memory. This is a known issue and the maintainers are actively working on a solution. You can track the progress [here](https://github.com/delta-io/delta-rs/pull/2289). Until the issue is resolved, you can mitigate the memory consumption by doing multiple smaller incremental pipeline runs. +::: \ No newline at end of file diff --git a/docs/website/docs/dlt-ecosystem/destinations/filesystem.md b/docs/website/docs/dlt-ecosystem/destinations/filesystem.md index 9b243b9429..de3d12e8e1 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/filesystem.md +++ b/docs/website/docs/dlt-ecosystem/destinations/filesystem.md @@ -108,7 +108,8 @@ You need to create an S3 bucket and a user who can access that bucket. dlt does #### Using S3 compatible storage -To use an S3 compatible storage other than AWS S3, such as [MinIO](https://min.io/) or [Cloudflare R2](https://www.cloudflare.com/en-ca/developer-platform/r2/), you may supply an `endpoint_url` in the config. This should be set along with AWS credentials: +To use an S3 compatible storage other than AWS S3, such as [MinIO](https://min.io/), [Cloudflare R2](https://www.cloudflare.com/en-ca/developer-platform/r2/) or [Google +Cloud Storage](https://cloud.google.com/storage/docs/interoperability), you may supply an `endpoint_url` in the config. This should be set along with AWS credentials: ```toml [destination.filesystem] @@ -166,6 +167,8 @@ Run `pip install "dlt[az]"` which will install the `adlfs` package to interface Edit the credentials in `.dlt/secrets.toml`, you'll see AWS credentials by default; replace them with your Azure credentials. +#### Supported schemes + `dlt` supports both forms of the blob storage urls: ```toml [destination.filesystem] @@ -404,29 +407,6 @@ The filesystem destination handles the write dispositions as follows: - `replace` - all files that belong to such tables are deleted from the dataset folder, and then the current set of files is added. - `merge` - falls back to `append` -### Merge with Delta table format (experimental) -The [`upsert`](../../general-usage/incremental-loading.md#upsert-strategy) merge strategy is supported when using the [Delta table format](#delta-table-format). - -:::caution -The `upsert` merge strategy for the filesystem destination with Delta table format is experimental. -::: - -```py -@dlt.resource( - write_disposition={"disposition": "merge", "strategy": "upsert"}, - primary_key="my_primary_key", - table_format="delta" -) -def my_upsert_resource(): - ... -... -``` - -#### Known limitations -- `hard_delete` hint not supported -- Deleting records from nested tables not supported - - This means updates to JSON columns that involve element removals are not propagated. For example, if you first load `{"key": 1, "nested": [1, 2]}` and then load `{"key": 1, "nested": [1]}`, then the record for element `2` will not be deleted from the nested table. - ## File compression The filesystem destination in the dlt library uses `gzip` compression by default for efficiency, which may result in the files being stored in a compressed format. This format may not be easily readable as plain text or JSON Lines (`jsonl`) files. If you encounter files that seem unreadable, they may be compressed. @@ -645,88 +625,9 @@ You can choose the following file formats: ## Supported table formats -You can choose the following table formats: -* [Delta table](../table-formats/delta.md) is supported - -### Delta table format - -You need the `deltalake` package to use this format: - -```sh -pip install "dlt[deltalake]" -``` - -You also need `pyarrow>=17.0.0`: - -```sh -pip install 'pyarrow>=17.0.0' -``` - -Set the `table_format` argument to `delta` when defining your resource: - -```py -@dlt.resource(table_format="delta") -def my_delta_resource(): - ... -``` - -:::note -`dlt` always uses Parquet as `loader_file_format` when using the `delta` table format. Any setting of `loader_file_format` is disregarded. -::: - -:::caution -Beware that when loading a large amount of data for one table, the underlying rust implementation will consume a lot of memory. This is a known issue and the maintainers are actively working on a solution. You can track the progress [here](https://github.com/delta-io/delta-rs/pull/2289). Until the issue is resolved, you can mitigate the memory consumption by doing multiple smaller incremental pipeline runs. -::: - -#### Delta table partitioning -A Delta table can be partitioned ([Hive-style partitioning](https://delta.io/blog/pros-cons-hive-style-partionining/)) by specifying one or more `partition` column hints. This example partitions the Delta table by the `foo` column: - -```py -@dlt.resource( - table_format="delta", - columns={"foo": {"partition": True}} -) -def my_delta_resource(): - ... -``` - -:::caution -It is **not** possible to change partition columns after the Delta table has been created. Trying to do so causes an error stating that the partition columns don't match. -::: - - -#### Storage options -You can pass storage options by configuring `destination.filesystem.deltalake_storage_options`: - -```toml -[destination.filesystem] -deltalake_storage_options = '{"AWS_S3_LOCKING_PROVIDER": "dynamodb", "DELTA_DYNAMO_TABLE_NAME": "custom_table_name"}' -``` - -`dlt` passes these options to the `storage_options` argument of the `write_deltalake` method in the `deltalake` library. Look at their [documentation](https://delta-io.github.io/delta-rs/api/delta_writer/#deltalake.write_deltalake) to see which options can be used. - -You don't need to specify credentials here. `dlt` merges the required credentials with the options you provided before passing it as `storage_options`. - ->❗When using `s3`, you need to specify storage options to [configure](https://delta-io.github.io/delta-rs/usage/writing/writing-to-s3-with-locking-provider/) locking behavior. - -#### `get_delta_tables` helper -You can use the `get_delta_tables` helper function to get `deltalake` [DeltaTable](https://delta-io.github.io/delta-rs/api/delta_table/) objects for your Delta tables: - -```py -from dlt.common.libs.deltalake import get_delta_tables - -... - -# get dictionary of DeltaTable objects -delta_tables = get_delta_tables(pipeline) - -# execute operations on DeltaTable objects -delta_tables["my_delta_table"].optimize.compact() -delta_tables["another_delta_table"].optimize.z_order(["col_a", "col_b"]) -# delta_tables["my_delta_table"].vacuum() -# etc. - -``` +You can choose the following [table formats](./delta-iceberg.md): +* Delta table +* Iceberg ## Syncing of dlt state This destination fully supports [dlt state sync](../../general-usage/state#syncing-state-with-destination). To this end, special folders and files will be created at your destination which hold information about your pipeline state, schemas, and completed loads. These folders DO NOT respect your settings in the layout section. When using filesystem as a staging destination, not all of these folders are created, as the state and schemas are managed in the regular way by the final destination you have configured. diff --git a/docs/website/docs/dlt-ecosystem/table-formats/iceberg.md b/docs/website/docs/dlt-ecosystem/table-formats/iceberg.md index 233ae0ce21..edca521e52 100644 --- a/docs/website/docs/dlt-ecosystem/table-formats/iceberg.md +++ b/docs/website/docs/dlt-ecosystem/table-formats/iceberg.md @@ -10,5 +10,5 @@ keywords: [iceberg, table formats] ## Supported destinations -Supported by: **Athena** +Supported by: **Athena**, **filesystem** diff --git a/docs/website/docs/general-usage/dataset-access/ibis-backend.md b/docs/website/docs/general-usage/dataset-access/ibis-backend.md index 8f4b0fb6b6..9f9b65e9c0 100644 --- a/docs/website/docs/general-usage/dataset-access/ibis-backend.md +++ b/docs/website/docs/general-usage/dataset-access/ibis-backend.md @@ -6,7 +6,7 @@ keywords: [data, dataset, ibis] # Ibis -Ibis is a powerful portable Python dataframe library. Learn more about what it is and how to use it in the [official documentation](https://ibis-project.org/). +Ibis is a powerful portable Python dataframe library. Learn more about what it is and how to use it in the [official documentation](https://ibis-project.org/). `dlt` provides an easy way to hand over your loaded dataset to an Ibis backend connection. @@ -46,4 +46,3 @@ print(table.limit(10).execute()) # Visit the ibis docs to learn more about the available methods ``` - diff --git a/docs/website/sidebars.js b/docs/website/sidebars.js index 274f3e82b3..8e8c11fc09 100644 --- a/docs/website/sidebars.js +++ b/docs/website/sidebars.js @@ -167,6 +167,7 @@ const sidebars = { 'dlt-ecosystem/destinations/synapse', 'dlt-ecosystem/destinations/clickhouse', 'dlt-ecosystem/destinations/filesystem', + 'dlt-ecosystem/destinations/delta-iceberg', 'dlt-ecosystem/destinations/postgres', 'dlt-ecosystem/destinations/redshift', 'dlt-ecosystem/destinations/snowflake', diff --git a/mypy.ini b/mypy.ini index 769e84b13a..fdf0ceb1e6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -135,3 +135,9 @@ ignore_missing_imports = True [mypy-time_machine.*] ignore_missing_imports = True + +[mypy-pyiceberg.*] +ignore_missing_imports = True + +[mypy-airflow.*] +ignore_missing_imports = True diff --git a/poetry.lock b/poetry.lock index 749979439d..83090360b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1543,13 +1543,13 @@ files = [ [[package]] name = "cachetools" -version = "5.3.1" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] @@ -5872,44 +5872,49 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.12.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801"}, + {file = "mypy-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5"}, + {file = "mypy-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1"}, + {file = "mypy-1.12.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627"}, + {file = "mypy-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20"}, + {file = "mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735"}, + {file = "mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66"}, + {file = "mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6"}, + {file = "mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931"}, + {file = "mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811"}, + {file = "mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f"}, + {file = "mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0"}, + {file = "mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042"}, + {file = "mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179"}, + {file = "mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a"}, + {file = "mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc"}, + {file = "mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635"}, + {file = "mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81"}, + {file = "mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4"}, + {file = "mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02"}, + {file = "mypy-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b16fe09f9c741d85a2e3b14a5257a27a4f4886c171d562bc5a5e90d8591906b8"}, + {file = "mypy-1.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0dcc1e843d58f444fce19da4cce5bd35c282d4bde232acdeca8279523087088a"}, + {file = "mypy-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e10ba7de5c616e44ad21005fa13450cd0de7caaa303a626147d45307492e4f2d"}, + {file = "mypy-1.12.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e6fe449223fa59fbee351db32283838a8fee8059e0028e9e6494a03802b4004"}, + {file = "mypy-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:dc6e2a2195a290a7fd5bac3e60b586d77fc88e986eba7feced8b778c373f9afe"}, + {file = "mypy-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:de5b2a8988b4e1269a98beaf0e7cc71b510d050dce80c343b53b4955fff45f19"}, + {file = "mypy-1.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843826966f1d65925e8b50d2b483065c51fc16dc5d72647e0236aae51dc8d77e"}, + {file = "mypy-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe20f89da41a95e14c34b1ddb09c80262edcc295ad891f22cc4b60013e8f78d"}, + {file = "mypy-1.12.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8135ffec02121a75f75dc97c81af7c14aa4ae0dda277132cfcd6abcd21551bfd"}, + {file = "mypy-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a7b76fa83260824300cc4834a3ab93180db19876bce59af921467fd03e692810"}, + {file = "mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e"}, + {file = "mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -7521,6 +7526,74 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyiceberg" +version = "0.8.1" +description = "Apache Iceberg is an open table format for huge analytic datasets" +optional = true +python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*,>=3.9" +files = [ + {file = "pyiceberg-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c121d1d3baf64510db94740ad870ae4b6eb9eb59a5ff7ecb4e96f7510666b2f"}, + {file = "pyiceberg-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6f14aa588a3883fc7fddc136ca75b75660b4abb0b55b4c541619953f8971e7"}, + {file = "pyiceberg-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c720c2a191ac6faf01fe4c0f4c01c64b94bf064185b0292003d42939049277c"}, + {file = "pyiceberg-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d421d6e51ac1c581cba9fce96aa6b9118cf4a02270066a7fdc9490ab5d57ece9"}, + {file = "pyiceberg-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:ae11fb0515ea0a046370e09a7f6039a7e86622ab910360eaa732f0106b8f00c7"}, + {file = "pyiceberg-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9488954c9eb5ce42ca6b816fc61873f219414cfdb9e9928d1c4a302702be1d89"}, + {file = "pyiceberg-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44179e0fb844887b440c162279ba526dfe0e0f72d32945236528838518b55af0"}, + {file = "pyiceberg-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e121c6f5505d8ec711a1dd1690e07156cd54fb3d0844d5d991e02f1593f2708"}, + {file = "pyiceberg-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5961a288f2d4bbb2ab300c803da1bf0e70cea837e3f14b14108827cc821af252"}, + {file = "pyiceberg-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbe192324a6fb552c2fd29cab51086e21fa248ea2a0b95fbab921dede49e5a69"}, + {file = "pyiceberg-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:60430f0d8f6d650ed7d1893d038b847565a8e9ac135a1cc812e57d24f0482f6c"}, + {file = "pyiceberg-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0f697977dac672d8b00e125836423585a97ebf59a28b865b1296a2b6ee81c51"}, + {file = "pyiceberg-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370de7c230970ff858f713d150164d492ba8450e771e59a0c520520b13ea6226"}, + {file = "pyiceberg-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3036ed226020d50e30648a71f968cf78bde5d6b609294508e60754e100e5ef36"}, + {file = "pyiceberg-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:9ac9555f3bd25a31059229089ae639cf738a8e8286a175cea128561ac1ed9452"}, + {file = "pyiceberg-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51da3a553d3a881042bf436e66a91cc2b6c4a3fea0e174cd73af2eb6ed255323"}, + {file = "pyiceberg-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:863f1dce7340e6ed870706a3fa4a73457178dae8529725bb80522ddcd4253afb"}, + {file = "pyiceberg-0.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dbf52b39080a6a2cda6a5126a74e3a88d5b206f609c128d001a728b36b81075"}, + {file = "pyiceberg-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb77d65e8efbb883c163817e4a9c373d907110ab6343c1b816b48f336955d4d7"}, + {file = "pyiceberg-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1fcd35b7de0eddc3fd8fd0c38b98741217ef6de4eeb0e72b798b4007692aa76c"}, + {file = "pyiceberg-0.8.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6f0f56f8fc61bcd795f6a3d03e8ce6bee09ebaa64425eb08327e975f906d98be"}, + {file = "pyiceberg-0.8.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7099c6631743ad29c451de2bebd9ed3c96c42bcb1fe5d5d5c93aec895858e3f"}, + {file = "pyiceberg-0.8.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6436f5a782491115f64131882a737d77c9dc0040493e1b7f9b3081ea8cf6a26"}, + {file = "pyiceberg-0.8.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c1d75b40a98a327f7436eb0d6187c51834c44b79adf61c6945b33645f4afbf17"}, + {file = "pyiceberg-0.8.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8de988fa2363e6a51b40b85b5ff1e8261cda5bfc14ac54dd4ebe58391b95acae"}, + {file = "pyiceberg-0.8.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:dd06c5b606011155aa0b76e7b001e30f1c40ab2fb3eeb8a0652b88629259c2bb"}, + {file = "pyiceberg-0.8.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8142f0dbc12dda0e6d7aaf564a3fbb0f17fc934630e7cf866773c8caaebf666"}, + {file = "pyiceberg-0.8.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6126ee3a46ff975f15abf2085f184591d21643bffb96330907e003eea0b63005"}, + {file = "pyiceberg-0.8.1.tar.gz", hash = "sha256:4502f0cfddf6f7cd48b9cd54016bce0ab94052b0ab01efcfa515879074f4c8e3"}, +] + +[package.dependencies] +cachetools = ">=5.5.0,<6.0.0" +click = ">=7.1.1,<9.0.0" +fsspec = ">=2023.1.0" +mmh3 = ">=4.0.0,<6.0.0" +pydantic = ">=2.0,<2.4.0 || >2.4.0,<2.4.1 || >2.4.1,<3.0" +pyparsing = ">=3.1.0,<4.0.0" +requests = ">=2.20.0,<3.0.0" +rich = ">=10.11.0,<14.0.0" +sortedcontainers = "2.4.0" +strictyaml = ">=1.7.0,<2.0.0" +tenacity = ">=8.2.3,<10.0.0" + +[package.extras] +adlfs = ["adlfs (>=2023.1.0)"] +daft = ["getdaft (>=0.2.12)"] +duckdb = ["duckdb (>=0.5.0,<2.0.0)", "pyarrow (>=14.0.0,<19.0.0)"] +dynamodb = ["boto3 (>=1.24.59)"] +gcsfs = ["gcsfs (>=2023.1.0)"] +glue = ["boto3 (>=1.24.59)", "mypy-boto3-glue (>=1.28.18)"] +hive = ["thrift (>=0.13.0,<1.0.0)"] +pandas = ["pandas (>=1.0.0,<3.0.0)", "pyarrow (>=14.0.0,<19.0.0)"] +pyarrow = ["pyarrow (>=14.0.0,<19.0.0)"] +ray = ["pandas (>=1.0.0,<3.0.0)", "pyarrow (>=14.0.0,<19.0.0)", "ray (==2.10.0)", "ray (>=2.10.0,<3.0.0)"] +s3fs = ["s3fs (>=2023.1.0)"] +snappy = ["python-snappy (>=0.6.0,<1.0.0)"] +sql-postgres = ["psycopg2-binary (>=2.9.6)", "sqlalchemy (>=2.0.18,<3.0.0)"] +sql-sqlite = ["sqlalchemy (>=2.0.18,<3.0.0)"] +zstandard = ["zstandard (>=0.13.0,<1.0.0)"] + [[package]] name = "pyjwt" version = "2.8.0" @@ -9327,6 +9400,20 @@ files = [ [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[[package]] +name = "strictyaml" +version = "1.7.3" +description = "Strict, typed YAML parser" +optional = true +python-versions = ">=3.7.0" +files = [ + {file = "strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7"}, + {file = "strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407"}, +] + +[package.dependencies] +python-dateutil = ">=2.6.0" + [[package]] name = "sympy" version = "1.12" @@ -10606,6 +10693,7 @@ mssql = ["pyodbc"] parquet = ["pyarrow"] postgis = ["psycopg2-binary", "psycopg2cffi"] postgres = ["psycopg2-binary", "psycopg2cffi"] +pyiceberg = ["pyarrow", "pyiceberg", "sqlalchemy"] qdrant = ["qdrant-client"] redshift = ["psycopg2-binary", "psycopg2cffi"] s3 = ["botocore", "s3fs"] @@ -10619,4 +10707,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.13" -content-hash = "a7cd6b599326d80b5beb8d4a3d3e3b4074eda6dc53daa5c296ef8d54002c5f78" +content-hash = "84e8b8eccd9b8ee104a2dc08f5b83987aeb06540d61330390ce849cc1ad6acb4" diff --git a/pyproject.toml b/pyproject.toml index 0fb7f94e36..bfa830cd06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ cron-descriptor = {version = ">=1.2.32", optional = true} pipdeptree = {version = ">=2.9.0,<2.10", optional = true} pyathena = {version = ">=2.9.6", optional = true} weaviate-client = {version = ">=3.22", optional = true} -adlfs = {version = ">=2022.4.0", optional = true} +adlfs = {version = ">=2024.7.0", optional = true} pyodbc = {version = ">=4.0.39", optional = true} qdrant-client = {version = ">=1.8", optional = true, extras = ["fastembed"]} databricks-sql-connector = {version = ">=2.9.3", optional = true} @@ -89,6 +89,12 @@ alembic = {version = ">1.10.0", optional = true} paramiko = {version = ">=3.3.0", optional = true} sqlglot = {version = ">=20.0.0", optional = true} db-dtypes = { version = ">=1.2.0", optional = true } +# `sql-sqlite` extra leads to dependency conflict with `apache-airflow` because `apache-airflow` +# requires `sqlalchemy<2.0.0` while the extra requires `sqlalchemy>=2.0.18` +# https://github.com/apache/airflow/issues/28723 +# pyiceberg = { version = ">=0.7.1", optional = true, extras = ["sql-sqlite"] } +# we will rely on manual installation of `sqlalchemy>=2.0.18` instead +pyiceberg = { version = ">=0.8.1", python = ">=3.9", optional = true } [tool.poetry.extras] gcp = ["grpcio", "google-cloud-bigquery", "db-dtypes", "gcsfs"] @@ -118,6 +124,7 @@ lancedb = ["lancedb", "pyarrow", "tantivy"] deltalake = ["deltalake", "pyarrow"] sql_database = ["sqlalchemy"] sqlalchemy = ["sqlalchemy", "alembic"] +pyiceberg = ["pyiceberg", "pyarrow", "sqlalchemy"] postgis = ["psycopg2-binary", "psycopg2cffi"] [tool.poetry.scripts] @@ -134,7 +141,7 @@ sqlfluff = "^2.3.2" types-deprecated = "^1.2.9.2" pytest-console-scripts = "^1.4.1" pytest = "^7.0.0" -mypy = "^1.10.0" +mypy = ">=1.11.0,<1.13.0" flake8 = "^5.0.0" bandit = "^1.7.0" black = "^23.7.0" diff --git a/tests/conftest.py b/tests/conftest.py index 6088fa976c..a5a349f8d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,6 +120,9 @@ def _create_pipeline_instance_id(self) -> str: # disable googleapiclient logging logging.getLogger("googleapiclient.discovery_cache").setLevel("WARNING") + # disable pyiceberg logging + logging.getLogger("pyiceberg").setLevel("WARNING") + # reset and init airflow db import warnings diff --git a/tests/libs/test_csv_writer.py b/tests/libs/test_csv_writer.py index 3c30123e1c..a120cd048e 100644 --- a/tests/libs/test_csv_writer.py +++ b/tests/libs/test_csv_writer.py @@ -178,7 +178,7 @@ def test_non_utf8_binary(item_type: TestDataItemFormat) -> None: table = pq.read_table(f) else: table = data - writer_type: Type[DataWriter] = ArrowToCsvWriter if item_type == "arrow-table" else CsvWriter # type: ignore + writer_type: Type[DataWriter] = ArrowToCsvWriter if item_type == "arrow-table" else CsvWriter with pytest.raises(InvalidDataItem) as inv_ex: with get_writer(writer_type, disable_compression=True) as writer: @@ -195,7 +195,7 @@ def test_arrow_struct() -> None: @pytest.mark.parametrize("item_type", ["object", "arrow-table"]) def test_csv_writer_empty(item_type: TestDataItemFormat) -> None: - writer_type: Type[DataWriter] = ArrowToCsvWriter if item_type == "arrow-table" else CsvWriter # type: ignore + writer_type: Type[DataWriter] = ArrowToCsvWriter if item_type == "arrow-table" else CsvWriter with get_writer(writer_type, disable_compression=True) as writer: writer.write_empty_file(TABLE_UPDATE_COLUMNS_SCHEMA) diff --git a/tests/load/filesystem/test_object_store_rs_credentials.py b/tests/load/filesystem/test_credentials_mixins.py similarity index 50% rename from tests/load/filesystem/test_object_store_rs_credentials.py rename to tests/load/filesystem/test_credentials_mixins.py index f23187a269..c1fb02c152 100644 --- a/tests/load/filesystem/test_object_store_rs_credentials.py +++ b/tests/load/filesystem/test_credentials_mixins.py @@ -1,12 +1,8 @@ -"""Tests translation of `dlt` credentials into `object_store` Rust crate credentials.""" - -from typing import Any, Dict +from typing import Any, Dict, Union, Type, get_args, cast import os import json # noqa: I251 import pytest -from deltalake import DeltaTable -from deltalake.exceptions import TableNotFoundError import dlt from dlt.common.configuration import resolve_configuration @@ -23,10 +19,15 @@ from dlt.common.utils import custom_environ from dlt.common.configuration.resolve import resolve_configuration from dlt.common.configuration.specs.gcp_credentials import GcpDefaultCredentials -from dlt.common.configuration.specs.exceptions import ObjectStoreRsCredentialsException +from dlt.common.configuration.specs.exceptions import ( + ObjectStoreRsCredentialsException, + UnsupportedAuthenticationMethodException, +) +from dlt.common.configuration.specs.mixins import WithObjectStoreRsCredentials, WithPyicebergConfig from tests.load.utils import ( AZ_BUCKET, + ABFS_BUCKET, AWS_BUCKET, GCS_BUCKET, R2_BUCKET_CONFIG, @@ -34,6 +35,9 @@ ) +TCredentialsMixin = Union[WithObjectStoreRsCredentials, WithPyicebergConfig] +ALL_CREDENTIALS_MIXINS = get_args(TCredentialsMixin) + pytestmark = pytest.mark.essential if all(driver not in ALL_FILESYSTEM_DRIVERS for driver in ("az", "s3", "gs", "r2")): @@ -53,11 +57,27 @@ def fs_creds() -> Dict[str, Any]: return creds -def can_connect(bucket_url: str, object_store_rs_credentials: Dict[str, str]) -> bool: - """Returns True if client can connect to object store, False otherwise. +def can_connect(bucket_url: str, credentials: TCredentialsMixin, mixin: Type[TCredentialsMixin]) -> bool: # type: ignore[return] + """Returns True if client can connect to object store, False otherwise.""" + if mixin == WithObjectStoreRsCredentials: + credentials = cast(WithObjectStoreRsCredentials, credentials) + return can_connect_object_store_rs_credentials( + bucket_url, credentials.to_object_store_rs_credentials() + ) + elif mixin == WithPyicebergConfig: + credentials = cast(WithPyicebergConfig, credentials) + return can_connect_pyiceberg_fileio_config( + bucket_url, credentials.to_pyiceberg_fileio_config() + ) + + +def can_connect_object_store_rs_credentials( + bucket_url: str, object_store_rs_credentials: Dict[str, str] +) -> bool: + # uses `deltatable` library as Python interface to `object_store` Rust crate + from deltalake import DeltaTable + from deltalake.exceptions import TableNotFoundError - Uses `deltatable` library as Python interface to `object_store` Rust crate. - """ try: DeltaTable( bucket_url, @@ -70,16 +90,40 @@ def can_connect(bucket_url: str, object_store_rs_credentials: Dict[str, str]) -> return False +def can_connect_pyiceberg_fileio_config( + bucket_url: str, pyiceberg_fileio_config: Dict[str, str] +) -> bool: + from pyiceberg.table import StaticTable + + try: + StaticTable.from_metadata( + f"{bucket_url}/non_existing_metadata_file.json", + properties=pyiceberg_fileio_config, + ) + except FileNotFoundError: + # this error implies the connection was successful + # there is no Iceberg metadata file at the specified path + return True + return False + + @pytest.mark.parametrize( - "driver", [driver for driver in ALL_FILESYSTEM_DRIVERS if driver in ("az")] + "driver", [driver for driver in ALL_FILESYSTEM_DRIVERS if driver in ("az", "abfss")] ) -def test_azure_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) -> None: +@pytest.mark.parametrize("mixin", ALL_CREDENTIALS_MIXINS) +def test_azure_credentials_mixins( + driver: str, fs_creds: Dict[str, Any], mixin: Type[TCredentialsMixin] +) -> None: + if mixin == WithPyicebergConfig and driver == "az": + pytest.skip("`pyiceberg` does not support `az` scheme") + + buckets = {"az": AZ_BUCKET, "abfss": ABFS_BUCKET} creds: AnyAzureCredentials creds = AzureServicePrincipalCredentialsWithoutDefaults( **dlt.secrets.get("destination.fsazureprincipal.credentials") ) - assert can_connect(AZ_BUCKET, creds.to_object_store_rs_credentials()) + assert can_connect(buckets[driver], creds, mixin) # without SAS token creds = AzureCredentialsWithoutDefaults( @@ -87,18 +131,21 @@ def test_azure_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any] azure_storage_account_key=fs_creds["azure_storage_account_key"], ) assert creds.azure_storage_sas_token is None - assert can_connect(AZ_BUCKET, creds.to_object_store_rs_credentials()) + assert can_connect(buckets[driver], creds, mixin) # with SAS token creds = resolve_configuration(creds) assert creds.azure_storage_sas_token is not None - assert can_connect(AZ_BUCKET, creds.to_object_store_rs_credentials()) + assert can_connect(buckets[driver], creds, mixin) @pytest.mark.parametrize( "driver", [driver for driver in ALL_FILESYSTEM_DRIVERS if driver in ("s3", "r2")] ) -def test_aws_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) -> None: +@pytest.mark.parametrize("mixin", ALL_CREDENTIALS_MIXINS) +def test_aws_credentials_mixins( + driver: str, fs_creds: Dict[str, Any], mixin: Type[TCredentialsMixin] +) -> None: creds: AwsCredentialsWithoutDefaults if driver == "r2": @@ -112,9 +159,11 @@ def test_aws_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) endpoint_url=fs_creds.get("endpoint_url"), ) assert creds.aws_session_token is None - object_store_rs_creds = creds.to_object_store_rs_credentials() - assert "aws_session_token" not in object_store_rs_creds # no auto-generated token - assert can_connect(AWS_BUCKET, object_store_rs_creds) + if mixin == WithObjectStoreRsCredentials: + assert ( + "aws_session_token" not in creds.to_object_store_rs_credentials() + ) # no auto-generated token + assert can_connect(AWS_BUCKET, creds, mixin) # AwsCredentials: no user-provided session token creds = AwsCredentials( @@ -124,24 +173,27 @@ def test_aws_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) endpoint_url=fs_creds.get("endpoint_url"), ) assert creds.aws_session_token is None - object_store_rs_creds = creds.to_object_store_rs_credentials() - assert "aws_session_token" not in object_store_rs_creds # no auto-generated token - assert can_connect(AWS_BUCKET, object_store_rs_creds) - - # exception should be raised if both `endpoint_url` and `region_name` are - # not provided - with pytest.raises(ObjectStoreRsCredentialsException): - AwsCredentials( - aws_access_key_id=fs_creds["aws_access_key_id"], - aws_secret_access_key=fs_creds["aws_secret_access_key"], - ).to_object_store_rs_credentials() - - if "endpoint_url" in object_store_rs_creds: - # TODO: make sure this case is tested on GitHub CI, e.g. by adding - # a local MinIO bucket to the set of tested buckets - if object_store_rs_creds["endpoint_url"].startswith("http://"): + assert can_connect(AWS_BUCKET, creds, mixin) + if mixin == WithObjectStoreRsCredentials: + object_store_rs_creds = creds.to_object_store_rs_credentials() + assert "aws_session_token" not in object_store_rs_creds # no auto-generated token + + # exception should be raised if both `endpoint_url` and `region_name` are + # not provided + with pytest.raises(ObjectStoreRsCredentialsException): + AwsCredentials( + aws_access_key_id=fs_creds["aws_access_key_id"], + aws_secret_access_key=fs_creds["aws_secret_access_key"], + ).to_object_store_rs_credentials() + + if "endpoint_url" in object_store_rs_creds and object_store_rs_creds[ + "endpoint_url" + ].startswith("http://"): + # TODO: make sure this case is tested on GitHub CI, e.g. by adding + # a local MinIO bucket to the set of tested buckets assert object_store_rs_creds["aws_allow_http"] == "true" + if creds.endpoint_url is not None: # remainder of tests use session tokens # we don't run them on S3 compatible storage because session tokens # may not be available @@ -158,9 +210,10 @@ def test_aws_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) region_name=fs_creds["region_name"], ) assert creds.aws_session_token is not None - object_store_rs_creds = creds.to_object_store_rs_credentials() - assert object_store_rs_creds["aws_session_token"] is not None - assert can_connect(AWS_BUCKET, object_store_rs_creds) + assert can_connect(AWS_BUCKET, creds, mixin) + if mixin == WithObjectStoreRsCredentials: + object_store_rs_creds = creds.to_object_store_rs_credentials() + assert object_store_rs_creds["aws_session_token"] is not None # AwsCredentialsWithoutDefaults: user-provided session token creds = AwsCredentialsWithoutDefaults( @@ -170,15 +223,19 @@ def test_aws_object_store_rs_credentials(driver: str, fs_creds: Dict[str, Any]) region_name=fs_creds["region_name"], ) assert creds.aws_session_token is not None - object_store_rs_creds = creds.to_object_store_rs_credentials() - assert object_store_rs_creds["aws_session_token"] is not None - assert can_connect(AWS_BUCKET, object_store_rs_creds) + assert can_connect(AWS_BUCKET, creds, mixin) + if mixin == WithObjectStoreRsCredentials: + object_store_rs_creds = creds.to_object_store_rs_credentials() + assert object_store_rs_creds["aws_session_token"] is not None @pytest.mark.parametrize( "driver", [driver for driver in ALL_FILESYSTEM_DRIVERS if driver in ("gs")] ) -def test_gcp_object_store_rs_credentials(driver, fs_creds: Dict[str, Any]) -> None: +@pytest.mark.parametrize("mixin", ALL_CREDENTIALS_MIXINS) +def test_gcp_credentials_mixins( + driver, fs_creds: Dict[str, Any], mixin: Type[TCredentialsMixin] +) -> None: creds: GcpCredentials # GcpServiceAccountCredentialsWithoutDefaults @@ -189,7 +246,11 @@ def test_gcp_object_store_rs_credentials(driver, fs_creds: Dict[str, Any]) -> No private_key_id=fs_creds["private_key_id"], client_email=fs_creds["client_email"], ) - assert can_connect(GCS_BUCKET, creds.to_object_store_rs_credentials()) + if mixin == WithPyicebergConfig: + with pytest.raises(UnsupportedAuthenticationMethodException): + assert can_connect(GCS_BUCKET, creds, mixin) + elif mixin == WithObjectStoreRsCredentials: + assert can_connect(GCS_BUCKET, creds, mixin) # GcpDefaultCredentials @@ -197,7 +258,7 @@ def test_gcp_object_store_rs_credentials(driver, fs_creds: Dict[str, Any]) -> No GcpDefaultCredentials._LAST_FAILED_DEFAULT = 0 # write service account key to JSON file - service_json = json.loads(creds.to_object_store_rs_credentials()["service_account_key"]) + service_json = json.loads(creds.to_native_representation()) path = "_secrets/service.json" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: @@ -206,8 +267,18 @@ def test_gcp_object_store_rs_credentials(driver, fs_creds: Dict[str, Any]) -> No with custom_environ({"GOOGLE_APPLICATION_CREDENTIALS": path}): creds = GcpDefaultCredentials() resolve_configuration(creds) - can_connect(GCS_BUCKET, creds.to_object_store_rs_credentials()) - - # GcpOAuthCredentialsWithoutDefaults is currently not supported - with pytest.raises(NotImplementedError): - GcpOAuthCredentialsWithoutDefaults().to_object_store_rs_credentials() + if mixin == WithPyicebergConfig: + with pytest.raises(UnsupportedAuthenticationMethodException): + assert can_connect(GCS_BUCKET, creds, mixin) + elif mixin == WithObjectStoreRsCredentials: + assert can_connect(GCS_BUCKET, creds, mixin) + + # GcpOAuthCredentialsWithoutDefaults + creds = resolve_configuration( + GcpOAuthCredentialsWithoutDefaults(), sections=("destination", "fsgcpoauth") + ) + if mixin == WithPyicebergConfig: + assert can_connect(GCS_BUCKET, creds, mixin) + elif mixin == WithObjectStoreRsCredentials: + with pytest.raises(UnsupportedAuthenticationMethodException): + assert can_connect(GCS_BUCKET, creds, mixin) diff --git a/tests/load/filesystem/test_sql_client.py b/tests/load/filesystem/test_sql_client.py index ac2ada2551..a73b0f7e31 100644 --- a/tests/load/filesystem/test_sql_client.py +++ b/tests/load/filesystem/test_sql_client.py @@ -1,17 +1,17 @@ """Test the duckdb supported sql client for special internal features""" -from typing import Any +from typing import Optional import pytest import dlt import os import shutil -import logging from dlt import Pipeline from dlt.common.utils import uniq_id +from dlt.common.schema.typing import TTableFormat from tests.load.utils import ( destinations_configs, @@ -19,7 +19,6 @@ GCS_BUCKET, SFTP_BUCKET, MEMORY_BUCKET, - AWS_BUCKET, ) from dlt.destinations import filesystem from tests.utils import TEST_STORAGE_ROOT @@ -37,7 +36,7 @@ def _run_dataset_checks( pipeline: Pipeline, destination_config: DestinationTestConfiguration, secret_directory: str, - table_format: Any = None, + table_format: Optional[TTableFormat] = None, alternate_access_pipeline: Pipeline = None, ) -> None: total_records = 200 @@ -144,6 +143,8 @@ def _external_duckdb_connection() -> duckdb.DuckDBPyConnection: # the line below solves problems with certificate path lookup on linux, see duckdb docs external_db.sql("SET azure_transport_option_type = 'curl';") external_db.sql(f"SET secret_directory = '{secret_directory}';") + if table_format == "iceberg": + FilesystemSqlClient._setup_iceberg(external_db) return external_db def _fs_sql_client_for_external_db( @@ -283,13 +284,13 @@ def test_read_interfaces_filesystem( "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_exclude=[SFTP_BUCKET, MEMORY_BUCKET], # NOTE: delta does not work on memory buckets ), ids=lambda x: x.name, ) -def test_delta_tables( +def test_table_formats( destination_config: DestinationTestConfiguration, secret_directory: str ) -> None: os.environ["DATA_WRITER__FILE_MAX_ITEMS"] = "700" @@ -302,8 +303,9 @@ def test_delta_tables( # in case of gcs we use the s3 compat layer for reading # for writing we still need to use the gc authentication, as delta_rs seems to use # methods on the s3 interface that are not implemented by gcs + # s3 compat layer does not work with `iceberg` table format access_pipeline = pipeline - if destination_config.bucket_url == GCS_BUCKET: + if destination_config.bucket_url == GCS_BUCKET and destination_config.table_format != "iceberg": gcp_bucket = filesystem( GCS_BUCKET.replace("gs://", "s3://"), destination_name="filesystem_s3_gcs_comp" ) @@ -315,7 +317,7 @@ def test_delta_tables( pipeline, destination_config, secret_directory=secret_directory, - table_format="delta", + table_format=destination_config.table_format, alternate_access_pipeline=access_pipeline, ) diff --git a/tests/load/pipeline/test_filesystem_pipeline.py b/tests/load/pipeline/test_filesystem_pipeline.py index 8d890642ee..c70fa5ab5d 100644 --- a/tests/load/pipeline/test_filesystem_pipeline.py +++ b/tests/load/pipeline/test_filesystem_pipeline.py @@ -2,7 +2,7 @@ import os import posixpath from pathlib import Path -from typing import Any, Callable, List, Dict, cast +from typing import Any, Callable, List, Dict, cast, Tuple from importlib.metadata import version as pkg_version from packaging.version import Version @@ -15,7 +15,7 @@ from dlt.common.storages.configuration import FilesystemConfiguration from dlt.common.storages.load_package import ParsedLoadJobFileName from dlt.common.utils import uniq_id -from dlt.common.schema.typing import TWriteDisposition +from dlt.common.schema.typing import TWriteDisposition, TTableFormat from dlt.common.configuration.exceptions import ConfigurationValueError from dlt.destinations import filesystem from dlt.destinations.impl.filesystem.filesystem import FilesystemClient @@ -223,6 +223,48 @@ def some_source(): assert table.column("value").to_pylist() == [1, 2, 3, 4, 5] +# here start the `table_format` tests + + +def get_expected_actual( + pipeline: dlt.Pipeline, + table_name: str, + table_format: TTableFormat, + arrow_table: "pyarrow.Table", # type: ignore[name-defined] # noqa: F821 +) -> Tuple["pyarrow.Table", "pyarrow.Table"]: # type: ignore[name-defined] # noqa: F821 + from dlt.common.libs.pyarrow import pyarrow, cast_arrow_schema_types + + if table_format == "delta": + from dlt.common.libs.deltalake import ( + get_delta_tables, + ensure_delta_compatible_arrow_data, + ) + + dt = get_delta_tables(pipeline, table_name)[table_name] + expected = ensure_delta_compatible_arrow_data(arrow_table) + actual = dt.to_pyarrow_table() + elif table_format == "iceberg": + from dlt.common.libs.pyiceberg import ( + get_iceberg_tables, + ensure_iceberg_compatible_arrow_data, + ) + + it = get_iceberg_tables(pipeline, table_name)[table_name] + expected = ensure_iceberg_compatible_arrow_data(arrow_table) + actual = it.scan().to_arrow() + + # work around pyiceberg bug https://github.com/apache/iceberg-python/issues/1128 + schema = cast_arrow_schema_types( + actual.schema, + { + pyarrow.types.is_large_string: pyarrow.string(), + pyarrow.types.is_large_binary: pyarrow.binary(), + }, + ) + actual = actual.cast(schema) + return (expected, actual) + + @pytest.mark.skip( reason="pyarrow version check not needed anymore, since we have 17 as a dependency" ) @@ -258,44 +300,44 @@ def foo(): "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_exclude=(MEMORY_BUCKET, SFTP_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_core( +def test_table_format_core( destination_config: DestinationTestConfiguration, ) -> None: - """Tests core functionality for `delta` table format. + """Tests core functionality for `delta` and `iceberg` table formats. Tests all data types, all filesystems. Tests `append` and `replace` write dispositions (`merge` is tested elsewhere). """ - - from dlt.common.libs.deltalake import get_delta_tables + if destination_config.table_format == "delta": + from dlt.common.libs.deltalake import get_delta_tables # create resource that yields rows with all data types column_schemas, row = table_update_and_row() - @dlt.resource(columns=column_schemas, table_format="delta") + @dlt.resource(columns=column_schemas, table_format=destination_config.table_format) def data_types(): nonlocal row yield [row] * 10 pipeline = destination_config.setup_pipeline("fs_pipe", dev_mode=True) - # run pipeline, this should create Delta table + # run pipeline, this should create table info = pipeline.run(data_types()) assert_load_info(info) - # `delta` table format should use `parquet` file format + # table formats should use `parquet` file format completed_jobs = info.load_packages[0].jobs["completed_jobs"] data_types_jobs = [ job for job in completed_jobs if job.job_file_info.table_name == "data_types" ] assert all([job.file_path.endswith((".parquet", ".reference")) for job in data_types_jobs]) - # 10 rows should be loaded to the Delta table and the content of the first + # 10 rows should be loaded to the table and the content of the first # row should match expected values rows = load_tables_to_dicts(pipeline, "data_types", exclude_system_cols=True)["data_types"] assert len(rows) == 10 @@ -322,7 +364,8 @@ def data_types(): # should do logical replace, increasing the table version info = pipeline.run(data_types(), write_disposition="replace") assert_load_info(info) - assert get_delta_tables(pipeline, "data_types")["data_types"].version() == 2 + if destination_config.table_format == "delta": + assert get_delta_tables(pipeline, "data_types")["data_types"].version() == 2 rows = load_tables_to_dicts(pipeline, "data_types", exclude_system_cols=True)["data_types"] assert len(rows) == 10 @@ -331,15 +374,16 @@ def data_types(): "destination_config", destinations_configs( table_format_filesystem_configs=True, + # job orchestration is same across table formats—no need to test all formats with_table_format="delta", bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_does_not_contain_job_files( +def test_table_format_does_not_contain_job_files( destination_config: DestinationTestConfiguration, ) -> None: - """Asserts Parquet job files do not end up in Delta table.""" + """Asserts Parquet job files do not end up in table.""" pipeline = destination_config.setup_pipeline("fs_pipe", dev_mode=True) @@ -376,17 +420,18 @@ def delta_table(): "destination_config", destinations_configs( table_format_filesystem_configs=True, + # job orchestration is same across table formats—no need to test all formats with_table_format="delta", bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_multiple_files( +def test_table_format_multiple_files( destination_config: DestinationTestConfiguration, ) -> None: - """Tests loading multiple files into a Delta table. + """Tests loading multiple files into a table. - Files should be loaded into the Delta table in a single commit. + Files should be loaded into the table in a single commit. """ from dlt.common.libs.deltalake import get_delta_tables @@ -422,17 +467,17 @@ def delta_table(): "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_child_tables( +def test_table_format_child_tables( destination_config: DestinationTestConfiguration, ) -> None: - """Tests child table handling for `delta` table format.""" + """Tests child table handling for `delta` and `iceberg` table formats.""" - @dlt.resource(table_format="delta") + @dlt.resource(table_format=destination_config.table_format) def nested_table(): yield [ { @@ -494,49 +539,63 @@ def nested_table(): assert len(rows_dict["nested_table__child"]) == 3 assert len(rows_dict["nested_table__child__grandchild"]) == 5 - # now drop children and grandchildren, use merge write disposition to create and pass full table chain - # also for tables that do not have jobs - info = pipeline.run( - [{"foo": 3}] * 10000, - table_name="nested_table", - primary_key="foo", - write_disposition="merge", - ) - assert_load_info(info) + if destination_config.supports_merge: + # now drop children and grandchildren, use merge write disposition to create and pass full table chain + # also for tables that do not have jobs + info = pipeline.run( + [{"foo": 3}] * 10000, + table_name="nested_table", + primary_key="foo", + write_disposition="merge", + ) + assert_load_info(info) @pytest.mark.parametrize( "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_partitioning( +def test_table_format_partitioning( destination_config: DestinationTestConfiguration, ) -> None: - """Tests partitioning for `delta` table format.""" + """Tests partitioning for `delta` and `iceberg` table formats.""" - from dlt.common.libs.deltalake import get_delta_tables from tests.pipeline.utils import users_materialize_table_schema + def assert_partition_columns( + table_name: str, table_format: TTableFormat, expected_partition_columns: List[str] + ) -> None: + if table_format == "delta": + from dlt.common.libs.deltalake import get_delta_tables + + dt = get_delta_tables(pipeline, table_name)[table_name] + actual_partition_columns = dt.metadata().partition_columns + elif table_format == "iceberg": + from dlt.common.libs.pyiceberg import get_iceberg_tables + + it = get_iceberg_tables(pipeline, table_name)[table_name] + actual_partition_columns = [f.name for f in it.metadata.specs_struct().fields] + assert actual_partition_columns == expected_partition_columns + pipeline = destination_config.setup_pipeline("fs_pipe", dev_mode=True) # zero partition columns - @dlt.resource(table_format="delta") + @dlt.resource(table_format=destination_config.table_format) def zero_part(): yield {"foo": 1, "bar": 1} info = pipeline.run(zero_part()) assert_load_info(info) - dt = get_delta_tables(pipeline, "zero_part")["zero_part"] - assert dt.metadata().partition_columns == [] + assert_partition_columns("zero_part", destination_config.table_format, []) assert load_table_counts(pipeline, "zero_part")["zero_part"] == 1 # one partition column - @dlt.resource(table_format="delta", columns={"c1": {"partition": True}}) + @dlt.resource(table_format=destination_config.table_format, columns={"c1": {"partition": True}}) def one_part(): yield [ {"c1": "foo", "c2": 1}, @@ -547,13 +606,13 @@ def one_part(): info = pipeline.run(one_part()) assert_load_info(info) - dt = get_delta_tables(pipeline, "one_part")["one_part"] - assert dt.metadata().partition_columns == ["c1"] + assert_partition_columns("one_part", destination_config.table_format, ["c1"]) assert load_table_counts(pipeline, "one_part")["one_part"] == 4 # two partition columns @dlt.resource( - table_format="delta", columns={"c1": {"partition": True}, "c2": {"partition": True}} + table_format=destination_config.table_format, + columns={"c1": {"partition": True}, "c2": {"partition": True}}, ) def two_part(): yield [ @@ -565,29 +624,31 @@ def two_part(): info = pipeline.run(two_part()) assert_load_info(info) - dt = get_delta_tables(pipeline, "two_part")["two_part"] - assert dt.metadata().partition_columns == ["c1", "c2"] + assert_partition_columns("two_part", destination_config.table_format, ["c1", "c2"]) assert load_table_counts(pipeline, "two_part")["two_part"] == 4 # test partitioning with empty source users_materialize_table_schema.apply_hints( - table_format="delta", + table_format=destination_config.table_format, columns={"id": {"partition": True}}, ) info = pipeline.run(users_materialize_table_schema()) assert_load_info(info) - dt = get_delta_tables(pipeline, "users")["users"] - assert dt.metadata().partition_columns == ["id"] + assert_partition_columns("users", destination_config.table_format, ["id"]) assert load_table_counts(pipeline, "users")["users"] == 0 # changing partitioning after initial table creation is not supported zero_part.apply_hints(columns={"foo": {"partition": True}}) - with pytest.raises(PipelineStepFailed) as pip_ex: + if destination_config.table_format == "delta": + # Delta raises error when trying to change partitioning + with pytest.raises(PipelineStepFailed) as pip_ex: + pipeline.run(zero_part()) + assert isinstance(pip_ex.value.__context__, LoadClientJobRetry) + assert "partitioning" in pip_ex.value.__context__.retry_message + elif destination_config.table_format == "iceberg": + # while Iceberg supports partition evolution, we don't apply it pipeline.run(zero_part()) - assert isinstance(pip_ex.value.__context__, LoadClientJobRetry) - assert "partitioning" in pip_ex.value.__context__.retry_message - dt = get_delta_tables(pipeline, "zero_part")["zero_part"] - assert dt.metadata().partition_columns == [] + assert_partition_columns("zero_part", destination_config.table_format, []) @pytest.mark.parametrize( @@ -646,7 +707,7 @@ def test_delta_table_partitioning_arrow_load_id( "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, @@ -659,20 +720,25 @@ def test_delta_table_partitioning_arrow_load_id( pytest.param({"disposition": "merge", "strategy": "upsert"}, id="upsert"), ), ) -def test_delta_table_schema_evolution( +def test_table_format_schema_evolution( destination_config: DestinationTestConfiguration, write_disposition: TWriteDisposition, ) -> None: - """Tests schema evolution (adding new columns) for `delta` table format.""" - from dlt.common.libs.deltalake import get_delta_tables, ensure_delta_compatible_arrow_data + """Tests schema evolution (adding new columns) for `delta` and `iceberg` table formats.""" + if destination_config.table_format == "iceberg" and write_disposition == { + "disposition": "merge", + "strategy": "upsert", + }: + pytest.skip("`upsert` currently not implemented for `iceberg`") + from dlt.common.libs.pyarrow import pyarrow @dlt.resource( write_disposition=write_disposition, primary_key="pk", - table_format="delta", + table_format=destination_config.table_format, ) - def delta_table(data): + def evolving_table(data): yield data pipeline = destination_config.setup_pipeline("fs_pipe", dev_mode=True) @@ -684,11 +750,11 @@ def delta_table(data): assert arrow_table.shape == (1, 1) # initial load - info = pipeline.run(delta_table(arrow_table)) + info = pipeline.run(evolving_table(arrow_table)) assert_load_info(info) - dt = get_delta_tables(pipeline, "delta_table")["delta_table"] - expected = ensure_delta_compatible_arrow_data(arrow_table) - actual = dt.to_pyarrow_table() + expected, actual = get_expected_actual( + pipeline, "evolving_table", destination_config.table_format, arrow_table + ) assert actual.equals(expected) # create Arrow table with many columns, two rows @@ -703,11 +769,11 @@ def delta_table(data): arrow_table = arrow_table.add_column(0, pk_field, [[1, 2]]) # second load — this should evolve the schema (i.e. add the new columns) - info = pipeline.run(delta_table(arrow_table)) + info = pipeline.run(evolving_table(arrow_table)) assert_load_info(info) - dt = get_delta_tables(pipeline, "delta_table")["delta_table"] - actual = dt.to_pyarrow_table() - expected = ensure_delta_compatible_arrow_data(arrow_table) + expected, actual = get_expected_actual( + pipeline, "evolving_table", destination_config.table_format, arrow_table + ) if write_disposition == "append": # just check shape and schema for `append`, because table comparison is # more involved than with the other dispositions @@ -724,13 +790,21 @@ def delta_table(data): empty_arrow_table = arrow_table.schema.empty_table() # load 3 — this should evolve the schema without changing data - info = pipeline.run(delta_table(empty_arrow_table)) + info = pipeline.run(evolving_table(empty_arrow_table)) assert_load_info(info) - dt = get_delta_tables(pipeline, "delta_table")["delta_table"] - actual = dt.to_pyarrow_table() - expected_schema = ensure_delta_compatible_arrow_data(arrow_table).schema - assert actual.schema.equals(expected_schema) - expected_num_rows = 3 if write_disposition == "append" else 2 + expected, actual = get_expected_actual( + pipeline, "evolving_table", destination_config.table_format, arrow_table + ) + assert actual.schema.equals(expected.schema) + if write_disposition == "append": + expected_num_rows = 3 + elif write_disposition == "replace": + expected_num_rows = 0 + if destination_config.table_format == "delta": + # TODO: fix https://github.com/dlt-hub/dlt/issues/2092 and remove this if-clause + expected_num_rows = 2 + elif write_disposition == {"disposition": "merge", "strategy": "upsert"}: + expected_num_rows = 2 assert actual.num_rows == expected_num_rows # new column should have NULLs only assert ( @@ -743,23 +817,38 @@ def delta_table(data): "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_subset=(FILE_BUCKET, AZ_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_empty_source( +def test_table_format_empty_source( destination_config: DestinationTestConfiguration, ) -> None: - """Tests empty source handling for `delta` table format. + """Tests empty source handling for `delta` and `iceberg` table formats. Tests both empty Arrow table and `dlt.mark.materialize_table_schema()`. """ - from dlt.common.libs.deltalake import ensure_delta_compatible_arrow_data, get_delta_tables from tests.pipeline.utils import users_materialize_table_schema - @dlt.resource(table_format="delta") - def delta_table(data): + def get_table_version( # type: ignore[return] + pipeline: dlt.Pipeline, + table_name: str, + table_format: TTableFormat, + ) -> int: + if table_format == "delta": + from dlt.common.libs.deltalake import get_delta_tables + + dt = get_delta_tables(pipeline, table_name)[table_name] + return dt.version() + elif table_format == "iceberg": + from dlt.common.libs.pyiceberg import get_iceberg_tables + + it = get_iceberg_tables(pipeline, table_name)[table_name] + return it.last_sequence_number - 1 # subtract 1 to match `delta` + + @dlt.resource(table_format=destination_config.table_format) + def a_table(data): yield data # create empty Arrow table with schema @@ -779,61 +868,62 @@ def delta_table(data): # run 1: empty Arrow table with schema # this should create empty Delta table with same schema as Arrow table - info = pipeline.run(delta_table(empty_arrow_table)) + info = pipeline.run(a_table(empty_arrow_table)) assert_load_info(info) - dt = get_delta_tables(pipeline, "delta_table")["delta_table"] - assert dt.version() == 0 - dt_arrow_table = dt.to_pyarrow_table() - assert dt_arrow_table.shape == (0, empty_arrow_table.num_columns) - assert dt_arrow_table.schema.equals( - ensure_delta_compatible_arrow_data(empty_arrow_table).schema + assert get_table_version(pipeline, "a_table", destination_config.table_format) == 0 + expected, actual = get_expected_actual( + pipeline, "a_table", destination_config.table_format, empty_arrow_table ) + assert actual.shape == (0, expected.num_columns) + assert actual.schema.equals(expected.schema) # run 2: non-empty Arrow table with same schema as run 1 # this should load records into Delta table - info = pipeline.run(delta_table(arrow_table)) + info = pipeline.run(a_table(arrow_table)) assert_load_info(info) - dt = get_delta_tables(pipeline, "delta_table")["delta_table"] - assert dt.version() == 1 - dt_arrow_table = dt.to_pyarrow_table() - assert dt_arrow_table.shape == (2, empty_arrow_table.num_columns) - assert dt_arrow_table.schema.equals( - ensure_delta_compatible_arrow_data(empty_arrow_table).schema + assert get_table_version(pipeline, "a_table", destination_config.table_format) == 1 + expected, actual = get_expected_actual( + pipeline, "a_table", destination_config.table_format, empty_arrow_table ) + assert actual.shape == (2, expected.num_columns) + assert actual.schema.equals(expected.schema) # now run the empty frame again - info = pipeline.run(delta_table(empty_arrow_table)) + info = pipeline.run(a_table(empty_arrow_table)) assert_load_info(info) - # use materialized list - # NOTE: this will create an empty parquet file with a schema takes from dlt schema. - # the original parquet file had a nested (struct) type in `json` field that is now - # in the delta table schema. the empty parquet file lost this information and had - # string type (converted from dlt `json`) - info = pipeline.run([dlt.mark.materialize_table_schema()], table_name="delta_table") - assert_load_info(info) + if destination_config.table_format == "delta": + # use materialized list + # NOTE: this will create an empty parquet file with a schema takes from dlt schema. + # the original parquet file had a nested (struct) type in `json` field that is now + # in the delta table schema. the empty parquet file lost this information and had + # string type (converted from dlt `json`) + info = pipeline.run([dlt.mark.materialize_table_schema()], table_name="a_table") + assert_load_info(info) # test `dlt.mark.materialize_table_schema()` - users_materialize_table_schema.apply_hints(table_format="delta") + users_materialize_table_schema.apply_hints(table_format=destination_config.table_format) info = pipeline.run(users_materialize_table_schema(), loader_file_format="parquet") assert_load_info(info) - dt = get_delta_tables(pipeline, "users")["users"] - assert dt.version() == 0 - dt_arrow_table = dt.to_pyarrow_table() - assert dt_arrow_table.num_rows == 0 - assert "id", "name" == dt_arrow_table.schema.names[:2] + assert get_table_version(pipeline, "users", destination_config.table_format) == 0 + _, actual = get_expected_actual( + pipeline, "users", destination_config.table_format, empty_arrow_table + ) + assert actual.num_rows == 0 + assert "id", "name" == actual.schema.names[:2] @pytest.mark.parametrize( "destination_config", destinations_configs( table_format_filesystem_configs=True, + # job orchestration is same across table formats—no need to test all formats with_table_format="delta", bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_mixed_source( +def test_table_format_mixed_source( destination_config: DestinationTestConfiguration, ) -> None: """Tests file format handling in mixed source. @@ -877,12 +967,13 @@ def s(): "destination_config", destinations_configs( table_format_filesystem_configs=True, + # job orchestration is same across table formats—no need to test all formats with_table_format="delta", bucket_subset=(FILE_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_dynamic_dispatch( +def test_table_format_dynamic_dispatch( destination_config: DestinationTestConfiguration, ) -> None: @dlt.resource(primary_key="id", table_name=lambda i: i["type"], table_format="delta") @@ -905,80 +996,96 @@ def github_events(): "destination_config", destinations_configs( table_format_filesystem_configs=True, - with_table_format="delta", + with_table_format=("delta", "iceberg"), bucket_subset=(FILE_BUCKET, AZ_BUCKET), ), ids=lambda x: x.name, ) -def test_delta_table_get_delta_tables_helper( +def test_table_format_get_tables_helper( destination_config: DestinationTestConfiguration, ) -> None: - """Tests `get_delta_tables` helper function.""" - from dlt.common.libs.deltalake import DeltaTable, get_delta_tables + """Tests `get_delta_tables` / `get_iceberg_tables` helper functions.""" + get_tables: Any + if destination_config.table_format == "delta": + from dlt.common.libs.deltalake import DeltaTable, get_delta_tables - @dlt.resource(table_format="delta") - def foo_delta(): + get_tables = get_delta_tables + get_num_rows = lambda table: table.to_pyarrow_table().num_rows + elif destination_config.table_format == "iceberg": + from dlt.common.libs.pyiceberg import IcebergTable, get_iceberg_tables + + get_tables = get_iceberg_tables + get_num_rows = lambda table: table.scan().to_arrow().num_rows + + @dlt.resource(table_format=destination_config.table_format) + def foo_table_format(): yield [{"foo": 1}, {"foo": 2}] - @dlt.resource(table_format="delta") - def bar_delta(): + @dlt.resource(table_format=destination_config.table_format) + def bar_table_format(): yield [{"bar": 1}] @dlt.resource - def baz_not_delta(): + def baz_not_table_format(): yield [{"baz": 1}] pipeline = destination_config.setup_pipeline("fs_pipe", dev_mode=True) - info = pipeline.run(foo_delta()) + info = pipeline.run(foo_table_format()) assert_load_info(info) - delta_tables = get_delta_tables(pipeline) - assert delta_tables.keys() == {"foo_delta"} - assert isinstance(delta_tables["foo_delta"], DeltaTable) - assert delta_tables["foo_delta"].to_pyarrow_table().num_rows == 2 - - info = pipeline.run([foo_delta(), bar_delta(), baz_not_delta()]) + tables = get_tables(pipeline) + assert tables.keys() == {"foo_table_format"} + if destination_config.table_format == "delta": + assert isinstance(tables["foo_table_format"], DeltaTable) + elif destination_config.table_format == "iceberg": + assert isinstance(tables["foo_table_format"], IcebergTable) + assert get_num_rows(tables["foo_table_format"]) == 2 + + info = pipeline.run([foo_table_format(), bar_table_format(), baz_not_table_format()]) assert_load_info(info) - delta_tables = get_delta_tables(pipeline) - assert delta_tables.keys() == {"foo_delta", "bar_delta"} - assert delta_tables["bar_delta"].to_pyarrow_table().num_rows == 1 - assert get_delta_tables(pipeline, "foo_delta").keys() == {"foo_delta"} - assert get_delta_tables(pipeline, "bar_delta").keys() == {"bar_delta"} - assert get_delta_tables(pipeline, "foo_delta", "bar_delta").keys() == {"foo_delta", "bar_delta"} + tables = get_tables(pipeline) + assert tables.keys() == {"foo_table_format", "bar_table_format"} + assert get_num_rows(tables["bar_table_format"]) == 1 + assert get_tables(pipeline, "foo_table_format").keys() == {"foo_table_format"} + assert get_tables(pipeline, "bar_table_format").keys() == {"bar_table_format"} + assert get_tables(pipeline, "foo_table_format", "bar_table_format").keys() == { + "foo_table_format", + "bar_table_format", + } # test with child table - @dlt.resource(table_format="delta") - def parent_delta(): + @dlt.resource(table_format=destination_config.table_format) + def parent_table_format(): yield [{"foo": 1, "child": [1, 2, 3]}] - info = pipeline.run(parent_delta()) + info = pipeline.run(parent_table_format()) assert_load_info(info) - delta_tables = get_delta_tables(pipeline) - assert "parent_delta__child" in delta_tables.keys() - assert delta_tables["parent_delta__child"].to_pyarrow_table().num_rows == 3 + tables = get_tables(pipeline) + assert "parent_table_format__child" in tables.keys() + assert get_num_rows(tables["parent_table_format__child"]) == 3 # test invalid input with pytest.raises(ValueError): - get_delta_tables(pipeline, "baz_not_delta") + get_tables(pipeline, "baz_not_table_format") with pytest.raises(ValueError): - get_delta_tables(pipeline, "non_existing_table") + get_tables(pipeline, "non_existing_table") # test unknown schema with pytest.raises(FileNotFoundError): - get_delta_tables(pipeline, "non_existing_table", schema_name="aux_2") + get_tables(pipeline, "non_existing_table", schema_name="aux_2") # load to a new schema and under new name aux_schema = dlt.Schema("aux_2") # NOTE: you cannot have a file with name - info = pipeline.run(parent_delta().with_name("aux_delta"), schema=aux_schema) + info = pipeline.run(parent_table_format().with_name("aux_table"), schema=aux_schema) # also state in seprate package assert_load_info(info, expected_load_packages=2) - delta_tables = get_delta_tables(pipeline, schema_name="aux_2") - assert "aux_delta__child" in delta_tables.keys() - get_delta_tables(pipeline, "aux_delta", schema_name="aux_2") + tables = get_tables(pipeline, schema_name="aux_2") + assert "aux_table__child" in tables.keys() + get_tables(pipeline, "aux_table", schema_name="aux_2") with pytest.raises(ValueError): - get_delta_tables(pipeline, "aux_delta") + get_tables(pipeline, "aux_table") @pytest.mark.parametrize( diff --git a/tests/load/sources/sql_database/test_sql_database_source.py b/tests/load/sources/sql_database/test_sql_database_source.py index 00257471e0..2de923fe38 100644 --- a/tests/load/sources/sql_database/test_sql_database_source.py +++ b/tests/load/sources/sql_database/test_sql_database_source.py @@ -1286,10 +1286,7 @@ def assert_no_precision_columns( ) -> None: actual = list(columns.values()) # we always infer and emit nullability - expected = cast( - List[TColumnSchema], - deepcopy(NULL_NO_PRECISION_COLUMNS if nullable else NOT_NULL_NO_PRECISION_COLUMNS), - ) + expected = deepcopy(NULL_NO_PRECISION_COLUMNS if nullable else NOT_NULL_NO_PRECISION_COLUMNS) if backend == "pyarrow": expected = cast( List[TColumnSchema], diff --git a/tests/load/utils.py b/tests/load/utils.py index 5c24b2d1dc..5660202ec3 100644 --- a/tests/load/utils.py +++ b/tests/load/utils.py @@ -26,7 +26,10 @@ from dlt.common.configuration import resolve_configuration from dlt.common.configuration.container import Container from dlt.common.configuration.specs.config_section_context import ConfigSectionContext -from dlt.common.configuration.specs import CredentialsConfiguration +from dlt.common.configuration.specs import ( + CredentialsConfiguration, + GcpOAuthCredentialsWithoutDefaults, +) from dlt.common.destination.reference import ( DestinationClientDwhConfiguration, JobClientBase, @@ -57,6 +60,7 @@ from dlt.pipeline.exceptions import SqlClientNotAvailable from tests.utils import ( ACTIVE_DESTINATIONS, + ACTIVE_TABLE_FORMATS, IMPLEMENTED_DESTINATIONS, SQL_DESTINATIONS, EXCLUDED_DESTINATION_CONFIGURATIONS, @@ -171,7 +175,9 @@ def destination_factory(self, **kwargs) -> Destination[Any, Any]: dest_type = kwargs.pop("destination", self.destination_type) dest_name = kwargs.pop("destination_name", self.destination_name) self.setup() - return Destination.from_reference(dest_type, destination_name=dest_name, **kwargs) + return Destination.from_reference( + dest_type, self.credentials, destination_name=dest_name, **kwargs + ) def raw_capabilities(self) -> DestinationCapabilitiesContext: dest = Destination.from_reference(self.destination_type) @@ -604,7 +610,7 @@ def destinations_configs( DestinationTestConfiguration( destination_type="filesystem", bucket_url=bucket, - extra_info=bucket + "-delta", + extra_info=bucket, table_format="delta", supports_merge=True, file_format="parquet", @@ -619,12 +625,33 @@ def destinations_configs( ), ) ] + if bucket == AZ_BUCKET: + # `pyiceberg` does not support `az` scheme + continue + destination_configs += [ + DestinationTestConfiguration( + destination_type="filesystem", + bucket_url=bucket, + extra_info=bucket, + table_format="iceberg", + supports_merge=False, + file_format="parquet", + destination_name="fsgcpoauth" if bucket == GCS_BUCKET else None, + ) + ] # filter out non active destinations destination_configs = [ conf for conf in destination_configs if conf.destination_type in ACTIVE_DESTINATIONS ] + # filter out non active table formats + destination_configs = [ + conf + for conf in destination_configs + if conf.table_format is None or conf.table_format in ACTIVE_TABLE_FORMATS + ] + # filter out destinations not in subset if subset: destination_configs = [ diff --git a/tests/pipeline/utils.py b/tests/pipeline/utils.py index 0ae734f72e..e72a27c827 100644 --- a/tests/pipeline/utils.py +++ b/tests/pipeline/utils.py @@ -197,10 +197,23 @@ def _load_tables_to_dicts_fs( delta_tables = get_delta_tables(p, *table_names, schema_name=schema_name) + iceberg_table_names = [ + table_name + for table_name in table_names + if get_table_format(client.schema.tables, table_name) == "iceberg" + ] + if len(iceberg_table_names) > 0: + from dlt.common.libs.pyiceberg import get_iceberg_tables + + iceberg_tables = get_iceberg_tables(p, *table_names, schema_name=schema_name) + for table_name in table_names: if table_name in client.schema.data_table_names() and table_name in delta_table_names: dt = delta_tables[table_name] result[table_name] = dt.to_pyarrow_table().to_pylist() + elif table_name in client.schema.data_table_names() and table_name in iceberg_table_names: + it = iceberg_tables[table_name] + result[table_name] = it.scan().to_arrow().to_pylist() else: table_files = client.list_table_files(table_name) for file in table_files: diff --git a/tests/sources/helpers/rest_client/test_client.py b/tests/sources/helpers/rest_client/test_client.py index 36fe009b93..e67ff9c70a 100644 --- a/tests/sources/helpers/rest_client/test_client.py +++ b/tests/sources/helpers/rest_client/test_client.py @@ -401,7 +401,7 @@ def test_paginate_json_body_without_params(self, rest_client) -> None: posts_skip = (DEFAULT_TOTAL_PAGES - 3) * DEFAULT_PAGE_SIZE class JSONBodyPageCursorPaginator(BaseReferencePaginator): - def update_state(self, response, data): + def update_state(self, response, data): # type: ignore[override] self._next_reference = response.json().get("next_page") def update_request(self, request): diff --git a/tests/utils.py b/tests/utils.py index 1aafa4bfe4..82d742ac65 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -32,6 +32,7 @@ from dlt.common.runtime.run_context import DOT_DLT, RunContext from dlt.common.runtime.telemetry import start_telemetry, stop_telemetry from dlt.common.schema import Schema +from dlt.common.schema.typing import TTableFormat from dlt.common.storages import FileStorage from dlt.common.storages.versioned_storage import VersionedStorage from dlt.common.typing import DictStrAny, StrAny, TDataItem @@ -88,6 +89,12 @@ ACTIVE_SQL_DESTINATIONS = SQL_DESTINATIONS.intersection(ACTIVE_DESTINATIONS) ACTIVE_NON_SQL_DESTINATIONS = NON_SQL_DESTINATIONS.intersection(ACTIVE_DESTINATIONS) +# filter out active table formats for current tests +IMPLEMENTED_TABLE_FORMATS = set(get_args(TTableFormat)) +ACTIVE_TABLE_FORMATS = set( + dlt.config.get("ACTIVE_TABLE_FORMATS", list) or IMPLEMENTED_TABLE_FORMATS +) + # sanity checks assert len(ACTIVE_DESTINATIONS) >= 0, "No active destinations selected" From 3a8dfa7a00298be9b45fa3cf01bd2881a846cdbf Mon Sep 17 00:00:00 2001 From: Anton Burnashev Date: Thu, 12 Dec 2024 15:49:53 +0100 Subject: [PATCH 08/23] Fix validation error in for custom auth classes (#2129) --- dlt/common/typing.py | 15 +++++++++++++++ dlt/sources/rest_api/config_setup.py | 8 +++++++- tests/common/test_typing.py | 17 +++++++++++++++++ .../configurations/test_custom_auth_config.py | 17 ++++++++++++++++- .../test_custom_paginator_config.py | 12 +++++++++++- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/dlt/common/typing.py b/dlt/common/typing.py index 8986d753f3..a0322fe01e 100644 --- a/dlt/common/typing.py +++ b/dlt/common/typing.py @@ -484,3 +484,18 @@ def decorator( return func return decorator + + +def add_value_to_literal(literal: Any, value: Any) -> None: + """Extends a Literal at runtime with a new value. + + Args: + literal (Type[Any]): Literal to extend + value (Any): Value to add + + """ + type_args = get_args(literal) + + if value not in type_args: + type_args += (value,) + literal.__args__ = type_args diff --git a/dlt/sources/rest_api/config_setup.py b/dlt/sources/rest_api/config_setup.py index d03a4fd59b..bf62c6c4f7 100644 --- a/dlt/sources/rest_api/config_setup.py +++ b/dlt/sources/rest_api/config_setup.py @@ -20,6 +20,7 @@ from dlt.common.configuration import resolve_configuration from dlt.common.schema.utils import merge_columns from dlt.common.utils import update_dict_nested, exclude_keys +from dlt.common.typing import add_value_to_literal from dlt.common import jsonpath from dlt.extract.incremental import Incremental @@ -64,6 +65,8 @@ ResponseActionDict, Endpoint, EndpointResource, + AuthType, + PaginatorType, ) @@ -103,6 +106,7 @@ def register_paginator( "Your custom paginator has to be a subclass of BasePaginator" ) PAGINATOR_MAP[paginator_name] = paginator_class + add_value_to_literal(PaginatorType, paginator_name) def get_paginator_class(paginator_name: str) -> Type[BasePaginator]: @@ -153,6 +157,8 @@ def register_auth( ) AUTH_MAP[auth_name] = auth_class + add_value_to_literal(AuthType, auth_name) + def get_auth_class(auth_type: str) -> Type[AuthConfigBase]: try: @@ -285,7 +291,7 @@ def build_resource_dependency_graph( resolved_param_map[resource_name] = None break assert isinstance(endpoint_resource["endpoint"], dict) - # connect transformers to resources via resolved params + # find resolved parameters to connect dependent resources resolved_params = _find_resolved_params(endpoint_resource["endpoint"]) # set of resources in resolved params diff --git a/tests/common/test_typing.py b/tests/common/test_typing.py index 2749e3ebb1..e81c3e7fa2 100644 --- a/tests/common/test_typing.py +++ b/tests/common/test_typing.py @@ -43,6 +43,7 @@ is_union_type, is_annotated, is_callable_type, + add_value_to_literal, ) @@ -293,3 +294,19 @@ def test_secret_type() -> None: assert TSecretStrValue("x_str") == "x_str" assert TSecretStrValue({}) == "{}" + + +def test_add_value_to_literal() -> None: + TestLiteral = Literal["red", "blue"] + + add_value_to_literal(TestLiteral, "green") + + assert get_args(TestLiteral) == ("red", "blue", "green") + + add_value_to_literal(TestLiteral, "red") + assert get_args(TestLiteral) == ("red", "blue", "green") + + TestSingleLiteral = Literal["red"] + add_value_to_literal(TestSingleLiteral, "green") + add_value_to_literal(TestSingleLiteral, "blue") + assert get_args(TestSingleLiteral) == ("red", "green", "blue") diff --git a/tests/sources/rest_api/configurations/test_custom_auth_config.py b/tests/sources/rest_api/configurations/test_custom_auth_config.py index 1a5a2e58a3..52cdb95735 100644 --- a/tests/sources/rest_api/configurations/test_custom_auth_config.py +++ b/tests/sources/rest_api/configurations/test_custom_auth_config.py @@ -5,7 +5,7 @@ from dlt.sources import rest_api from dlt.sources.helpers.rest_client.auth import APIKeyAuth, OAuth2ClientCredentials -from dlt.sources.rest_api.typing import ApiKeyAuthConfig, AuthConfig +from dlt.sources.rest_api.typing import ApiKeyAuthConfig, AuthConfig, RESTAPIConfig class CustomOAuth2(OAuth2ClientCredentials): @@ -77,3 +77,18 @@ class NotAuthConfigBase: "not_an_auth_config_base", NotAuthConfigBase # type: ignore ) assert e.match("Invalid auth: NotAuthConfigBase.") + + def test_valid_config_raises_no_error(self, custom_auth_config: AuthConfig) -> None: + rest_api.config_setup.register_auth("custom_oauth_2", CustomOAuth2) + + valid_config: RESTAPIConfig = { + "client": { + "base_url": "https://example.com", + "auth": custom_auth_config, + }, + "resources": ["test"], + } + + rest_api.rest_api_source(valid_config) + + del rest_api.config_setup.AUTH_MAP["custom_oauth_2"] diff --git a/tests/sources/rest_api/configurations/test_custom_paginator_config.py b/tests/sources/rest_api/configurations/test_custom_paginator_config.py index f8ac060218..975ab10176 100644 --- a/tests/sources/rest_api/configurations/test_custom_paginator_config.py +++ b/tests/sources/rest_api/configurations/test_custom_paginator_config.py @@ -4,7 +4,7 @@ from dlt.sources import rest_api from dlt.sources.helpers.rest_client.paginators import JSONLinkPaginator -from dlt.sources.rest_api.typing import PaginatorConfig +from dlt.sources.rest_api.typing import PaginatorConfig, RESTAPIConfig class CustomPaginator(JSONLinkPaginator): @@ -67,3 +67,13 @@ class NotAPaginator: with pytest.raises(ValueError) as e: rest_api.config_setup.register_paginator("not_a_paginator", NotAPaginator) # type: ignore[arg-type] assert e.match("Invalid paginator: NotAPaginator.") + + def test_test_valid_config_raises_no_error(self, custom_paginator_config) -> None: + rest_api.config_setup.register_paginator("custom_paginator", CustomPaginator) + + valid_config: RESTAPIConfig = { + "client": {"base_url": "https://example.com", "paginator": custom_paginator_config}, + "resources": ["test"], + } + + rest_api.rest_api_source(valid_config) From 80ca47417fe694c5537cedb6eb594bcd3ce2491d Mon Sep 17 00:00:00 2001 From: HulmaNaseer <42720638+HulmaNaseer@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:47:40 +0100 Subject: [PATCH 09/23] explicitly adding docs for destination item size control (#2118) * explicitly adding docs for destination item size control * alena's feedback * revised for explicit note * Update docs/website/docs/reference/performance.md --------- Co-authored-by: hulmanaseer00 <163604758+hulmanaseer00@users.noreply.github.com> Co-authored-by: Alena Astrakhantseva --- docs/website/docs/reference/performance.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/website/docs/reference/performance.md b/docs/website/docs/reference/performance.md index ab171ac069..1e58080200 100644 --- a/docs/website/docs/reference/performance.md +++ b/docs/website/docs/reference/performance.md @@ -48,9 +48,7 @@ Some file formats (e.g., Parquet) do not support schema changes when writing a s Below, we set files to rotate after 100,000 items written or when the filesize exceeds 1MiB. - - - + ### Disabling and enabling file compression Several [text file formats](../dlt-ecosystem/file-formats/) have `gzip` compression enabled by default. If you wish that your load packages have uncompressed files (e.g., to debug the content easily), change `data_writer.disable_compression` in config.toml. The entry below will disable the compression of the files processed in the `normalize` stage. @@ -148,7 +146,10 @@ As before, **if you have just a single table with millions of records, you shoul -Since the normalize stage uses a process pool to create load packages concurrently, adjusting the `file_max_items` and `file_max_bytes` settings can significantly impact load behavior. By setting a lower value for `file_max_items`, you reduce the size of each data chunk sent to the destination database, which can be particularly useful for managing memory constraints on the database server. Without explicit configuration of `file_max_items`, `dlt` writes all data rows into one large intermediary file, attempting to insert all data from this single file. Configuring `file_max_items` ensures data is inserted in manageable chunks, enhancing performance and preventing potential memory issues. +The **normalize** stage in `dlt` uses a process pool to create load packages concurrently, and the settings for `file_max_items` and `file_max_bytes` play a crucial role in determining the size of data chunks. Lower values for these settings reduce the size of each chunk sent to the destination database, which is particularly helpful for managing memory constraints on the database server. By default, `dlt` writes all data rows into one large intermediary file, attempting to load all data at once. Configuring these settings enables file rotation, splitting the data into smaller, more manageable chunks. This not only improves performance but also minimizes memory-related issues when working with large tables containing millions of records. + +#### Controlling destination items size +The intermediary files generated during the **normalize** stage are also used in the **load** stage. Therefore, adjusting `file_max_items` and `file_max_bytes` in the **normalize** stage directly impacts the size and number of data chunks sent to the destination, influencing loading behavior and performance. ### Parallel pipeline config example The example below simulates the loading of a large database table with 1,000,000 records. The **config.toml** below sets the parallelization as follows: From beb8465c5be5eb55dfd532c405a2e22bf2027bb6 Mon Sep 17 00:00:00 2001 From: Anton Burnashev Date: Fri, 13 Dec 2024 14:12:57 +0100 Subject: [PATCH 10/23] Update primary key for pokemon resource from id to name in REST API tutorial (#2147) --- docs/website/docs/tutorial/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/website/docs/tutorial/rest-api.md b/docs/website/docs/tutorial/rest-api.md index 56051e80de..70c7f7e964 100644 --- a/docs/website/docs/tutorial/rest-api.md +++ b/docs/website/docs/tutorial/rest-api.md @@ -246,7 +246,7 @@ pokemon_source = rest_api_source( # the primary key and write disposition { "name": "pokemon", - "primary_key": "id", + "primary_key": "name", "write_disposition": "merge", }, # The `berry` and `location` resources will use the default @@ -257,7 +257,7 @@ pokemon_source = rest_api_source( ) ``` -Run the pipeline with `python rest_api_pipeline.py`, the data for the `pokemon` resource will be merged with the existing data in the destination table based on the `id` field. +Run the pipeline with `python rest_api_pipeline.py`, the data for the `pokemon` resource will be merged with the existing data in the destination table based on the `name` field. ## Loading data incrementally From 39c0a01f58058f0f6df0ed3d4e16079ec84196c9 Mon Sep 17 00:00:00 2001 From: Julian Alves <28436330+donotpush@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:26:05 +0100 Subject: [PATCH 11/23] add databricks oauth authentication (#2138) * add databricks oauth authentication * improve auth databricks test * force token-based auth for azure external location tests --- .../impl/databricks/configuration.py | 12 ++++ .../impl/databricks/sql_client.py | 20 ++++++- .../dlt-ecosystem/destinations/databricks.md | 25 ++++++++- poetry.lock | 26 ++++++++- pyproject.toml | 3 +- .../test_databricks_configuration.py | 10 ++++ .../load/pipeline/test_databricks_pipeline.py | 56 +++++++++++++++++++ 7 files changed, 145 insertions(+), 7 deletions(-) diff --git a/dlt/destinations/impl/databricks/configuration.py b/dlt/destinations/impl/databricks/configuration.py index c95b6eba4c..21338bd310 100644 --- a/dlt/destinations/impl/databricks/configuration.py +++ b/dlt/destinations/impl/databricks/configuration.py @@ -4,6 +4,7 @@ from dlt.common.typing import TSecretStrValue from dlt.common.configuration.specs.base_configuration import CredentialsConfiguration, configspec from dlt.common.destination.reference import DestinationClientDwhWithStagingConfiguration +from dlt.common.configuration.exceptions import ConfigurationValueError DATABRICKS_APPLICATION_ID = "dltHub_dlt" @@ -15,6 +16,8 @@ class DatabricksCredentials(CredentialsConfiguration): server_hostname: str = None http_path: str = None access_token: Optional[TSecretStrValue] = None + client_id: Optional[TSecretStrValue] = None + client_secret: Optional[TSecretStrValue] = None http_headers: Optional[Dict[str, str]] = None session_configuration: Optional[Dict[str, Any]] = None """Dict of session parameters that will be passed to `databricks.sql.connect`""" @@ -27,9 +30,18 @@ class DatabricksCredentials(CredentialsConfiguration): "server_hostname", "http_path", "catalog", + "client_id", + "client_secret", "access_token", ] + def on_resolved(self) -> None: + if not ((self.client_id and self.client_secret) or self.access_token): + raise ConfigurationValueError( + "No valid authentication method detected. Provide either 'client_id' and" + " 'client_secret' for OAuth, or 'access_token' for token-based authentication." + ) + def to_connector_params(self) -> Dict[str, Any]: conn_params = dict( catalog=self.catalog, diff --git a/dlt/destinations/impl/databricks/sql_client.py b/dlt/destinations/impl/databricks/sql_client.py index 8bff4e0d73..16e1e73d93 100644 --- a/dlt/destinations/impl/databricks/sql_client.py +++ b/dlt/destinations/impl/databricks/sql_client.py @@ -11,10 +11,12 @@ Tuple, Union, Dict, + cast, + Callable, ) - -from databricks import sql as databricks_lib +from databricks.sdk.core import Config, oauth_service_principal +from databricks import sql as databricks_lib # type: ignore[attr-defined] from databricks.sql.client import ( Connection as DatabricksSqlConnection, Cursor as DatabricksSqlCursor, @@ -73,8 +75,22 @@ def __init__( self._conn: DatabricksSqlConnection = None self.credentials = credentials + def _get_oauth_credentials(self) -> Optional[Callable[[], Dict[str, str]]]: + config = Config( + host=f"https://{self.credentials.server_hostname}", + client_id=self.credentials.client_id, + client_secret=self.credentials.client_secret, + ) + return cast(Callable[[], Dict[str, str]], oauth_service_principal(config)) + def open_connection(self) -> DatabricksSqlConnection: conn_params = self.credentials.to_connector_params() + + if self.credentials.client_id and self.credentials.client_secret: + conn_params["credentials_provider"] = self._get_oauth_credentials + else: + conn_params["access_token"] = self.credentials.access_token + self._conn = databricks_lib.connect( **conn_params, schema=self.dataset_name, use_inline_params="silent" ) diff --git a/docs/website/docs/dlt-ecosystem/destinations/databricks.md b/docs/website/docs/dlt-ecosystem/destinations/databricks.md index 513a3b792f..dd046ce28a 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/databricks.md +++ b/docs/website/docs/dlt-ecosystem/destinations/databricks.md @@ -90,6 +90,29 @@ If you already have your Databricks workspace set up, you can skip to the [Loade Click your email in the top right corner and go to "User Settings". Go to "Developer" -> "Access Tokens". Generate a new token and save it. You will use it in your `dlt` configuration. +## OAuth M2M (Machine-to-Machine) Authentication + +You can authenticate to Databricks using a service principal via OAuth M2M. This method allows for secure, programmatic access to Databricks resources without requiring a user-managed personal access token. + +### Create a Service Principal in Databricks +Follow the instructions in the Databricks documentation to create a service principal and retrieve the client_id and client_secret: + +[Authenticate access to Databricks using OAuth M2M](https://docs.databricks.com/en/dev-tools/auth/oauth-m2m.html) + +Once you have the service principal credentials, update your secrets.toml as shown bellow. + +### Configuration + +Add the following fields to your `.dlt/secrets.toml` file: +```toml +[destination.databricks.credentials] +server_hostname = "MY_DATABRICKS.azuredatabricks.net" +http_path = "/sql/1.0/warehouses/12345" +catalog = "my_catalog" +client_id = "XXX" +client_secret = "XXX" +``` + ## Loader setup guide **1. Initialize a project with a pipeline that loads to Databricks by running** @@ -118,7 +141,7 @@ Example: [destination.databricks.credentials] server_hostname = "MY_DATABRICKS.azuredatabricks.net" http_path = "/sql/1.0/warehouses/12345" -access_token = "MY_ACCESS_TOKEN" +access_token = "MY_ACCESS_TOKEN" # Replace for client_id and client_secret when using OAuth catalog = "my_catalog" ``` diff --git a/poetry.lock b/poetry.lock index 83090360b0..82d9bf90f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "about-time" @@ -2208,6 +2208,26 @@ nr-date = ">=2.0.0,<3.0.0" typeapi = ">=2.0.1,<3.0.0" typing-extensions = ">=3.10.0" +[[package]] +name = "databricks-sdk" +version = "0.39.0" +description = "Databricks SDK for Python (Beta)" +optional = true +python-versions = ">=3.7" +files = [ + {file = "databricks_sdk-0.39.0-py3-none-any.whl", hash = "sha256:915fbf12b249264f74ddae2ca739530e3c4a9c5a454617ac403115d6466c2f99"}, + {file = "databricks_sdk-0.39.0.tar.gz", hash = "sha256:2e04edbb9e050f4362da804fb5dad07637c5adecfcffb4d0ca8abb5aefa36d06"}, +] + +[package.dependencies] +google-auth = ">=2.0,<3.0" +requests = ">=2.28.1,<3" + +[package.extras] +dev = ["autoflake", "databricks-connect", "httpx", "ipython", "ipywidgets", "isort", "langchain-openai", "openai", "pycodestyle", "pyfakefs", "pytest", "pytest-cov", "pytest-mock", "pytest-rerunfailures", "pytest-xdist", "requests-mock", "wheel", "yapf"] +notebook = ["ipython (>=8,<9)", "ipywidgets (>=8,<9)"] +openai = ["httpx", "langchain-openai", "openai"] + [[package]] name = "databricks-sql-connector" version = "2.9.6" @@ -10680,7 +10700,7 @@ az = ["adlfs"] bigquery = ["db-dtypes", "gcsfs", "google-cloud-bigquery", "grpcio", "pyarrow"] cli = ["cron-descriptor", "pipdeptree"] clickhouse = ["adlfs", "clickhouse-connect", "clickhouse-driver", "gcsfs", "pyarrow", "s3fs"] -databricks = ["databricks-sql-connector"] +databricks = ["databricks-sdk", "databricks-sql-connector"] deltalake = ["deltalake", "pyarrow"] dremio = ["pyarrow"] duckdb = ["duckdb"] @@ -10707,4 +10727,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.13" -content-hash = "84e8b8eccd9b8ee104a2dc08f5b83987aeb06540d61330390ce849cc1ad6acb4" +content-hash = "5513aca05ae04d7941f2a890d0fefa86a08371508a2d319c1e558c29ff8a45f3" diff --git a/pyproject.toml b/pyproject.toml index bfa830cd06..d12073601d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ db-dtypes = { version = ">=1.2.0", optional = true } # pyiceberg = { version = ">=0.7.1", optional = true, extras = ["sql-sqlite"] } # we will rely on manual installation of `sqlalchemy>=2.0.18` instead pyiceberg = { version = ">=0.8.1", python = ">=3.9", optional = true } +databricks-sdk = {version = ">=0.38.0", optional = true} [tool.poetry.extras] gcp = ["grpcio", "google-cloud-bigquery", "db-dtypes", "gcsfs"] @@ -117,7 +118,7 @@ weaviate = ["weaviate-client"] mssql = ["pyodbc"] synapse = ["pyodbc", "adlfs", "pyarrow"] qdrant = ["qdrant-client"] -databricks = ["databricks-sql-connector"] +databricks = ["databricks-sql-connector", "databricks-sdk"] clickhouse = ["clickhouse-driver", "clickhouse-connect", "s3fs", "gcsfs", "adlfs", "pyarrow"] dremio = ["pyarrow"] lancedb = ["lancedb", "pyarrow", "tantivy"] diff --git a/tests/load/databricks/test_databricks_configuration.py b/tests/load/databricks/test_databricks_configuration.py index e27da4db2a..8b3beed2b3 100644 --- a/tests/load/databricks/test_databricks_configuration.py +++ b/tests/load/databricks/test_databricks_configuration.py @@ -4,6 +4,7 @@ pytest.importorskip("databricks") from dlt.common.exceptions import TerminalValueError +from dlt.common.configuration.exceptions import ConfigurationValueError from dlt.destinations.impl.databricks.databricks import DatabricksLoadJob from dlt.common.configuration import resolve_configuration @@ -86,3 +87,12 @@ def test_databricks_abfss_converter() -> None: abfss_url == "abfss://dlt-ci-test-bucket@my_account.dfs.core.windows.net/path/to/file.parquet" ) + + +def test_databricks_auth_invalid() -> None: + with pytest.raises(ConfigurationValueError, match="No valid authentication method detected.*"): + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_ID"] = "" + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_SECRET"] = "" + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__ACCESS_TOKEN"] = "" + bricks = databricks() + bricks.configuration(None, accept_partial=True) diff --git a/tests/load/pipeline/test_databricks_pipeline.py b/tests/load/pipeline/test_databricks_pipeline.py index e802cde693..078dce3a7f 100644 --- a/tests/load/pipeline/test_databricks_pipeline.py +++ b/tests/load/pipeline/test_databricks_pipeline.py @@ -2,6 +2,7 @@ import os from dlt.common.utils import uniq_id +from dlt.destinations import databricks from tests.load.utils import ( GCS_BUCKET, DestinationTestConfiguration, @@ -23,6 +24,10 @@ ids=lambda x: x.name, ) def test_databricks_external_location(destination_config: DestinationTestConfiguration) -> None: + # force token-based authentication + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_ID"] = "" + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_SECRET"] = "" + # do not interfere with state os.environ["RESTORE_FROM_DESTINATION"] = "False" # let the package complete even with failed jobs @@ -145,3 +150,54 @@ def test_databricks_gcs_external_location(destination_config: DestinationTestCon assert ( "credential_x" in pipeline.list_failed_jobs_in_package(info.loads_ids[0])[0].failed_message ) + + +@pytest.mark.parametrize( + "destination_config", + destinations_configs(default_sql_configs=True, subset=("databricks",)), + ids=lambda x: x.name, +) +def test_databricks_auth_oauth(destination_config: DestinationTestConfiguration) -> None: + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__ACCESS_TOKEN"] = "" + bricks = databricks() + config = bricks.configuration(None, accept_partial=True) + assert config.credentials.client_id and config.credentials.client_secret + assert not config.credentials.access_token + + dataset_name = "test_databricks_oauth" + uniq_id() + pipeline = destination_config.setup_pipeline( + "test_databricks_oauth", dataset_name=dataset_name, destination=bricks + ) + + info = pipeline.run([1, 2, 3], table_name="digits", **destination_config.run_kwargs) + assert info.has_failed_jobs is False + + with pipeline.sql_client() as client: + rows = client.execute_sql(f"select * from {dataset_name}.digits") + assert len(rows) == 3 + + +@pytest.mark.parametrize( + "destination_config", + destinations_configs(default_sql_configs=True, subset=("databricks",)), + ids=lambda x: x.name, +) +def test_databricks_auth_token(destination_config: DestinationTestConfiguration) -> None: + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_ID"] = "" + os.environ["DESTINATION__DATABRICKS__CREDENTIALS__CLIENT_SECRET"] = "" + bricks = databricks() + config = bricks.configuration(None, accept_partial=True) + assert config.credentials.access_token + assert not (config.credentials.client_secret and config.credentials.client_id) + + dataset_name = "test_databricks_token" + uniq_id() + pipeline = destination_config.setup_pipeline( + "test_databricks_token", dataset_name=dataset_name, destination=bricks + ) + + info = pipeline.run([1, 2, 3], table_name="digits", **destination_config.run_kwargs) + assert info.has_failed_jobs is False + + with pipeline.sql_client() as client: + rows = client.execute_sql(f"select * from {dataset_name}.digits") + assert len(rows) == 3 From fd5ba0bcefc098d6b245bbf68e1bbb4d0ee9fb77 Mon Sep 17 00:00:00 2001 From: Jorrit Sandbrink <47451109+jorritsandbrink@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:17:28 +0400 Subject: [PATCH 12/23] make duckdb handle Iceberg table with nested types (#2141) * make duckdb handle iceberg table with nested types * replace duckdb views for iceberg tables * remove unnecessary context closing and opening * replace duckdb views for abfss protocol * restore original destination for write path * use dev_mode to work around leftover data from previous tests leftover data caused by https://github.com/dlt-hub/dlt/issues/2148 --- .../impl/filesystem/sql_client.py | 25 +++++++---- tests/load/filesystem/test_sql_client.py | 42 +++++++++++++++---- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/dlt/destinations/impl/filesystem/sql_client.py b/dlt/destinations/impl/filesystem/sql_client.py index d39f4c3431..e6b84343bb 100644 --- a/dlt/destinations/impl/filesystem/sql_client.py +++ b/dlt/destinations/impl/filesystem/sql_client.py @@ -214,14 +214,17 @@ def create_views_for_tables(self, tables: Dict[str, str]) -> None: # unknown views will not be created continue - # only create view if it does not exist in the current schema yet - existing_tables = [tname[0] for tname in self._conn.execute("SHOW TABLES").fetchall()] - if view_name in existing_tables: - continue - # NOTE: if this is staging configuration then `prepare_load_table` will remove some info # from table schema, if we ever extend this to handle staging destination, this needs to change schema_table = self.fs_client.prepare_load_table(table_name) + table_format = schema_table.get("table_format") + + # skip if view already exists and does not need to be replaced each time + existing_tables = [tname[0] for tname in self._conn.execute("SHOW TABLES").fetchall()] + needs_replace = table_format == "iceberg" or self.fs_client.config.protocol == "abfss" + if view_name in existing_tables and not needs_replace: + continue + # discover file type folder = self.fs_client.get_table_dir(table_name) files = self.fs_client.list_table_files(table_name) @@ -258,15 +261,17 @@ def create_views_for_tables(self, tables: Dict[str, str]) -> None: # create from statement from_statement = "" - if schema_table.get("table_format") == "delta": + if table_format == "delta": from_statement = f"delta_scan('{resolved_folder}')" - elif schema_table.get("table_format") == "iceberg": + elif table_format == "iceberg": from dlt.common.libs.pyiceberg import _get_last_metadata_file self._setup_iceberg(self._conn) metadata_path = f"{resolved_folder}/metadata" last_metadata_file = _get_last_metadata_file(metadata_path, self.fs_client) - from_statement = f"iceberg_scan('{last_metadata_file}')" + # skip schema inference to make nested data types work + # https://github.com/duckdb/duckdb_iceberg/issues/47 + from_statement = f"iceberg_scan('{last_metadata_file}', skip_schema_inference=True)" elif first_file_type == "parquet": from_statement = f"read_parquet([{resolved_files_string}])" elif first_file_type == "jsonl": @@ -281,7 +286,9 @@ def create_views_for_tables(self, tables: Dict[str, str]) -> None: # create table view_name = self.make_qualified_table_name(view_name) - create_table_sql_base = f"CREATE VIEW {view_name} AS SELECT * FROM {from_statement}" + create_table_sql_base = ( + f"CREATE OR REPLACE VIEW {view_name} AS SELECT * FROM {from_statement}" + ) self._conn.execute(create_table_sql_base) @contextmanager diff --git a/tests/load/filesystem/test_sql_client.py b/tests/load/filesystem/test_sql_client.py index a73b0f7e31..4f537d129c 100644 --- a/tests/load/filesystem/test_sql_client.py +++ b/tests/load/filesystem/test_sql_client.py @@ -22,6 +22,7 @@ ) from dlt.destinations import filesystem from tests.utils import TEST_STORAGE_ROOT +from tests.cases import arrow_table_all_data_types from dlt.destinations.exceptions import DatabaseUndefinedRelation @@ -81,12 +82,17 @@ def double_items(): for i in range(total_records) ] - return [items, double_items] + @dlt.resource(table_format=table_format) + def arrow_all_types(): + yield arrow_table_all_data_types("arrow-table", num_rows=total_records)[0] + + return [items, double_items, arrow_all_types] # run source pipeline.run(source(), loader_file_format=destination_config.file_format) if alternate_access_pipeline: + orig_dest = pipeline.destination pipeline.destination = alternate_access_pipeline.destination import duckdb @@ -96,8 +102,11 @@ def double_items(): DuckDbCredentials, ) - # check we can create new tables from the views with pipeline.sql_client() as c: + # check if all data types are handled properly + c.execute_sql("SELECT * FROM arrow_all_types;") + + # check we can create new tables from the views c.execute_sql( "CREATE TABLE items_joined AS (SELECT i.id, di.double_id FROM items as i JOIN" " double_items as di ON (i.id = di.id));" @@ -109,16 +118,14 @@ def double_items(): assert list(joined_table[5]) == [5, 10] assert list(joined_table[10]) == [10, 20] - # inserting values into a view should fail gracefully - with pipeline.sql_client() as c: + # inserting values into a view should fail gracefully try: c.execute_sql("INSERT INTO double_items VALUES (1, 2)") except Exception as exc: assert "double_items is not an table" in str(exc) - # check that no automated views are created for a schema different than - # the known one - with pipeline.sql_client() as c: + # check that no automated views are created for a schema different than + # the known one c.execute_sql("CREATE SCHEMA other_schema;") with pytest.raises(DatabaseUndefinedRelation): with c.execute_query("SELECT * FROM other_schema.items ORDER BY id ASC;") as cursor: @@ -172,6 +179,24 @@ def _fs_sql_client_for_external_db( # views exist assert len(external_db.sql("SELECT * FROM second.referenced_items").fetchall()) == total_records assert len(external_db.sql("SELECT * FROM first.items").fetchall()) == 3 + + # test if view reflects source table accurately after it has changed + # conretely, this tests if an existing view is replaced with formats that need it, such as + # `iceberg` table format + with fs_sql_client as sql_client: + sql_client.create_views_for_tables({"arrow_all_types": "arrow_all_types"}) + assert external_db.sql("FROM second.arrow_all_types;").arrow().num_rows == total_records + if alternate_access_pipeline: + # switch back for the write path + pipeline.destination = orig_dest + pipeline.run( # run pipeline again to add rows to source table + source().with_resources("arrow_all_types"), + loader_file_format=destination_config.file_format, + ) + with fs_sql_client as sql_client: + sql_client.create_views_for_tables({"arrow_all_types": "arrow_all_types"}) + assert external_db.sql("FROM second.arrow_all_types;").arrow().num_rows == (2 * total_records) + external_db.close() # in case we are not connecting to a bucket that needs secrets, views should still be here after connection reopen @@ -298,6 +323,7 @@ def test_table_formats( pipeline = destination_config.setup_pipeline( "read_pipeline", dataset_name="read_test", + dev_mode=True, ) # in case of gcs we use the s3 compat layer for reading @@ -310,7 +336,7 @@ def test_table_formats( GCS_BUCKET.replace("gs://", "s3://"), destination_name="filesystem_s3_gcs_comp" ) access_pipeline = destination_config.setup_pipeline( - "read_pipeline", dataset_name="read_test", destination=gcp_bucket + "read_pipeline", dataset_name="read_test", dev_mode=True, destination=gcp_bucket ) _run_dataset_checks( From 95d606396157a968e35bf1170b1e43989dfc97f5 Mon Sep 17 00:00:00 2001 From: rudolfix Date: Sun, 15 Dec 2024 16:49:14 +0100 Subject: [PATCH 13/23] Fix/refresh standalone resources (#2140) * drops tables from schema and relational * documents custom sections for sql_database and source rename * clones schema without data tables when resources without source are extacted, adds tests * skips airflow tests if not installed * adds doc on setting up FUSE on bucket * adds doc on setting up FUSE on bucket * adds row key propagation for table when its nested table require it * fixes tests --- Makefile | 3 +- dlt/common/normalizers/json/__init__.py | 4 + dlt/common/normalizers/json/relational.py | 54 ++++++++++--- dlt/common/schema/schema.py | 2 + dlt/extract/extract.py | 7 +- docs/tools/check_embedded_snippets.py | 9 ++- .../verified-sources/sql_database/advanced.md | 21 +++++ docs/website/docs/general-usage/source.md | 17 +++- docs/website/docs/reference/performance.md | 26 ++++++ .../normalizers/test_json_relational.py | 33 ++++++++ .../airflow_tests/test_airflow_provider.py | 4 + .../airflow_tests/test_airflow_wrapper.py | 2 + .../test_join_airflow_scheduler.py | 3 + tests/helpers/airflow_tests/utils.py | 8 +- tests/load/pipeline/test_drop.py | 55 ++++++++++--- tests/load/pipeline/test_refresh_modes.py | 79 ++++++++++++++----- tests/pipeline/test_pipeline.py | 24 ++++++ 17 files changed, 304 insertions(+), 47 deletions(-) diff --git a/Makefile b/Makefile index 0ca8a2e0c3..975a8a42da 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,6 @@ format: lint-snippets: cd docs/tools && poetry run python check_embedded_snippets.py full - lint-and-test-snippets: lint-snippets poetry run mypy --config-file mypy.ini docs/website docs/tools --exclude docs/tools/lint_setup --exclude docs/website/docs_processed poetry run flake8 --max-line-length=200 docs/website docs/tools --exclude docs/website/.dlt-repo @@ -82,7 +81,7 @@ lint-security: poetry run bandit -r dlt/ -n 3 -l test: - (set -a && . tests/.env && poetry run pytest tests) + poetry run pytest tests test-load-local: DESTINATION__POSTGRES__CREDENTIALS=postgresql://loader:loader@localhost:5432/dlt_data DESTINATION__DUCKDB__CREDENTIALS=duckdb:///_storage/test_quack.duckdb poetry run pytest tests -k '(postgres or duckdb)' diff --git a/dlt/common/normalizers/json/__init__.py b/dlt/common/normalizers/json/__init__.py index 725f6a8355..ae5e06fe2e 100644 --- a/dlt/common/normalizers/json/__init__.py +++ b/dlt/common/normalizers/json/__init__.py @@ -36,6 +36,10 @@ def extend_schema(self) -> None: def extend_table(self, table_name: str) -> None: pass + @abc.abstractmethod + def remove_table(self, table_name: str) -> None: + pass + @classmethod @abc.abstractmethod def update_normalizer_config(cls, schema: Schema, config: TNormalizerConfig) -> None: diff --git a/dlt/common/normalizers/json/relational.py b/dlt/common/normalizers/json/relational.py index e365017125..36845b2e14 100644 --- a/dlt/common/normalizers/json/relational.py +++ b/dlt/common/normalizers/json/relational.py @@ -1,4 +1,16 @@ -from typing import Dict, List, Mapping, Optional, Sequence, Tuple, cast, TypedDict, Any +from typing import ( + ClassVar, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + cast, + TypedDict, + Any, +) from dlt.common.normalizers.exceptions import InvalidJsonNormalizer from dlt.common.normalizers.typing import TJSONNormalizer @@ -14,6 +26,9 @@ from dlt.common.schema.utils import ( column_name_validator, is_nested_table, + get_nested_tables, + has_column_with_prop, + get_first_column_name_with_prop, ) from dlt.common.utils import update_dict_nested from dlt.common.normalizers.json import ( @@ -48,6 +63,7 @@ class DataItemNormalizer(DataItemNormalizerBase[RelationalNormalizerConfig]): # other constants EMPTY_KEY_IDENTIFIER = "_empty" # replace empty keys with this + RELATIONAL_CONFIG_TYPE: ClassVar[Type[RelationalNormalizerConfig]] = RelationalNormalizerConfig normalizer_config: RelationalNormalizerConfig propagation_config: RelationalNormalizerConfigPropagation @@ -310,20 +326,38 @@ def extend_table(self, table_name: str) -> None: Table name should be normalized. """ table = self.schema.tables.get(table_name) - if not is_nested_table(table) and table.get("write_disposition") == "merge": - DataItemNormalizer.update_normalizer_config( + # add root key prop when merge disposition is used or any of nested tables needs row_key + if not is_nested_table(table) and ( + table.get("write_disposition") == "merge" + or any( + has_column_with_prop(t, "root_key", include_incomplete=True) + for t in get_nested_tables(self.schema.tables, table_name) + ) + ): + # get row id column from table, assume that we propagate it into c_dlt_root_id always + c_dlt_id = get_first_column_name_with_prop(table, "row_key", include_incomplete=True) + self.update_normalizer_config( self.schema, { "propagation": { "tables": { table_name: { - TColumnName(self.c_dlt_id): TColumnName(self.c_dlt_root_id) + TColumnName(c_dlt_id or self.c_dlt_id): TColumnName( + self.c_dlt_root_id + ) } } } }, ) + def remove_table(self, table_name: str) -> None: + """Called by the Schema when table is removed from it.""" + config = self.get_normalizer_config(self.schema) + if propagation := config.get("propagation"): + if tables := propagation.get("tables"): + tables.pop(table_name, None) + def normalize_data_item( self, item: TDataItem, load_id: str, table_name: str ) -> TNormalizedRowIterator: @@ -352,8 +386,8 @@ def normalize_data_item( def ensure_this_normalizer(cls, norm_config: TJSONNormalizer) -> None: # make sure schema has right normalizer present_normalizer = norm_config["module"] - if present_normalizer != __name__: - raise InvalidJsonNormalizer(__name__, present_normalizer) + if present_normalizer != cls.__module__: + raise InvalidJsonNormalizer(cls.__module__, present_normalizer) @classmethod def update_normalizer_config(cls, schema: Schema, config: RelationalNormalizerConfig) -> None: @@ -371,8 +405,10 @@ def get_normalizer_config(cls, schema: Schema) -> RelationalNormalizerConfig: cls.ensure_this_normalizer(norm_config) return cast(RelationalNormalizerConfig, norm_config.get("config", {})) - @staticmethod - def _validate_normalizer_config(schema: Schema, config: RelationalNormalizerConfig) -> None: + @classmethod + def _validate_normalizer_config( + cls, schema: Schema, config: RelationalNormalizerConfig + ) -> None: """Normalizes all known column identifiers according to the schema and then validates the configuration""" def _normalize_prop( @@ -397,7 +433,7 @@ def _normalize_prop( ) validate_dict( - RelationalNormalizerConfig, + cls.RELATIONAL_CONFIG_TYPE, config, "./normalizers/json/config", validator_f=column_name_validator(schema.naming), diff --git a/dlt/common/schema/schema.py b/dlt/common/schema/schema.py index 276bbe9c09..f2d75638fe 100644 --- a/dlt/common/schema/schema.py +++ b/dlt/common/schema/schema.py @@ -451,10 +451,12 @@ def drop_tables( ) -> List[TTableSchema]: """Drops tables from the schema and returns the dropped tables""" result = [] + # TODO: make sure all nested tables to table_names are also dropped for table_name in table_names: table = self.get_table(table_name) if table and (not seen_data_only or utils.has_table_seen_data(table)): result.append(self._schema_tables.pop(table_name)) + self.data_item_normalizer.remove_table(table_name) return result def filter_row_with_hint( diff --git a/dlt/extract/extract.py b/dlt/extract/extract.py index 25c3a0dbae..c062a74920 100644 --- a/dlt/extract/extract.py +++ b/dlt/extract/extract.py @@ -87,7 +87,12 @@ def choose_schema() -> Schema: schema_ = schema # take pipeline schema to make newest version visible to the resources elif pipeline.default_schema_name: - schema_ = pipeline.schemas[pipeline.default_schema_name].clone() + # clones with name which will drop previous hashes + schema_ = pipeline.schemas[pipeline.default_schema_name].clone( + with_name=pipeline.default_schema_name + ) + # delete data tables + schema_.drop_tables(schema_.data_table_names(include_incomplete=True)) else: schema_ = pipeline._make_schema_with_default_name() return schema_ diff --git a/docs/tools/check_embedded_snippets.py b/docs/tools/check_embedded_snippets.py index e8399fce6e..b917cafee1 100644 --- a/docs/tools/check_embedded_snippets.py +++ b/docs/tools/check_embedded_snippets.py @@ -21,7 +21,7 @@ SNIPPET_MARKER = "```" -ALLOWED_LANGUAGES = ["py", "toml", "json", "yaml", "text", "sh", "bat", "sql"] +ALLOWED_LANGUAGES = ["py", "toml", "json", "yaml", "text", "sh", "bat", "sql", "hcl"] LINT_TEMPLATE = "./lint_setup/template.py" LINT_FILE = "./lint_setup/lint_me.py" @@ -163,8 +163,11 @@ def parse_snippets(snippets: List[Snippet], verbose: bool) -> None: json.loads(snippet.code) elif snippet.language == "yaml": yaml.safe_load(snippet.code) - # ignore text and sh scripts - elif snippet.language in ["text", "sh", "bat", "sql"]: + elif snippet.language == "hcl": + # TODO: implement hcl parsers + pass + # ignore all other scripts + elif snippet.language in ALLOWED_LANGUAGES: pass else: raise ValueError(f"Unknown language {snippet.language}") diff --git a/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md b/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md index c532f6d357..954c1fb493 100644 --- a/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md +++ b/docs/website/docs/dlt-ecosystem/verified-sources/sql_database/advanced.md @@ -256,3 +256,24 @@ SOURCES__SQL_DATABASE__CHUNK_SIZE=1000 SOURCES__SQL_DATABASE__CHAT_MESSAGE__INCREMENTAL__CURSOR_PATH=updated_at ``` +### Configure many sources side by side with custom sections +`dlt` allows you to rename any source to place the source configuration into custom section or to have many instances +of the source created side by side. For example: +```py +from dlt.sources.sql_database import sql_database + +my_db = sql_database.with_args(name="my_db", section="my_db")(table_names=["chat_message"]) +print(my_db.name) +``` +Here we create a renamed version of the `sql_database` and then instantiate it. Such source will read +credentials from: +```toml +[sources.my_db] +credentials="mssql+pyodbc://loader.database.windows.net/dlt_data?trusted_connection=yes&driver=ODBC+Driver+17+for+SQL+Server" +schema="data" +backend="pandas" +chunk_size=1000 + +[sources.my_db.chat_message.incremental] +cursor_path="updated_at" +``` diff --git a/docs/website/docs/general-usage/source.md b/docs/website/docs/general-usage/source.md index a5f1f04dee..87c07a3e44 100644 --- a/docs/website/docs/general-usage/source.md +++ b/docs/website/docs/general-usage/source.md @@ -52,7 +52,6 @@ Do not extract data in the source function. Leave that task to your resources if If this is impractical (for example, you want to reflect a database to create resources for tables), make sure you do not call the source function too often. [See this note if you plan to deploy on Airflow](../walkthroughs/deploy-a-pipeline/deploy-with-airflow-composer.md#2-modify-dag-file) - ## Customize sources ### Access and select resources to load @@ -114,6 +113,22 @@ Note that `add_limit` **does not limit the number of records** but rather the "n Find more on sampling data [here](resource.md#sample-from-large-data). +### Rename the source +`dlt` allows you to rename the source ie. to place the source configuration into custom section or to have many instances +of the source created side by side. For example: +```py +from dlt.sources.sql_database import sql_database + +my_db = sql_database.with_args(name="my_db", section="my_db")(table_names=["table_1"]) +print(my_db.name) +``` +Here we create a renamed version of the `sql_database` and then instantiate it. Such source will read +credentials from: +```toml +[sources.my_db.my_db.credentials] +password="..." +``` + ### Add more resources to existing source You can add a custom resource to a source after it was created. Imagine that you want to score all the deals with a keras model that will tell you if the deal is a fraud or not. In order to do that, you declare a new [transformer that takes the data from](resource.md#feeding-data-from-one-resource-into-another) `deals` resource and add it to the source. diff --git a/docs/website/docs/reference/performance.md b/docs/website/docs/reference/performance.md index 1e58080200..0f536fa786 100644 --- a/docs/website/docs/reference/performance.md +++ b/docs/website/docs/reference/performance.md @@ -265,3 +265,29 @@ DLT_USE_JSON=simplejson Instead of using Python Requests directly, you can use the built-in [requests wrapper](../general-usage/http/requests) or [`RESTClient`](../general-usage/http/rest-client) for API calls. This will make your pipeline more resilient to intermittent network errors and other random glitches. + +## Keep pipeline working folder in a bucket on constrained environments. +`dlt` stores extracted data in load packages in order to load them atomically. In case you extract a lot of data at once (ie. backfill) or +your runtime env has constrained local storage (ie. cloud functions) you can keep your data on a bucket by using [FUSE](https://github.com/libfuse/libfuse) or +any other option which your cloud provider supplies. + +`dlt` users rename when saving files and "committing" packages (folder rename). Those may be not supported on bucket filesystems. Often +`rename` is translated into `copy` automatically. In other cases `dlt` will fallback to copy itself. + +In case of cloud function and gs bucket mounts, increasing the rename limit for folders is possible: +```hcl +volume_mounts { + mount_path = "/usr/src/ingestion/pipeline_storage" + name = "pipeline_bucket" + } +volumes { + name = "pipeline_bucket" + gcs { + bucket = google_storage_bucket.dlt_pipeline_data_bucket.name + read_only = false + mount_options = [ + "rename-dir-limit=100000" + ] + } +} +``` diff --git a/tests/common/normalizers/test_json_relational.py b/tests/common/normalizers/test_json_relational.py index 35bc80add2..c35ecdef7f 100644 --- a/tests/common/normalizers/test_json_relational.py +++ b/tests/common/normalizers/test_json_relational.py @@ -880,6 +880,35 @@ def test_propagation_update_on_table_change(norm: RelationalNormalizer): "table_3" ] == {"_dlt_id": "_dlt_root_id", "prop1": "prop2"} + # force propagation when table has nested table that needs root_key + # also use custom name for row_key + table_4 = new_table( + "table_4", write_disposition="replace", columns=[{"name": "primary_key", "row_key": True}] + ) + table_4_nested = new_table( + "table_4__nested", + parent_table_name="table_4", + columns=[{"name": "_dlt_root_id", "root_key": True}], + ) + # must add table_4 first + norm.schema.update_table(table_4) + norm.schema.update_table(table_4_nested) + # row key table_4 not propagated because it was added before nested that needs that + # TODO: maybe fix it + assert ( + "table_4" not in norm.schema._normalizers_config["json"]["config"]["propagation"]["tables"] + ) + norm.schema.update_table(table_4) + # also custom key was used + assert norm.schema._normalizers_config["json"]["config"]["propagation"]["tables"][ + "table_4" + ] == {"primary_key": "_dlt_root_id"} + # drop table from schema + norm.schema.drop_tables(["table_4"]) + assert ( + "table_4" not in norm.schema._normalizers_config["json"]["config"]["propagation"]["tables"] + ) + def test_caching_perf(norm: RelationalNormalizer) -> None: from time import time @@ -893,6 +922,10 @@ def test_caching_perf(norm: RelationalNormalizer) -> None: print(f"{time() - start}") +def test_extend_table(norm: RelationalNormalizer) -> None: + pass + + def set_max_nesting(norm: RelationalNormalizer, max_nesting: int) -> None: RelationalNormalizer.update_normalizer_config(norm.schema, {"max_nesting": max_nesting}) norm._reset() diff --git a/tests/helpers/airflow_tests/test_airflow_provider.py b/tests/helpers/airflow_tests/test_airflow_provider.py index 43fb23e48a..2a8e46e2c8 100644 --- a/tests/helpers/airflow_tests/test_airflow_provider.py +++ b/tests/helpers/airflow_tests/test_airflow_provider.py @@ -1,3 +1,7 @@ +import pytest + +pytest.importorskip("airflow") + from airflow import DAG from airflow.decorators import task, dag from airflow.operators.python import PythonOperator diff --git a/tests/helpers/airflow_tests/test_airflow_wrapper.py b/tests/helpers/airflow_tests/test_airflow_wrapper.py index 69e48733e3..06603ffcec 100644 --- a/tests/helpers/airflow_tests/test_airflow_wrapper.py +++ b/tests/helpers/airflow_tests/test_airflow_wrapper.py @@ -2,6 +2,8 @@ import pytest from unittest import mock from typing import Iterator, List + +pytest.importorskip("airflow") from airflow import DAG from airflow.decorators import dag from airflow.operators.python import PythonOperator, get_current_context diff --git a/tests/helpers/airflow_tests/test_join_airflow_scheduler.py b/tests/helpers/airflow_tests/test_join_airflow_scheduler.py index d737f254e3..503aa62359 100644 --- a/tests/helpers/airflow_tests/test_join_airflow_scheduler.py +++ b/tests/helpers/airflow_tests/test_join_airflow_scheduler.py @@ -1,5 +1,8 @@ +import pytest import datetime from pendulum.tz import UTC + +pytest.importorskip("airflow") from airflow import DAG from airflow.decorators import dag, task from airflow.models import DagRun diff --git a/tests/helpers/airflow_tests/utils.py b/tests/helpers/airflow_tests/utils.py index a98ad4333a..4c1482a2ef 100644 --- a/tests/helpers/airflow_tests/utils.py +++ b/tests/helpers/airflow_tests/utils.py @@ -2,9 +2,6 @@ import os import argparse import pytest -from airflow.cli.commands.db_command import resetdb -from airflow.configuration import conf -from airflow.models.variable import Variable from dlt.common.configuration.container import Container from dlt.common.configuration.specs import PluggableRunContext @@ -19,6 +16,8 @@ @pytest.fixture(scope="function", autouse=True) def initialize_airflow_db(): + from airflow.models.variable import Variable + setup_airflow() # backup context providers providers = Container()[PluggableRunContext].providers @@ -35,6 +34,9 @@ def initialize_airflow_db(): def setup_airflow() -> None: + from airflow.cli.commands.db_command import resetdb + from airflow.configuration import conf + # Disable loading examples try: conf.add_section("core") diff --git a/tests/load/pipeline/test_drop.py b/tests/load/pipeline/test_drop.py index 0e44c754e7..330f2606ff 100644 --- a/tests/load/pipeline/test_drop.py +++ b/tests/load/pipeline/test_drop.py @@ -27,13 +27,17 @@ def _attach(pipeline: Pipeline) -> Pipeline: @dlt.source(section="droppable", name="droppable") -def droppable_source() -> List[DltResource]: +def droppable_source(drop_columns: bool = False) -> List[DltResource]: @dlt.resource def droppable_a( - a: dlt.sources.incremental[int] = dlt.sources.incremental("a", 0) + a: dlt.sources.incremental[int] = dlt.sources.incremental("a", 0, range_start="open") ) -> Iterator[Dict[str, Any]]: - yield dict(a=1, b=2, c=3) - yield dict(a=4, b=23, c=24) + if drop_columns: + yield dict(a=1, b=2) + yield dict(a=4, b=23) + else: + yield dict(a=1, b=2, c=3) + yield dict(a=4, b=23, c=24) @dlt.resource def droppable_b( @@ -47,9 +51,17 @@ def droppable_c( qe: dlt.sources.incremental[int] = dlt.sources.incremental("qe"), ) -> Iterator[Dict[str, Any]]: # Grandchild table - yield dict( - asdasd=2424, qe=111, items=[dict(k=2, r=2, labels=[dict(name="abc"), dict(name="www")])] - ) + if drop_columns: + # dropped asdasd, items[r], items.labels.value + yield dict(qe=111, items=[dict(k=2, labels=[dict(name="abc"), dict(name="www")])]) + else: + yield dict( + asdasd=2424, + qe=111, + items=[ + dict(k=2, r=2, labels=[dict(name="abc", value=1), dict(name="www", value=2)]) + ], + ) @dlt.resource def droppable_d( @@ -134,11 +146,17 @@ def assert_destination_state_loaded(pipeline: Pipeline) -> None: ), ids=lambda x: x.name, ) -def test_drop_command_resources_and_state(destination_config: DestinationTestConfiguration) -> None: +@pytest.mark.parametrize("in_source", (True, False)) +def test_drop_command_resources_and_state( + destination_config: DestinationTestConfiguration, in_source: bool +) -> None: """Test the drop command with resource and state path options and verify correct data is deleted from destination and locally""" - source = droppable_source() - pipeline = destination_config.setup_pipeline("drop_test_" + uniq_id(), dev_mode=True) + source: Any = droppable_source() + if not in_source: + source = list(source.selected_resources.values()) + + pipeline = destination_config.setup_pipeline("droppable", dev_mode=True) info = pipeline.run(source, **destination_config.run_kwargs) assert_load_info(info) assert load_table_counts(pipeline, *pipeline.default_schema.tables.keys()) == { @@ -173,6 +191,9 @@ def test_drop_command_resources_and_state(destination_config: DestinationTestCon assert_destination_state_loaded(pipeline) # now run the same droppable_source to see if tables are recreated and they contain right number of items + source = droppable_source(drop_columns=True) + if not in_source: + source = list(source.selected_resources.values()) info = pipeline.run(source, **destination_config.run_kwargs) assert_load_info(info) # 2 versions (one dropped and replaced with schema with dropped tables, then we added missing tables) @@ -192,6 +213,20 @@ def test_drop_command_resources_and_state(destination_config: DestinationTestCon "droppable_c__items": 1, "droppable_c__items__labels": 2, } + # check if columns got correctly dropped + droppable_a_schema = pipeline.default_schema.get_table("droppable_a") + # this table was not dropped so column still exists + assert "c" in droppable_a_schema["columns"] + # dropped asdasd, items[r], items.labels.value + droppable_c_schema = pipeline.default_schema.get_table("droppable_c") + assert "asdasd" not in droppable_c_schema["columns"] + assert "qe" in droppable_c_schema["columns"] + droppable_c_i_schema = pipeline.default_schema.get_table("droppable_c__items") + assert "r" not in droppable_c_i_schema["columns"] + assert "k" in droppable_c_i_schema["columns"] + droppable_c_l_schema = pipeline.default_schema.get_table("droppable_c__items__labels") + assert "value" not in droppable_c_l_schema["columns"] + assert "name" in droppable_c_l_schema["columns"] @pytest.mark.parametrize( diff --git a/tests/load/pipeline/test_refresh_modes.py b/tests/load/pipeline/test_refresh_modes.py index 86479acd2b..fb88ba915c 100644 --- a/tests/load/pipeline/test_refresh_modes.py +++ b/tests/load/pipeline/test_refresh_modes.py @@ -1,5 +1,5 @@ from typing import Any, List - +import os import pytest import dlt from dlt.common.destination.exceptions import DestinationUndefinedEntity @@ -12,7 +12,7 @@ from dlt.extract.source import DltSource from dlt.pipeline.state_sync import load_pipeline_state_from_destination -from tests.utils import clean_test_storage +from tests.utils import clean_test_storage, TEST_STORAGE_ROOT from tests.pipeline.utils import ( _is_filesystem, assert_load_info, @@ -106,19 +106,40 @@ def some_data_4(): ), ids=lambda x: x.name, ) -def test_refresh_drop_sources(destination_config: DestinationTestConfiguration): - pipeline = destination_config.setup_pipeline("refresh_full_test", refresh="drop_sources") +@pytest.mark.parametrize("in_source", (True, False)) +@pytest.mark.parametrize("with_wipe", (True, False)) +def test_refresh_drop_sources( + destination_config: DestinationTestConfiguration, in_source: bool, with_wipe: bool +): + # do not place duckdb in the working dir, because we may wipe it + os.environ["DESTINATION__DUCKDB__CREDENTIALS"] = os.path.join( + TEST_STORAGE_ROOT, "refresh_source_db.duckdb" + ) + + pipeline = destination_config.setup_pipeline("refresh_source") + + data: Any = refresh_source(first_run=True, drop_sources=True) + if not in_source: + data = list(data.selected_resources.values()) # First run pipeline so destination so tables are created - info = pipeline.run( - refresh_source(first_run=True, drop_sources=True), **destination_config.run_kwargs - ) + info = pipeline.run(data, refresh="drop_sources", **destination_config.run_kwargs) assert_load_info(info) + # Second run of pipeline with only selected resources + if with_wipe: + pipeline._wipe_working_folder() + pipeline = destination_config.setup_pipeline("refresh_source") + + data = refresh_source(first_run=False, drop_sources=True).with_resources( + "some_data_1", "some_data_2" + ) + if not in_source: + data = list(data.selected_resources.values()) + info = pipeline.run( - refresh_source(first_run=False, drop_sources=True).with_resources( - "some_data_1", "some_data_2" - ), + data, + refresh="drop_sources", **destination_config.run_kwargs, ) @@ -199,16 +220,37 @@ def test_existing_schema_hash(destination_config: DestinationTestConfiguration): ), ids=lambda x: x.name, ) -def test_refresh_drop_resources(destination_config: DestinationTestConfiguration): +@pytest.mark.parametrize("in_source", (True, False)) +@pytest.mark.parametrize("with_wipe", (True, False)) +def test_refresh_drop_resources( + destination_config: DestinationTestConfiguration, in_source: bool, with_wipe: bool +): + # do not place duckdb in the working dir, because we may wipe it + os.environ["DESTINATION__DUCKDB__CREDENTIALS"] = os.path.join( + TEST_STORAGE_ROOT, "refresh_source_db.duckdb" + ) # First run pipeline with load to destination so tables are created - pipeline = destination_config.setup_pipeline("refresh_full_test", refresh="drop_tables") + pipeline = destination_config.setup_pipeline("refresh_source") - info = pipeline.run(refresh_source(first_run=True), **destination_config.run_kwargs) + data: Any = refresh_source(first_run=True) + if not in_source: + data = list(data.selected_resources.values()) + + info = pipeline.run(data, refresh="drop_resources", **destination_config.run_kwargs) assert_load_info(info) # Second run of pipeline with only selected resources + if with_wipe: + pipeline._wipe_working_folder() + pipeline = destination_config.setup_pipeline("refresh_source") + + data = refresh_source(first_run=False).with_resources("some_data_1", "some_data_2") + if not in_source: + data = list(data.selected_resources.values()) + info = pipeline.run( - refresh_source(first_run=False).with_resources("some_data_1", "some_data_2"), + data, + refresh="drop_resources", **destination_config.run_kwargs, ) @@ -309,7 +351,9 @@ def test_refresh_drop_data_only(destination_config: DestinationTestConfiguration @pytest.mark.parametrize( "destination_config", - destinations_configs(default_sql_configs=True, subset=["duckdb"]), + destinations_configs( + default_sql_configs=True, local_filesystem_configs=True, subset=["duckdb", "filesystem"] + ), ids=lambda x: x.name, ) def test_refresh_drop_sources_multiple_sources(destination_config: DestinationTestConfiguration): @@ -364,7 +408,6 @@ def source_2_data_2(): **destination_config.run_kwargs, ) assert_load_info(info, 2) - # breakpoint() info = pipeline.run( refresh_source_2(first_run=False).with_resources("source_2_data_1"), **destination_config.run_kwargs, @@ -388,7 +431,7 @@ def source_2_data_2(): result = sorted([(row["id"], row["name"]) for row in data["some_data_1"]]) assert result == [(1, "John"), (2, "Jane")] - # # First table from source2 exists, with only first column + # First table from source2 exists, with only first column data = load_tables_to_dicts(pipeline, "source_2_data_1", schema_name="refresh_source_2") assert_only_table_columns( pipeline, "source_2_data_1", ["product"], schema_name="refresh_source_2" @@ -396,7 +439,7 @@ def source_2_data_2(): result = sorted([row["product"] for row in data["source_2_data_1"]]) assert result == ["orange", "pear"] - # # Second table from source 2 is gone + # Second table from source 2 is gone assert not table_exists(pipeline, "source_2_data_2", schema_name="refresh_source_2") diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index e58db64e5e..b32854b110 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -1566,6 +1566,30 @@ def test_drop() -> None: pipeline.run([1, 2, 3], table_name="numbers") +def test_source_schema_in_resource() -> None: + run_count = 0 + + @dlt.resource + def schema_inspector(): + schema = dlt.current.source_schema() + if run_count == 0: + assert "schema_inspector" not in schema.tables + if run_count == 1: + assert "schema_inspector" in schema.tables + assert schema.tables["schema_inspector"]["columns"]["value"]["x-custom"] == "X" # type: ignore[typeddict-item] + + yield [1, 2, 3] + + pipeline = dlt.pipeline(pipeline_name="test_inspector", destination="duckdb") + pipeline.run(schema_inspector()) + + # add custom annotation + pipeline.default_schema.tables["schema_inspector"]["columns"]["value"]["x-custom"] = "X" # type: ignore[typeddict-unknown-key] + + run_count += 1 + pipeline.run(schema_inspector()) + + def test_schema_version_increase_and_source_update() -> None: now = pendulum.now() From b8bac750fa8089079bf73badaf78c6c514aedbbf Mon Sep 17 00:00:00 2001 From: David Scharf Date: Sun, 15 Dec 2024 17:16:39 +0100 Subject: [PATCH 14/23] prepare dataset release & docs updates (#2126) * remove standalone dataset from exports * make pipeline dataset factory public * rework transformation section * fix some linting errors * add row counts feature for readabledataset * add dataset access example to getting started scripts * add notes about row_counts special query to datasets docs * fix internal docusaurus links * Update docs/website/docs/intro.md * Update docs/website/docs/tutorial/load-data-from-an-api.md * Update docs/website/docs/tutorial/load-data-from-an-api.md * Update docs/website/docs/tutorial/load-data-from-an-api.md * Update docs/website/docs/general-usage/dataset-access/dataset.md * Update docs/website/docs/general-usage/dataset-access/dataset.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/destinations/duckdb.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/transformations/index.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/python.md * Update docs/website/docs/dlt-ecosystem/transformations/sql.md * Update docs/website/docs/dlt-ecosystem/transformations/sql.md * Update docs/website/docs/dlt-ecosystem/transformations/sql.md * Update docs/website/docs/dlt-ecosystem/transformations/sql.md * Update docs/website/docs/dlt-ecosystem/transformations/sql.md * Update docs/website/docs/general-usage/dataset-access/dataset.md --------- Co-authored-by: Alena Astrakhantseva --- dlt/__init__.py | 2 - dlt/common/destination/reference.py | 4 + dlt/destinations/dataset/dataset.py | 28 +++- dlt/pipeline/pipeline.py | 2 +- .../website/docs/build-a-pipeline-tutorial.md | 27 ++-- .../docs/dlt-ecosystem/destinations/duckdb.md | 2 +- .../dlt-ecosystem/transformations/dbt/dbt.md | 8 +- .../dlt-ecosystem/transformations/index.md | 27 ++++ .../dlt-ecosystem/transformations/pandas.md | 42 ------ .../dlt-ecosystem/transformations/python.md | 109 ++++++++++++++ .../docs/dlt-ecosystem/transformations/sql.md | 55 +++++--- .../verified-sources/rest_api/basic.md | 2 +- .../general-usage/dataset-access/dataset.md | 25 +++- .../dataset-access/ibis-backend.md | 2 +- .../website/docs/general-usage/destination.md | 2 +- docs/website/docs/general-usage/state.md | 15 +- docs/website/docs/intro.md | 18 ++- .../docs/tutorial/load-data-from-an-api.md | 20 ++- docs/website/sidebars.js | 11 +- .../test_readable_dbapi_dataset.py | 8 +- tests/extract/test_incremental.py | 8 +- tests/load/duckdb/test_duckdb_client.py | 4 +- tests/load/filesystem/test_sql_client.py | 6 +- tests/load/pipeline/test_bigquery.py | 8 +- tests/load/pipeline/test_duckdb.py | 4 +- tests/load/test_read_interfaces.py | 133 ++++++++++++++---- tests/pipeline/test_dlt_versions.py | 2 +- tests/pipeline/test_pipeline.py | 8 +- tests/pipeline/test_pipeline_extra.py | 4 +- 29 files changed, 432 insertions(+), 154 deletions(-) create mode 100644 docs/website/docs/dlt-ecosystem/transformations/index.md delete mode 100644 docs/website/docs/dlt-ecosystem/transformations/pandas.md create mode 100644 docs/website/docs/dlt-ecosystem/transformations/python.md diff --git a/dlt/__init__.py b/dlt/__init__.py index e8a1b7bf92..328817efd2 100644 --- a/dlt/__init__.py +++ b/dlt/__init__.py @@ -42,7 +42,6 @@ ) from dlt.pipeline import progress from dlt import destinations -from dlt.destinations.dataset import dataset as _dataset pipeline = _pipeline current = _current @@ -80,7 +79,6 @@ "TCredentials", "sources", "destinations", - "_dataset", ] # verify that no injection context was created diff --git a/dlt/common/destination/reference.py b/dlt/common/destination/reference.py index 048fe2186f..827034ddca 100644 --- a/dlt/common/destination/reference.py +++ b/dlt/common/destination/reference.py @@ -592,6 +592,10 @@ def __getattr__(self, table: str) -> SupportsReadableRelation: ... def ibis(self) -> IbisBackend: ... + def row_counts( + self, *, data_tables: bool = True, dlt_tables: bool = False, table_names: List[str] = None + ) -> SupportsReadableRelation: ... + class JobClientBase(ABC): def __init__( diff --git a/dlt/destinations/dataset/dataset.py b/dlt/destinations/dataset/dataset.py index e443045e49..fc55393a60 100644 --- a/dlt/destinations/dataset/dataset.py +++ b/dlt/destinations/dataset/dataset.py @@ -1,4 +1,4 @@ -from typing import Any, Union, TYPE_CHECKING +from typing import Any, Union, TYPE_CHECKING, List from dlt.common.json import json @@ -133,6 +133,32 @@ def table(self, table_name: str) -> SupportsReadableRelation: table_name=table_name, ) # type: ignore[abstract] + def row_counts( + self, *, data_tables: bool = True, dlt_tables: bool = False, table_names: List[str] = None + ) -> SupportsReadableRelation: + """Returns a dictionary of table names and their row counts, returns counts of all data tables by default""" + """If table_names is provided, only the tables in the list are returned regardless of the data_tables and dlt_tables flags""" + + selected_tables = table_names or [] + if not selected_tables: + if data_tables: + selected_tables += self.schema.data_table_names(seen_data_only=True) + if dlt_tables: + selected_tables += self.schema.dlt_table_names() + + # Build UNION ALL query to get row counts for all selected tables + queries = [] + for table in selected_tables: + queries.append( + f"SELECT '{table}' as table_name, COUNT(*) as row_count FROM" + f" {self.sql_client.make_qualified_table_name(table)}" + ) + + query = " UNION ALL ".join(queries) + + # Execute query and build result dict + return self(query) + def __getitem__(self, table_name: str) -> SupportsReadableRelation: """access of table via dict notation""" return self.table(table_name) diff --git a/dlt/pipeline/pipeline.py b/dlt/pipeline/pipeline.py index 9bd2d6911f..74466a09e4 100644 --- a/dlt/pipeline/pipeline.py +++ b/dlt/pipeline/pipeline.py @@ -1750,7 +1750,7 @@ def __getstate__(self) -> Any: # pickle only the SupportsPipeline protocol fields return {"pipeline_name": self.pipeline_name} - def _dataset( + def dataset( self, schema: Union[Schema, str, None] = None, dataset_type: TDatasetType = "auto" ) -> SupportsReadableDataset: """Returns a dataset object for querying the destination data. diff --git a/docs/website/docs/build-a-pipeline-tutorial.md b/docs/website/docs/build-a-pipeline-tutorial.md index f85d2e19ea..36d30a184f 100644 --- a/docs/website/docs/build-a-pipeline-tutorial.md +++ b/docs/website/docs/build-a-pipeline-tutorial.md @@ -262,20 +262,30 @@ In this example, the first pipeline loads the data using `pipedrive_source()`. T #### [Using the `dlt` SQL client](dlt-ecosystem/transformations/sql.md) -Another option is to leverage the `dlt` SQL client to query the loaded data and perform transformations using SQL statements. You can execute SQL statements that change the database schema or manipulate data within tables. Here's an example of inserting a row into the `customers` table using the `dlt` SQL client: +Another option is to leverage the `dlt` SQL client to query the loaded data and perform transformations using SQL statements. You can execute SQL statements that change the database schema or manipulate data within tables. Here's an example of creating a new table with aggregated sales data in duckdb: ```py -pipeline = dlt.pipeline(destination="bigquery", dataset_name="crm") +pipeline = dlt.pipeline(destination="duckdb", dataset_name="crm") with pipeline.sql_client() as client: client.execute_sql( - "INSERT INTO customers VALUES (%s, %s, %s)", 10, "Fred", "fred@fred.com" - ) + """ CREATE TABLE aggregated_sales AS + SELECT + category, + region, + SUM(amount) AS total_sales, + AVG(amount) AS average_sales + FROM + sales + GROUP BY + category, + region; + """) ``` In this example, the `execute_sql` method of the SQL client allows you to execute SQL statements. The statement inserts a row with values into the `customers` table. -#### [Using Pandas](dlt-ecosystem/transformations/pandas.md) +#### [Using Pandas](dlt-ecosystem/transformations/python.md) You can fetch query results as Pandas data frames and perform transformations using Pandas functionalities. Here's an example of reading data from the `issues` table in DuckDB and counting reaction types using Pandas: @@ -287,11 +297,8 @@ pipeline = dlt.pipeline( dev_mode=True ) -with pipeline.sql_client() as client: - with client.execute_query( - 'SELECT "reactions__+1", "reactions__-1", reactions__laugh, reactions__hooray, reactions__rocket FROM issues' - ) as cursor: - reactions = cursor.df() +# get a dataframe of all reactions from the dataset +reactions = pipeline.dataset().issues.select("reactions__+1", "reactions__-1", "reactions__laugh", "reactions__hooray", "reactions__rocket").df() counts = reactions.sum(0).sort_values(0, ascending=False) ``` diff --git a/docs/website/docs/dlt-ecosystem/destinations/duckdb.md b/docs/website/docs/dlt-ecosystem/destinations/duckdb.md index 2b284e991a..a4537195ff 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/duckdb.md +++ b/docs/website/docs/dlt-ecosystem/destinations/duckdb.md @@ -118,7 +118,7 @@ to disable tz adjustments. ## Destination configuration -By default, a DuckDB database will be created in the current working directory with a name `.duckdb` (`chess.duckdb` in the example above). After loading, it is available in `read/write` mode via `with pipeline.sql_client() as con:`, which is a wrapper over `DuckDBPyConnection`. See [duckdb docs](https://duckdb.org/docs/api/python/overview#persistent-storage) for details. +By default, a DuckDB database will be created in the current working directory with a name `.duckdb` (`chess.duckdb` in the example above). After loading, it is available in **read/write** mode via `with pipeline.sql_client() as con:`, which is a wrapper over `DuckDBPyConnection`. See [duckdb docs](https://duckdb.org/docs/api/python/overview#persistent-storage) for details. If you want to **read** data, use [pipeline.dataset()](../../general-usage/dataset-access/dataset) instead of `sql_client`. The `duckdb` credentials do not require any secret values. [You are free to pass the credentials and configuration explicitly](../../general-usage/destination.md#pass-explicit-credentials). For example: ```py diff --git a/docs/website/docs/dlt-ecosystem/transformations/dbt/dbt.md b/docs/website/docs/dlt-ecosystem/transformations/dbt/dbt.md index 449f8b8bde..59eb340ef2 100644 --- a/docs/website/docs/dlt-ecosystem/transformations/dbt/dbt.md +++ b/docs/website/docs/dlt-ecosystem/transformations/dbt/dbt.md @@ -1,10 +1,10 @@ --- -title: Transform the data with dbt +title: Transforming data with dbt description: Transforming the data loaded by a dlt pipeline with dbt keywords: [transform, dbt, runner] --- -# Transform the data with dbt +# Transforming data with dbt [dbt](https://github.com/dbt-labs/dbt-core) is a framework that allows for the simple structuring of your transformations into DAGs. The benefits of using dbt include: @@ -105,8 +105,8 @@ You can run the example with dbt debug log: `RUNTIME__LOG_LEVEL=DEBUG python dbt ## Other transforming tools -If you want to transform the data before loading, you can use Python. If you want to transform the data after loading, you can use dbt or one of the following: +If you want to transform your data before loading, you can use Python. If you want to transform your data after loading, you can use dbt or one of the following: 1. [`dlt` SQL client.](../sql.md) -2. [Pandas.](../pandas.md) +2. [Python with dataframes or arrow tables.](../python.md) diff --git a/docs/website/docs/dlt-ecosystem/transformations/index.md b/docs/website/docs/dlt-ecosystem/transformations/index.md new file mode 100644 index 0000000000..6c51e8cd8d --- /dev/null +++ b/docs/website/docs/dlt-ecosystem/transformations/index.md @@ -0,0 +1,27 @@ +--- +title: Transforming your data +description: How to transform your data +keywords: [datasets, data, access, transformations] +--- +import DocCardList from '@theme/DocCardList'; + +# Transforming data + +If you'd like to transform your data after a pipeline load, you have 3 options available to you: + +* [Using dbt](./dbt/dbt.md) - dlt provides a convenient dbt wrapper to make integration easier. +* [Using the `dlt` SQL client](./sql.md) - dlt exposes an SQL client to transform data on your destination directly using SQL. +* [Using Python with DataFrames or Arrow tables](./python.md) - you can also transform your data using Arrow tables and DataFrames in Python. + +If you need to preprocess some of your data before it is loaded, you can learn about strategies to: + +* [Rename columns.](../../general-usage/customising-pipelines/renaming_columns) +* [Pseudonymize columns.](../../general-usage/customising-pipelines/pseudonymizing_columns) +* [Remove columns.](../../general-usage/customising-pipelines/removing_columns) + +This is particularly useful if you are trying to remove data related to PII or other sensitive data, you want to remove columns that are not needed for your use case or you are using a destination that does not support certain data types in your source data. + + +# Learn more + + diff --git a/docs/website/docs/dlt-ecosystem/transformations/pandas.md b/docs/website/docs/dlt-ecosystem/transformations/pandas.md deleted file mode 100644 index e431313d1c..0000000000 --- a/docs/website/docs/dlt-ecosystem/transformations/pandas.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Transform the data with Pandas -description: Transform the data loaded by a dlt pipeline with Pandas -keywords: [transform, pandas] ---- - -# Transform the data with Pandas - -You can fetch the results of any SQL query as a dataframe. If the destination supports that -natively (i.e., BigQuery and DuckDB), `dlt` uses the native method. Thanks to this, reading -dataframes can be really fast! The example below reads GitHub reactions data from the `issues` table and -counts the reaction types. - -```py -pipeline = dlt.pipeline( - pipeline_name="github_pipeline", - destination="duckdb", - dataset_name="github_reactions", - dev_mode=True -) -with pipeline.sql_client() as client: - with client.execute_query( - 'SELECT "reactions__+1", "reactions__-1", reactions__laugh, reactions__hooray, reactions__rocket FROM issues' - ) as cursor: - # calling `df` on a cursor, returns the data as a pandas data frame - reactions = cursor.df() -counts = reactions.sum(0).sort_values(0, ascending=False) -``` - -The `df` method above returns all the data in the cursor as a data frame. You can also fetch data in -chunks by passing the `chunk_size` argument to the `df` method. - -Once your data is in a Pandas dataframe, you can transform it as needed. - -## Other transforming tools - -If you want to transform the data before loading, you can use Python. If you want to transform the -data after loading, you can use Pandas or one of the following: - -1. [dbt.](dbt/dbt.md) (recommended) -2. [`dlt` SQL client.](sql.md) - diff --git a/docs/website/docs/dlt-ecosystem/transformations/python.md b/docs/website/docs/dlt-ecosystem/transformations/python.md new file mode 100644 index 0000000000..d43f8caaca --- /dev/null +++ b/docs/website/docs/dlt-ecosystem/transformations/python.md @@ -0,0 +1,109 @@ +--- +title: Transforming data in Python with Arrow tables or DataFrames +description: Transforming data loaded by a dlt pipeline with pandas dataframes or arrow tables +keywords: [transform, pandas] +--- + +# Transforming data in Python with Arrow tables or DataFrames + +You can transform your data in Python using Pandas DataFrames or Arrow tables. To get started, please read the [dataset docs](../../general-usage/dataset-access/dataset). + + +## Interactively transforming your data in Python + +Using the methods explained in the [dataset docs](../../general-usage/dataset-access/dataset), you can fetch data from your destination into a DataFrame or Arrow table in your local Python process and work with it interactively. This even works for filesystem destinations: + + +The example below reads GitHub reactions data from the `issues` table and +counts the reaction types. + +```py +pipeline = dlt.pipeline( + pipeline_name="github_pipeline", + destination="duckdb", + dataset_name="github_reactions", + dev_mode=True +) + +# get a dataframe of all reactions from the dataset +reactions = pipeline.dataset().issues.select("reactions__+1", "reactions__-1", "reactions__laugh", "reactions__hooray", "reactions__rocket").df() + +# calculate and print out the sum of all reactions +counts = reactions.sum(0).sort_values(0, ascending=False) +print(counts) + +# alternatively, you can fetch the data as an arrow table +reactions = pipeline.dataset().issues.select("reactions__+1", "reactions__-1", "reactions__laugh", "reactions__hooray", "reactions__rocket").arrow() +# ... do transformations on the arrow table +``` + +## Persisting your transformed data + +Since dlt supports DataFrames and Arrow tables from resources directly, you can use the same pipeline to load the transformed data back into the destination. + + +### A simple example + +A simple example that creates a new table from an existing user table but only with columns that do not contain private information. Note that we use the `iter_arrow()` method on the relation to iterate over the arrow table instead of fetching it all at once. + +```py +pipeline = dlt.pipeline( + pipeline_name="users_pipeline", + destination="duckdb", + dataset_name="users_raw", + dev_mode=True +) + +# get user relation with only a few columns selected, but omitting email and name +users = pipeline.dataset().users.select("age", "amount_spent", "country") + +# load the data into a new table called users_clean in the same dataset +pipeline.run(users.iter_arrow(chunk_size=1000), table_name="users_clean") +``` + +### A more complex example + +The example above could easily be done in SQL. Let's assume you'd like to actually do in Python some Arrow transformations. For this will create a resources from which we can yield the modified Arrow tables. The same is possibly with DataFrames. + +```py +import pyarrow.compute as pc + +pipeline = dlt.pipeline( + pipeline_name="users_pipeline", + destination="duckdb", + dataset_name="users_raw", + dev_mode=True +) + +# NOTE: this resource will work like a regular resource and support write_disposition, primary_key, etc. +# NOTE: For selecting only users above 18, we could also use the filter method on the relation with ibis expressions +@dlt.resource(table_name="users_clean") +def users_clean(): + users = pipeline.dataset().users + for arrow_table in users.iter_arrow(chunk_size=1000): + + # we want to filter out users under 18 + age_filter = pc.greater_equal(arrow_table["age"], 18) + arrow_table = arrow_table.filter(age_filter) + + # we want to hash the email column + arrow_table = arrow_table.append_column("email_hash", pc.sha256(arrow_table["email"])) + + # we want to remove the email column and name column + arrow_table = arrow_table.drop(["email", "name"]) + + # yield the transformed arrow table + yield arrow_table + + +pipeline.run(users_clean()) +``` + +## Other transforming tools + +If you want to transform your data before loading, you can use Python. If you want to transform the +data after loading, you can use Pandas or one of the following: + +1. [dbt.](dbt/dbt.md) (recommended) +2. [`dlt` SQL client.](sql.md) + diff --git a/docs/website/docs/dlt-ecosystem/transformations/sql.md b/docs/website/docs/dlt-ecosystem/transformations/sql.md index ffd348d1a0..60f3e7f7a5 100644 --- a/docs/website/docs/dlt-ecosystem/transformations/sql.md +++ b/docs/website/docs/dlt-ecosystem/transformations/sql.md @@ -1,33 +1,52 @@ --- -title: Transform the data with SQL +title: Transforming data with SQL description: Transforming the data loaded by a dlt pipeline with the dlt SQL client keywords: [transform, sql] --- -# Transform the data using the `dlt` SQL client +# Transforming data using the `dlt` SQL client A simple alternative to dbt is to query the data using the `dlt` SQL client and then perform the -transformations using Python. The `execute_sql` method allows you to execute any SQL statement, +transformations using SQL statements in Python. The `execute_sql` method allows you to execute any SQL statement, including statements that change the database schema or data in the tables. In the example below, we insert a row into the `customers` table. Note that the syntax is the same as for any standard `dbapi` connection. +:::info +* This method will work for all SQL destinations supported by `dlt`, but not for the filesystem destination. +* Read the [SQL client docs](../../ general-usage/dataset-access/dataset) for more information on how to access data with the SQL client. +* If you are simply trying to read data, you should use the powerful [dataset interface](../../general-usage/dataset-access/dataset) instead. +::: + + +Typically you will use this type of transformation if you can create or update tables directly from existing tables +without any need to insert data from your Python environment. + +The example below creates a new table `aggregated_sales` that contains the total and average sales for each category and region + + ```py -pipeline = dlt.pipeline(destination="bigquery", dataset_name="crm") -try: - with pipeline.sql_client() as client: - client.execute_sql( - "INSERT INTO customers VALUES (%s, %s, %s)", - 10, - "Fred", - "fred@fred.com" - ) -except Exception: - ... +pipeline = dlt.pipeline(destination="duckdb", dataset_name="crm") + +# NOTE: this is the duckdb sql dialect, other destinations may use different expressions +with pipeline.sql_client() as client: + client.execute_sql( + """ CREATE OR REPLACE TABLE aggregated_sales AS + SELECT + category, + region, + SUM(amount) AS total_sales, + AVG(amount) AS average_sales + FROM + sales + GROUP BY + category, + region; + """) ``` -In the case of SELECT queries, the data is returned as a list of rows, with the elements of a row -corresponding to selected columns. +You can also use the `execute_sql` method to run select queries. The data is returned as a list of rows, with the elements of a row +corresponding to selected columns. A more convenient way to extract data is to use dlt datasets. ```py try: @@ -44,9 +63,9 @@ except Exception: ## Other transforming tools -If you want to transform the data before loading, you can use Python. If you want to transform the +If you want to transform your data before loading, you can use Python. If you want to transform the data after loading, you can use SQL or one of the following: 1. [dbt](dbt/dbt.md) (recommended). -2. [Pandas](pandas.md). +2. [Python with DataFrames or Arrow tables](python.md). diff --git a/docs/website/docs/dlt-ecosystem/verified-sources/rest_api/basic.md b/docs/website/docs/dlt-ecosystem/verified-sources/rest_api/basic.md index 14d9ecb04b..ea3c9c768b 100644 --- a/docs/website/docs/dlt-ecosystem/verified-sources/rest_api/basic.md +++ b/docs/website/docs/dlt-ecosystem/verified-sources/rest_api/basic.md @@ -306,7 +306,7 @@ A resource configuration is used to define a [dlt resource](../../../general-usa - `write_disposition`: The write disposition for the resource. - `primary_key`: The primary key for the resource. - `include_from_parent`: A list of fields from the parent resource to be included in the resource output. See the [resource relationships](#include-fields-from-the-parent-resource) section for more details. -- `processing_steps`: A list of [processing steps](#processing-steps-filter-and-transform-data) to filter and transform the data. +- `processing_steps`: A list of [processing steps](#processing-steps-filter-and-transform-data) to filter and transform your data. - `selected`: A flag to indicate if the resource is selected for loading. This could be useful when you want to load data only from child resources and not from the parent resource. - `auth`: An optional `AuthConfig` instance. If passed, is used over the one defined in the [client](#client) definition. Example: ```py diff --git a/docs/website/docs/general-usage/dataset-access/dataset.md b/docs/website/docs/general-usage/dataset-access/dataset.md index b2e3f03d4d..f9c01603f6 100644 --- a/docs/website/docs/general-usage/dataset-access/dataset.md +++ b/docs/website/docs/general-usage/dataset-access/dataset.md @@ -19,7 +19,7 @@ Here's a full example of how to retrieve data from a pipeline and load it into a # and you have loaded data to a table named 'items' in the destination # Step 1: Get the readable dataset from the pipeline -dataset = pipeline._dataset() +dataset = pipeline.dataset() # Step 2: Access a table as a ReadableRelation items_relation = dataset.items # Or dataset["items"] @@ -39,7 +39,10 @@ Assuming you have a `Pipeline` object (let's call it `pipeline`), you can obtain ```py # Get the readable dataset from the pipeline -dataset = pipeline._dataset() +dataset = pipeline.dataset() + +# print the row counts of all tables in the destination as dataframe +print(dataset.row_counts().df()) ``` ### Access tables as `ReadableRelation` @@ -116,6 +119,18 @@ for items_chunk in items_relation.iter_fetch(chunk_size=500): The methods available on the ReadableRelation correspond to the methods available on the cursor returned by the SQL client. Please refer to the [SQL client](./sql-client.md#supported-methods-on-the-cursor) guide for more information. +## Special queries + +You can use the `row_counts` method to get the row counts of all tables in the destination as a DataFrame. + +```py +# print the row counts of all tables in the destination as dataframe +print(dataset.row_counts().df()) + +# or as tuples +print(dataset.row_counts().fetchall()) +``` + ## Modifying queries You can refine your data retrieval by limiting the number of records, selecting specific columns, or chaining these operations. @@ -168,7 +183,7 @@ dlt will then wrap an `ibis.UnboundTable` with a `ReadableIbisRelation` object u ```py # now that ibis is installed, we can get a dataset with ibis relations -dataset = pipeline._dataset() +dataset = pipeline.dataset() # get two relations items_relation = dataset["items"] @@ -284,7 +299,9 @@ other_pipeline = dlt.pipeline(pipeline_name="other_pipeline", destination="duckd other_pipeline.run(limited_items_relation.iter_arrow(chunk_size=10_000), table_name="limited_items") ``` -### Using `ibis` to query the data +Learn more about [transforming data in Python with Arrow tables or DataFrames](../../dlt-ecosystem/transformations/python). + +### Using `ibis` to query data Visit the [Native Ibis integration](./ibis-backend.md) guide to learn more. diff --git a/docs/website/docs/general-usage/dataset-access/ibis-backend.md b/docs/website/docs/general-usage/dataset-access/ibis-backend.md index 9f9b65e9c0..bc8487940e 100644 --- a/docs/website/docs/general-usage/dataset-access/ibis-backend.md +++ b/docs/website/docs/general-usage/dataset-access/ibis-backend.md @@ -28,7 +28,7 @@ pip install ibis-framework[duckdb] ```py # get the dataset from the pipeline -dataset = pipeline._dataset() +dataset = pipeline.dataset() dataset_name = pipeline.dataset_name # get the native ibis connection from the dataset diff --git a/docs/website/docs/general-usage/destination.md b/docs/website/docs/general-usage/destination.md index fa133b6257..ba42869957 100644 --- a/docs/website/docs/general-usage/destination.md +++ b/docs/website/docs/general-usage/destination.md @@ -128,7 +128,7 @@ When loading data, `dlt` will access the destination in two cases: 1. At the beginning of the `run` method to sync the pipeline state with the destination (or if you call `pipeline.sync_destination` explicitly). 2. In the `pipeline.load` method - to migrate the schema and load the load package. -Obviously, `dlt` will access the destination when you instantiate [sql_client](../dlt-ecosystem/transformations/sql.md). +`dlt` will also access the destination when you instantiate [sql_client](../dlt-ecosystem/transformations/sql.md). :::note `dlt` will not import the destination dependencies or access destination configuration if access is not needed. You can build multi-stage pipelines where steps are executed in separate processes or containers - the `extract` and `normalize` step do not need destination dependencies, configuration, and actual connection. diff --git a/docs/website/docs/general-usage/state.md b/docs/website/docs/general-usage/state.md index 46aa1d63ce..d1fb426452 100644 --- a/docs/website/docs/general-usage/state.md +++ b/docs/website/docs/general-usage/state.md @@ -123,14 +123,13 @@ def comments(user_id: str): # on the first pipeline run, the user_comments table does not yet exist so do not check at all # alternatively, catch DatabaseUndefinedRelation which is raised when an unknown table is selected if not current_pipeline.first_run: - with current_pipeline.sql_client() as client: - # we may get the last user comment or None which we replace with 0 - max_id = ( - client.execute_sql( - "SELECT MAX(_id) FROM user_comments WHERE user_id=?", user_id - )[0][0] - or 0 - ) + # get user comments table from pipeline dataset + user_comments = current_pipeline.dataset().user_comments + # get last user comment id with ibis expression, ibis-extras need to be installed + max_id_df = user_comments.filter(user_comments.user_id == user_id).select(user_comments["_id"].max()).df() + # if there are no comments for the user, max_id will be None, so we replace it with 0 + max_id = max_id_df[0][0] if len(max_id_df.index) else 0 + # use max_id to filter our results (we simulate an API query) yield from [ {"_id": i, "value": letter, "user_id": user_id} diff --git a/docs/website/docs/intro.md b/docs/website/docs/intro.md index b20d41c494..bc227b85ad 100644 --- a/docs/website/docs/intro.md +++ b/docs/website/docs/intro.md @@ -70,6 +70,10 @@ pipeline = dlt.pipeline( ) load_info = pipeline.run(source) + +# print load info and posts table as dataframe +print(load_info) +print(pipeline.dataset().posts.df()) ``` Follow the [REST API source tutorial](./tutorial/rest-api) to learn more about the source configuration and pagination methods. @@ -92,6 +96,10 @@ pipeline = dlt.pipeline( ) load_info = pipeline.run(source) + +# print load info and the "family" table as dataframe +print(load_info) +print(pipeline.dataset().family.df()) ``` Follow the [SQL source tutorial](./tutorial/sql-database) to learn more about the source configuration and supported databases. @@ -116,6 +124,10 @@ pipeline = dlt.pipeline( ) load_info = pipeline.run(resource) + +# print load info and the "example" table as dataframe +print(load_info) +print(pipeline.dataset().example.df()) ``` Follow the [filesystem source tutorial](./tutorial/filesystem) to learn more about the source configuration and supported storage services. @@ -128,7 +140,7 @@ dlt is able to load data from Python generators or directly from Python data str ```py import dlt -@dlt.resource +@dlt.resource(table_name="foo_data") def foo(): for i in range(10): yield {"id": i, "name": f"This is item {i}"} @@ -139,6 +151,10 @@ pipeline = dlt.pipeline( ) load_info = pipeline.run(foo) + +# print load info and the "foo_data" table as dataframe +print(load_info) +print(pipeline.dataset().foo_data.df()) ``` Check out the [Python data structures tutorial](./tutorial/load-data-from-an-api) to learn about dlt fundamentals and advanced usage scenarios. diff --git a/docs/website/docs/tutorial/load-data-from-an-api.md b/docs/website/docs/tutorial/load-data-from-an-api.md index ddfef2cbe8..73f780ba7a 100644 --- a/docs/website/docs/tutorial/load-data-from-an-api.md +++ b/docs/website/docs/tutorial/load-data-from-an-api.md @@ -72,7 +72,25 @@ Load package 1692364844.460054 is LOADED and contains no failed jobs `dlt` just created a database schema called **mydata** (the `dataset_name`) with a table **users** in it. -### Explore the data +### Explore data in Python + +You can use dlt [datasets](../general-usage/dataset-access/dataset) to easily query the data in pure Python. + +```py +# get the dataset +dataset = pipeline.dataset("mydata") + +# get the user relation +table = dataset.users + +# query the full table as dataframe +print(table.df()) + +# query the first 10 rows as arrow table +print(table.limit(10).arrow()) +``` + +### Explore data in Streamlit To allow a sneak peek and basic discovery, you can take advantage of [built-in integration with Streamlit](../reference/command-line-interface#show-tables-and-data-in-the-destination): diff --git a/docs/website/sidebars.js b/docs/website/sidebars.js index 8e8c11fc09..ca75c29392 100644 --- a/docs/website/sidebars.js +++ b/docs/website/sidebars.js @@ -211,13 +211,10 @@ const sidebars = { }, { type: 'category', - label: 'Transform the data', + label: 'Transforming data', link: { - type: 'generated-index', - title: 'Transform the data', - description: 'If you want to transform the data after loading, you can use one of the following methods: dbt, SQL, Pandas.', - slug: 'dlt-ecosystem/transformations', - keywords: ['transformations'], + type: 'doc', + id: 'dlt-ecosystem/transformations/index', }, items: [ { @@ -228,8 +225,8 @@ const sidebars = { 'dlt-ecosystem/transformations/dbt/dbt_cloud', ] }, + 'dlt-ecosystem/transformations/python', 'dlt-ecosystem/transformations/sql', - 'dlt-ecosystem/transformations/pandas', 'general-usage/customising-pipelines/renaming_columns', 'general-usage/customising-pipelines/pseudonymizing_columns', 'general-usage/customising-pipelines/removing_columns' diff --git a/tests/destinations/test_readable_dbapi_dataset.py b/tests/destinations/test_readable_dbapi_dataset.py index bc58a18fa0..e3b318e8d4 100644 --- a/tests/destinations/test_readable_dbapi_dataset.py +++ b/tests/destinations/test_readable_dbapi_dataset.py @@ -9,7 +9,7 @@ def test_query_builder() -> None: - dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline")._dataset() + dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline").dataset() # default query for a table assert dataset.my_table.query.strip() == 'SELECT * FROM "pipeline_dataset"."my_table"' @@ -55,7 +55,7 @@ def test_query_builder() -> None: def test_copy_and_chaining() -> None: - dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline")._dataset() + dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline").dataset() # create releation and set some stuff on it relation = dataset.items @@ -80,7 +80,7 @@ def test_copy_and_chaining() -> None: def test_computed_schema_columns() -> None: - dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline")._dataset() + dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline").dataset() relation = dataset.items # no schema present @@ -107,7 +107,7 @@ def test_computed_schema_columns() -> None: def test_prevent_changing_relation_with_query() -> None: - dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline")._dataset() + dataset = dlt.pipeline(destination="duckdb", pipeline_name="pipeline").dataset() relation = dataset("SELECT * FROM something") with pytest.raises(ReadableRelationHasQueryException): diff --git a/tests/extract/test_incremental.py b/tests/extract/test_incremental.py index 3ebc9d1201..d63dac93f2 100644 --- a/tests/extract/test_incremental.py +++ b/tests/extract/test_incremental.py @@ -228,7 +228,7 @@ def test_pandas_index_as_dedup_key() -> None: no_index_r = some_data.with_name(new_name="no_index") p.run(no_index_r) p.run(no_index_r) - data_ = p._dataset().no_index.arrow() + data_ = p.dataset().no_index.arrow() assert data_.schema.names == ["created_at", "id"] assert data_["id"].to_pylist() == ["a", "b", "c", "d", "e", "f", "g"] @@ -240,7 +240,7 @@ def test_pandas_index_as_dedup_key() -> None: unnamed_index_r.incremental.primary_key = "__index_level_0__" p.run(unnamed_index_r) p.run(unnamed_index_r) - data_ = p._dataset().unnamed_index.arrow() + data_ = p.dataset().unnamed_index.arrow() assert data_.schema.names == ["created_at", "id", "index_level_0"] # indexes 2 and 3 are removed from second batch because they were in the previous batch # and the created_at overlapped so they got deduplicated @@ -258,7 +258,7 @@ def _make_named_index(df_: pd.DataFrame) -> pd.DataFrame: named_index_r.incremental.primary_key = "order_id" p.run(named_index_r) p.run(named_index_r) - data_ = p._dataset().named_index.arrow() + data_ = p.dataset().named_index.arrow() assert data_.schema.names == ["created_at", "id", "order_id"] assert data_["order_id"].to_pylist() == [0, 1, 2, 3, 4, 0, 1, 4] @@ -268,7 +268,7 @@ def _make_named_index(df_: pd.DataFrame) -> pd.DataFrame: ) p.run(named_index_impl_r) p.run(named_index_impl_r) - data_ = p._dataset().named_index_impl.arrow() + data_ = p.dataset().named_index_impl.arrow() assert data_.schema.names == ["created_at", "id"] assert data_["id"].to_pylist() == ["a", "b", "c", "d", "e", "f", "g"] diff --git a/tests/load/duckdb/test_duckdb_client.py b/tests/load/duckdb/test_duckdb_client.py index 49475ce43f..652f75772a 100644 --- a/tests/load/duckdb/test_duckdb_client.py +++ b/tests/load/duckdb/test_duckdb_client.py @@ -282,14 +282,14 @@ def test_drops_pipeline_changes_bound() -> None: p = dlt.pipeline(pipeline_name="quack_pipeline", destination="duckdb") p.run([1, 2, 3], table_name="p_table") p = p.drop() - assert len(p._dataset().p_table.fetchall()) == 3 + assert len(p.dataset().p_table.fetchall()) == 3 # drops internal duckdb p = dlt.pipeline(pipeline_name="quack_pipeline", destination=duckdb(":pipeline:")) p.run([1, 2, 3], table_name="p_table") p = p.drop() with pytest.raises(DatabaseUndefinedRelation): - p._dataset().p_table.fetchall() + p.dataset().p_table.fetchall() def test_duckdb_database_delete() -> None: diff --git a/tests/load/filesystem/test_sql_client.py b/tests/load/filesystem/test_sql_client.py index 4f537d129c..cf4bbfb286 100644 --- a/tests/load/filesystem/test_sql_client.py +++ b/tests/load/filesystem/test_sql_client.py @@ -377,7 +377,7 @@ def items(): pipeline.run([items()], loader_file_format=destination_config.file_format) - df = pipeline._dataset().items.df() + df = pipeline.dataset().items.df() assert len(df.index) == 20 @dlt.resource(table_name="items") @@ -387,5 +387,5 @@ def items2(): pipeline.run([items2()], loader_file_format=destination_config.file_format) # check df and arrow access - assert len(pipeline._dataset().items.df().index) == 50 - assert pipeline._dataset().items.arrow().num_rows == 50 + assert len(pipeline.dataset().items.df().index) == 50 + assert pipeline.dataset().items.arrow().num_rows == 50 diff --git a/tests/load/pipeline/test_bigquery.py b/tests/load/pipeline/test_bigquery.py index cb65c6bcf1..83982bb998 100644 --- a/tests/load/pipeline/test_bigquery.py +++ b/tests/load/pipeline/test_bigquery.py @@ -384,8 +384,8 @@ def resource(): bigquery_adapter(resource, autodetect_schema=True) pipeline.run(resource) - assert len(pipeline._dataset().items.df()) == 5 - assert len(pipeline._dataset().items__nested.df()) == 5 + assert len(pipeline.dataset().items.df()) == 5 + assert len(pipeline.dataset().items__nested.df()) == 5 @dlt.resource(primary_key="id", table_name="items", write_disposition="merge") def resource2(): @@ -395,5 +395,5 @@ def resource2(): bigquery_adapter(resource2, autodetect_schema=True) pipeline.run(resource2) - assert len(pipeline._dataset().items.df()) == 7 - assert len(pipeline._dataset().items__nested.df()) == 7 + assert len(pipeline.dataset().items.df()) == 7 + assert len(pipeline.dataset().items__nested.df()) == 7 diff --git a/tests/load/pipeline/test_duckdb.py b/tests/load/pipeline/test_duckdb.py index a7aa4d36e4..2d1138a51d 100644 --- a/tests/load/pipeline/test_duckdb.py +++ b/tests/load/pipeline/test_duckdb.py @@ -273,10 +273,10 @@ def test_duckdb_credentials_separation( p2 = dlt.pipeline("p2", destination=duckdb(credentials=":pipeline:")) p1.run([1, 2, 3], table_name="p1_data") - p1_dataset = p1._dataset() + p1_dataset = p1.dataset() p2.run([1, 2, 3], table_name="p2_data") - p2_dataset = p2._dataset() + p2_dataset = p2.dataset() # both dataset should have independent duckdb databases # destinations should be bounded to pipelines still diff --git a/tests/load/test_read_interfaces.py b/tests/load/test_read_interfaces.py index d2f5f7951e..bca844d2c8 100644 --- a/tests/load/test_read_interfaces.py +++ b/tests/load/test_read_interfaces.py @@ -25,6 +25,7 @@ ReadableRelationUnknownColumnException, ) from tests.load.utils import drop_pipeline_data +from dlt.destinations.dataset import dataset as _dataset EXPECTED_COLUMNS = ["id", "decimal", "other_decimal", "_dlt_load_id", "_dlt_id"] @@ -169,9 +170,9 @@ def test_explicit_dataset_type_selection(populated_pipeline: Pipeline): from dlt.destinations.dataset.ibis_relation import ReadableIbisRelation assert isinstance( - populated_pipeline._dataset(dataset_type="default").items, ReadableDBAPIRelation + populated_pipeline.dataset(dataset_type="default").items, ReadableDBAPIRelation ) - assert isinstance(populated_pipeline._dataset(dataset_type="ibis").items, ReadableIbisRelation) + assert isinstance(populated_pipeline.dataset(dataset_type="ibis").items, ReadableIbisRelation) @pytest.mark.no_load @@ -183,7 +184,7 @@ def test_explicit_dataset_type_selection(populated_pipeline: Pipeline): ids=lambda x: x.name, ) def test_arrow_access(populated_pipeline: Pipeline) -> None: - table_relationship = populated_pipeline._dataset().items + table_relationship = populated_pipeline.dataset().items total_records = _total_records(populated_pipeline) chunk_size = _chunk_size(populated_pipeline) expected_chunk_counts = _expected_chunk_count(populated_pipeline) @@ -216,7 +217,7 @@ def test_arrow_access(populated_pipeline: Pipeline) -> None: ) def test_dataframe_access(populated_pipeline: Pipeline) -> None: # access via key - table_relationship = populated_pipeline._dataset()["items"] + table_relationship = populated_pipeline.dataset()["items"] total_records = _total_records(populated_pipeline) chunk_size = _chunk_size(populated_pipeline) expected_chunk_counts = _expected_chunk_count(populated_pipeline) @@ -233,7 +234,6 @@ def test_dataframe_access(populated_pipeline: Pipeline) -> None: if not skip_df_chunk_size_check: assert len(df.index) == chunk_size - # lowercase results for the snowflake case assert set(df.columns.values) == set(EXPECTED_COLUMNS) # iterate all dataframes @@ -256,7 +256,7 @@ def test_dataframe_access(populated_pipeline: Pipeline) -> None: ) def test_db_cursor_access(populated_pipeline: Pipeline) -> None: # check fetch accessors - table_relationship = populated_pipeline._dataset().items + table_relationship = populated_pipeline.dataset().items total_records = _total_records(populated_pipeline) chunk_size = _chunk_size(populated_pipeline) expected_chunk_counts = _expected_chunk_count(populated_pipeline) @@ -290,8 +290,7 @@ def test_db_cursor_access(populated_pipeline: Pipeline) -> None: ids=lambda x: x.name, ) def test_hint_preservation(populated_pipeline: Pipeline) -> None: - # NOTE: for now hints are only preserved for the default dataset - table_relationship = populated_pipeline._dataset(dataset_type="default").items + table_relationship = populated_pipeline.dataset(dataset_type="default").items # check that hints are carried over to arrow table expected_decimal_precision = 10 expected_decimal_precision_2 = 12 @@ -319,10 +318,94 @@ def test_hint_preservation(populated_pipeline: Pipeline) -> None: ) def test_loads_table_access(populated_pipeline: Pipeline) -> None: # check loads table access, we should have one entry - loads_table = populated_pipeline._dataset()[populated_pipeline.default_schema.loads_table_name] + loads_table = populated_pipeline.dataset()[populated_pipeline.default_schema.loads_table_name] assert len(loads_table.fetchall()) == 1 +@pytest.mark.no_load +@pytest.mark.essential +@pytest.mark.parametrize( + "populated_pipeline", + configs, + indirect=True, + ids=lambda x: x.name, +) +def test_row_counts(populated_pipeline: Pipeline) -> None: + total_records = _total_records(populated_pipeline) + + dataset = populated_pipeline.dataset() + # default is all data tables + assert set(dataset.row_counts().df().itertuples(index=False, name=None)) == { + ( + "items", + total_records, + ), + ( + "double_items", + total_records, + ), + ( + "items__children", + total_records * 2, + ), + } + # get only one data table + assert set( + dataset.row_counts(table_names=["items"]).df().itertuples(index=False, name=None) + ) == { + ( + "items", + total_records, + ), + } + # get all dlt tables + assert set( + dataset.row_counts(dlt_tables=True, data_tables=False) + .df() + .itertuples(index=False, name=None) + ) == { + ( + "_dlt_version", + 1, + ), + ( + "_dlt_loads", + 1, + ), + ( + "_dlt_pipeline_state", + 1, + ), + } + # get them all + assert set(dataset.row_counts(dlt_tables=True).df().itertuples(index=False, name=None)) == { + ( + "_dlt_version", + 1, + ), + ( + "_dlt_loads", + 1, + ), + ( + "_dlt_pipeline_state", + 1, + ), + ( + "items", + total_records, + ), + ( + "double_items", + total_records, + ), + ( + "items__children", + total_records * 2, + ), + } + + @pytest.mark.no_load @pytest.mark.essential @pytest.mark.parametrize( @@ -334,7 +417,7 @@ def test_loads_table_access(populated_pipeline: Pipeline) -> None: def test_sql_queries(populated_pipeline: Pipeline) -> None: # simple check that query also works tname = populated_pipeline.sql_client().make_qualified_table_name("items") - query_relationship = populated_pipeline._dataset()(f"select * from {tname} where id < 20") + query_relationship = populated_pipeline.dataset()(f"select * from {tname} where id < 20") # we selected the first 20 table = query_relationship.arrow() @@ -346,7 +429,7 @@ def test_sql_queries(populated_pipeline: Pipeline) -> None: f"SELECT i.id, di.double_id FROM {tname} as i JOIN {tdname} as di ON (i.id = di.id) WHERE" " i.id < 20 ORDER BY i.id ASC" ) - join_relationship = populated_pipeline._dataset()(query) + join_relationship = populated_pipeline.dataset()(query) table = join_relationship.fetchall() assert len(table) == 20 assert list(table[0]) == [0, 0] @@ -363,7 +446,7 @@ def test_sql_queries(populated_pipeline: Pipeline) -> None: ids=lambda x: x.name, ) def test_limit_and_head(populated_pipeline: Pipeline) -> None: - table_relationship = populated_pipeline._dataset().items + table_relationship = populated_pipeline.dataset().items assert len(table_relationship.head().fetchall()) == 5 assert len(table_relationship.limit(24).fetchall()) == 24 @@ -384,7 +467,7 @@ def test_limit_and_head(populated_pipeline: Pipeline) -> None: ids=lambda x: x.name, ) def test_column_selection(populated_pipeline: Pipeline) -> None: - table_relationship = populated_pipeline._dataset(dataset_type="default").items + table_relationship = populated_pipeline.dataset(dataset_type="default").items columns = ["_dlt_load_id", "other_decimal"] data_frame = table_relationship.select(*columns).head().df() assert [v.lower() for v in data_frame.columns.values] == columns @@ -421,18 +504,18 @@ def test_schema_arg(populated_pipeline: Pipeline) -> None: """Simple test to ensure schemas may be selected via schema arg""" # if there is no arg, the defautl schema is used - dataset = populated_pipeline._dataset() + dataset = populated_pipeline.dataset() assert dataset.schema.name == populated_pipeline.default_schema_name assert "items" in dataset.schema.tables # setting a different schema name will try to load that schema, # not find one and create an empty schema with that name - dataset = populated_pipeline._dataset(schema="unknown_schema") + dataset = populated_pipeline.dataset(schema="unknown_schema") assert dataset.schema.name == "unknown_schema" assert "items" not in dataset.schema.tables # providing the schema name of the right schema will load it - dataset = populated_pipeline._dataset(schema=populated_pipeline.default_schema_name) + dataset = populated_pipeline.dataset(schema=populated_pipeline.default_schema_name) assert dataset.schema.name == populated_pipeline.default_schema_name assert "items" in dataset.schema.tables @@ -450,7 +533,7 @@ def test_ibis_expression_relation(populated_pipeline: Pipeline) -> None: import ibis # type: ignore # now we should get the more powerful ibis relation - dataset = populated_pipeline._dataset() + dataset = populated_pipeline.dataset() total_records = _total_records(populated_pipeline) items_table = dataset["items"] @@ -653,11 +736,11 @@ def test_ibis_dataset_access(populated_pipeline: Pipeline) -> None: # check correct error if not supported if populated_pipeline.destination.destination_type not in SUPPORTED_DESTINATIONS: with pytest.raises(NotImplementedError): - populated_pipeline._dataset().ibis() + populated_pipeline.dataset().ibis() return total_records = _total_records(populated_pipeline) - ibis_connection = populated_pipeline._dataset().ibis() + ibis_connection = populated_pipeline.dataset().ibis() map_i = lambda x: x if populated_pipeline.destination.destination_type == "dlt.destinations.snowflake": @@ -709,7 +792,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: total_records = _total_records(populated_pipeline) # check dataset factory - dataset = dlt._dataset( + dataset = _dataset( destination=populated_pipeline.destination, dataset_name=populated_pipeline.dataset_name ) # verfiy that sql client and schema are lazy loaded @@ -722,7 +805,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: # check that schema is loaded by name dataset = cast( ReadableDBAPIDataset, - dlt._dataset( + _dataset( destination=populated_pipeline.destination, dataset_name=populated_pipeline.dataset_name, schema=populated_pipeline.default_schema_name, @@ -733,7 +816,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: # check that schema is not loaded when wrong name given dataset = cast( ReadableDBAPIDataset, - dlt._dataset( + _dataset( destination=populated_pipeline.destination, dataset_name=populated_pipeline.dataset_name, schema="wrong_schema_name", @@ -745,7 +828,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: # check that schema is loaded if no schema name given dataset = cast( ReadableDBAPIDataset, - dlt._dataset( + _dataset( destination=populated_pipeline.destination, dataset_name=populated_pipeline.dataset_name, ), @@ -756,7 +839,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: # check that there is no error when creating dataset without schema table dataset = cast( ReadableDBAPIDataset, - dlt._dataset( + _dataset( destination=populated_pipeline.destination, dataset_name="unknown_dataset", ), @@ -779,7 +862,7 @@ def test_standalone_dataset(populated_pipeline: Pipeline) -> None: dataset = cast( ReadableDBAPIDataset, - dlt._dataset( + _dataset( destination=populated_pipeline.destination, dataset_name=populated_pipeline.dataset_name, ), diff --git a/tests/pipeline/test_dlt_versions.py b/tests/pipeline/test_dlt_versions.py index fbd4d412b3..51de3e0f76 100644 --- a/tests/pipeline/test_dlt_versions.py +++ b/tests/pipeline/test_dlt_versions.py @@ -538,5 +538,5 @@ def test_normalize_path_separator_legacy_behavior(test_storage: FileStorage) -> "_dlt_load_id", } # datasets must be the same - data_ = pipeline._dataset().issues_2.select("issue_id", "id").fetchall() + data_ = pipeline.dataset().issues_2.select("issue_id", "id").fetchall() print(data_) diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index b32854b110..2d72e23462 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -1754,7 +1754,7 @@ def test_column_name_with_break_path() -> None: # get data assert_data_table_counts(pipeline, {"custom__path": 1}) # get data via dataset with dbapi - data_ = pipeline._dataset().custom__path[["example_custom_field__c", "reg_c"]].fetchall() + data_ = pipeline.dataset().custom__path[["example_custom_field__c", "reg_c"]].fetchall() assert data_ == [("custom", "c")] @@ -1778,7 +1778,7 @@ def test_column_name_with_break_path_legacy() -> None: # get data assert_data_table_counts(pipeline, {"custom_path": 1}) # get data via dataset with dbapi - data_ = pipeline._dataset().custom_path[["example_custom_field_c", "reg_c"]].fetchall() + data_ = pipeline.dataset().custom_path[["example_custom_field_c", "reg_c"]].fetchall() assert data_ == [("custom", "c")] @@ -1806,7 +1806,7 @@ def flattened_dict(): assert table["columns"]["value__timestamp"]["data_type"] == "timestamp" # make sure data is there - data_ = pipeline._dataset().flattened__dict[["delta", "value__timestamp"]].limit(1).fetchall() + data_ = pipeline.dataset().flattened__dict[["delta", "value__timestamp"]].limit(1).fetchall() assert data_ == [(0, now)] @@ -1836,7 +1836,7 @@ def flattened_dict(): assert set(table["columns"]) == {"delta", "value__timestamp", "_dlt_id", "_dlt_load_id"} assert table["columns"]["value__timestamp"]["data_type"] == "timestamp" # make sure data is there - data_ = pipeline._dataset().flattened_dict[["delta", "value__timestamp"]].limit(1).fetchall() + data_ = pipeline.dataset().flattened_dict[["delta", "value__timestamp"]].limit(1).fetchall() assert data_ == [(0, now)] diff --git a/tests/pipeline/test_pipeline_extra.py b/tests/pipeline/test_pipeline_extra.py index a51052d247..32b16c234f 100644 --- a/tests/pipeline/test_pipeline_extra.py +++ b/tests/pipeline/test_pipeline_extra.py @@ -521,7 +521,7 @@ def test_parquet_with_flattened_columns() -> None: assert "issue__reactions__url" in pipeline.default_schema.tables["events"]["columns"] assert "issue_reactions_url" not in pipeline.default_schema.tables["events"]["columns"] - events_table = pipeline._dataset().events.arrow() + events_table = pipeline.dataset().events.arrow() assert "issue__reactions__url" in events_table.schema.names assert "issue_reactions_url" not in events_table.schema.names @@ -536,7 +536,7 @@ def test_parquet_with_flattened_columns() -> None: info = pipeline.run(events_table, table_name="events", loader_file_format="parquet") assert_load_info(info) - events_table_new = pipeline._dataset().events.arrow() + events_table_new = pipeline.dataset().events.arrow() assert events_table.schema == events_table_new.schema # double row count assert events_table.num_rows * 2 == events_table_new.num_rows From 4a051b0c99ddd93b3adf4d46000b1bcdbec6ac31 Mon Sep 17 00:00:00 2001 From: David Scharf Date: Mon, 16 Dec 2024 13:30:11 +0100 Subject: [PATCH 15/23] fix ibis az problems on linux (#2135) * try to fix ibis az problems on linux * remove duckdb certs fix * test explicitely setting transport options * sets the ssl curl on a correct connection clone --------- Co-authored-by: Marcin Rudolf --- .github/workflows/test_destinations.yml | 3 --- dlt/helpers/ibis.py | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test_destinations.yml b/.github/workflows/test_destinations.yml index 84a8f95d71..a9306c2f9c 100644 --- a/.github/workflows/test_destinations.yml +++ b/.github/workflows/test_destinations.yml @@ -79,9 +79,6 @@ jobs: - name: Install dependencies run: poetry install --no-interaction -E redshift -E postgis -E postgres -E gs -E s3 -E az -E parquet -E duckdb -E cli -E filesystem --with sentry-sdk --with pipeline,ibis -E deltalake -E pyiceberg - - name: enable certificates for azure and duckdb - run: sudo mkdir -p /etc/pki/tls/certs && sudo ln -s /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt - - name: Upgrade sqlalchemy run: poetry run pip install sqlalchemy==2.0.18 # minimum version required by `pyiceberg` diff --git a/dlt/helpers/ibis.py b/dlt/helpers/ibis.py index ed4264dac7..e15bb9bc16 100644 --- a/dlt/helpers/ibis.py +++ b/dlt/helpers/ibis.py @@ -10,7 +10,7 @@ import sqlglot from ibis import BaseBackend, Expr except ModuleNotFoundError: - raise MissingDependencyException("dlt ibis Helpers", ["ibis"]) + raise MissingDependencyException("dlt ibis helpers", ["ibis-framework"]) SUPPORTED_DESTINATIONS = [ @@ -123,18 +123,22 @@ def create_ibis_backend( ) from dlt.destinations.impl.duckdb.factory import DuckDbCredentials - # we create an in memory duckdb and create all tables on there - duck = duckdb.connect(":memory:") + # we create an in memory duckdb and create the ibis backend from it fs_client = cast(FilesystemClient, client) - creds = DuckDbCredentials(duck) sql_client = FilesystemSqlClient( - fs_client, dataset_name=fs_client.dataset_name, credentials=creds + fs_client, + dataset_name=fs_client.dataset_name, + credentials=DuckDbCredentials(duckdb.connect()), ) - + # do not use context manager to not return and close the cloned connection + duckdb_conn = sql_client.open_connection() + # make all tables available here # NOTE: we should probably have the option for the user to only select a subset of tables here - with sql_client as _: - sql_client.create_views_for_all_tables() - con = ibis.duckdb.from_connection(duck) + sql_client.create_views_for_all_tables() + # why this works now: whenever a clone of connection is made, all SET commands + # apply only to it. old code was setting `curl` on the internal clone of sql_client + # now we export this clone directly to ibis to it works + con = ibis.duckdb.from_connection(duckdb_conn) return con From 1b0d7b24e7b7dfb734f5bd13d8854600d2f630a2 Mon Sep 17 00:00:00 2001 From: rudolfix Date: Mon, 16 Dec 2024 20:34:22 +0100 Subject: [PATCH 16/23] allows data type diff and ensures valid migration separately (#2150) * allows data type diff and ensures valid migration separately * removes dlt init flag to skip core sources, adds flag to eject core source --- dlt/cli/command_wrappers.py | 4 +- dlt/cli/init_command.py | 31 ++-- dlt/cli/pipeline_files.py | 40 ++-- dlt/cli/plugins.py | 10 +- dlt/common/schema/utils.py | 70 ++++--- dlt/normalize/normalize.py | 40 +--- dlt/normalize/validate.py | 20 +- .../docs/reference/command-line-interface.md | 13 ++ tests/cli/test_init_command.py | 34 ++-- tests/common/schema/test_inference.py | 21 --- tests/common/schema/test_merges.py | 11 +- tests/load/pipeline/test_pipelines.py | 1 - tests/load/pipeline/test_postgres.py | 171 ------------------ tests/normalize/test_normalize.py | 70 +++++++ tests/pipeline/test_import_export_schema.py | 4 +- tests/pipeline/test_pipeline.py | 170 ++++++++++++++++- 16 files changed, 399 insertions(+), 311 deletions(-) diff --git a/dlt/cli/command_wrappers.py b/dlt/cli/command_wrappers.py index 0e6491688e..847b5daabb 100644 --- a/dlt/cli/command_wrappers.py +++ b/dlt/cli/command_wrappers.py @@ -43,14 +43,14 @@ def init_command_wrapper( destination_type: str, repo_location: str, branch: str, - omit_core_sources: bool = False, + eject_source: bool = False, ) -> None: init_command( source_name, destination_type, repo_location, branch, - omit_core_sources, + eject_source, ) diff --git a/dlt/cli/init_command.py b/dlt/cli/init_command.py index ac8adcc588..e81fa80c36 100644 --- a/dlt/cli/init_command.py +++ b/dlt/cli/init_command.py @@ -157,7 +157,7 @@ def _list_core_sources() -> Dict[str, SourceConfiguration]: sources: Dict[str, SourceConfiguration] = {} for source_name in files_ops.get_sources_names(core_sources_storage, source_type="core"): sources[source_name] = files_ops.get_core_source_configuration( - core_sources_storage, source_name + core_sources_storage, source_name, eject_source=False ) return sources @@ -295,7 +295,7 @@ def init_command( destination_type: str, repo_location: str, branch: str = None, - omit_core_sources: bool = False, + eject_source: bool = False, ) -> None: # try to import the destination and get config spec destination_reference = Destination.from_reference(destination_type) @@ -310,13 +310,9 @@ def init_command( # discover type of source source_type: files_ops.TSourceType = "template" - if ( - source_name in files_ops.get_sources_names(core_sources_storage, source_type="core") - ) and not omit_core_sources: + if source_name in files_ops.get_sources_names(core_sources_storage, source_type="core"): source_type = "core" else: - if omit_core_sources: - fmt.echo("Omitting dlt core sources.") verified_sources_storage = _clone_and_get_verified_sources_storage(repo_location, branch) if source_name in files_ops.get_sources_names( verified_sources_storage, source_type="verified" @@ -380,7 +376,7 @@ def init_command( else: if source_type == "core": source_configuration = files_ops.get_core_source_configuration( - core_sources_storage, source_name + core_sources_storage, source_name, eject_source ) from importlib.metadata import Distribution @@ -392,6 +388,9 @@ def init_command( if canonical_source_name in extras: source_configuration.requirements.update_dlt_extras(canonical_source_name) + + # create remote modified index to copy files when ejecting + remote_modified = {file_name: None for file_name in source_configuration.files} else: if not is_valid_schema_name(source_name): raise InvalidSchemaName(source_name) @@ -536,11 +535,17 @@ def init_command( "Creating a new pipeline with the dlt core source %s (%s)" % (fmt.bold(source_name), source_configuration.doc) ) - fmt.echo( - "NOTE: Beginning with dlt 1.0.0, the source %s will no longer be copied from the" - " verified sources repo but imported from dlt.sources. You can provide the" - " --omit-core-sources flag to revert to the old behavior." % (fmt.bold(source_name)) - ) + if eject_source: + fmt.echo( + "NOTE: Source code of %s will be ejected. Remember to modify the pipeline " + "example script to import the ejected source." % (fmt.bold(source_name)) + ) + else: + fmt.echo( + "NOTE: Beginning with dlt 1.0.0, the source %s will no longer be copied from" + " the verified sources repo but imported from dlt.sources. You can provide the" + " --eject flag to revert to the old behavior." % (fmt.bold(source_name)) + ) elif source_configuration.source_type == "verified": fmt.echo( "Creating and configuring a new pipeline with the verified source %s (%s)" diff --git a/dlt/cli/pipeline_files.py b/dlt/cli/pipeline_files.py index b6f8f85271..c0139fe2a7 100644 --- a/dlt/cli/pipeline_files.py +++ b/dlt/cli/pipeline_files.py @@ -226,11 +226,31 @@ def get_template_configuration( ) +def _get_source_files(sources_storage: FileStorage, source_name: str) -> List[str]: + """Get all files that belong to source `source_name`""" + files: List[str] = [] + for root, subdirs, _files in os.walk(sources_storage.make_full_path(source_name)): + # filter unwanted files + for subdir in list(subdirs): + if any(fnmatch.fnmatch(subdir, ignore) for ignore in IGNORE_FILES): + subdirs.remove(subdir) + rel_root = sources_storage.to_relative_path(root) + files.extend( + [ + os.path.join(rel_root, file) + for file in _files + if all(not fnmatch.fnmatch(file, ignore) for ignore in IGNORE_FILES) + ] + ) + return files + + def get_core_source_configuration( - sources_storage: FileStorage, source_name: str + sources_storage: FileStorage, source_name: str, eject_source: bool ) -> SourceConfiguration: src_pipeline_file = CORE_SOURCE_TEMPLATE_MODULE_NAME + "/" + source_name + PIPELINE_FILE_SUFFIX dest_pipeline_file = source_name + PIPELINE_FILE_SUFFIX + files: List[str] = _get_source_files(sources_storage, source_name) if eject_source else [] return SourceConfiguration( "core", @@ -238,7 +258,7 @@ def get_core_source_configuration( sources_storage, src_pipeline_file, dest_pipeline_file, - [".gitignore"], + files, SourceRequirements([]), _get_docstring_for_module(sources_storage, source_name), False, @@ -259,21 +279,7 @@ def get_verified_source_configuration( f"Pipeline example script {example_script} could not be found in the repository", source_name, ) - # get all files recursively - files: List[str] = [] - for root, subdirs, _files in os.walk(sources_storage.make_full_path(source_name)): - # filter unwanted files - for subdir in list(subdirs): - if any(fnmatch.fnmatch(subdir, ignore) for ignore in IGNORE_FILES): - subdirs.remove(subdir) - rel_root = sources_storage.to_relative_path(root) - files.extend( - [ - os.path.join(rel_root, file) - for file in _files - if all(not fnmatch.fnmatch(file, ignore) for ignore in IGNORE_FILES) - ] - ) + files = _get_source_files(sources_storage, source_name) # read requirements requirements_path = os.path.join(source_name, utils.REQUIREMENTS_TXT) if sources_storage.has_file(requirements_path): diff --git a/dlt/cli/plugins.py b/dlt/cli/plugins.py index cc2d4594b9..1712efbbd7 100644 --- a/dlt/cli/plugins.py +++ b/dlt/cli/plugins.py @@ -84,14 +84,10 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: ) parser.add_argument( - "--omit-core-sources", + "--eject", default=False, action="store_true", - help=( - "When present, will not create the new pipeline with a core source of the given" - " name but will take a source of this name from the default or provided" - " location." - ), + help="Ejects the source code of the core source like sql_database", ) def execute(self, args: argparse.Namespace) -> None: @@ -107,7 +103,7 @@ def execute(self, args: argparse.Namespace) -> None: args.destination, args.location, args.branch, - args.omit_core_sources, + args.eject, ) diff --git a/dlt/common/schema/utils.py b/dlt/common/schema/utils.py index 038abdc4d0..4f9e0eb42e 100644 --- a/dlt/common/schema/utils.py +++ b/dlt/common/schema/utils.py @@ -457,16 +457,8 @@ def diff_table( * when columns with the same name have different data types * when table links to different parent tables """ - if tab_a["name"] != tab_b["name"]: - raise TablePropertiesConflictException( - schema_name, tab_a["name"], "name", tab_a["name"], tab_b["name"] - ) - table_name = tab_a["name"] - # check if table properties can be merged - if tab_a.get("parent") != tab_b.get("parent"): - raise TablePropertiesConflictException( - schema_name, table_name, "parent", tab_a.get("parent"), tab_b.get("parent") - ) + # allow for columns to differ + ensure_compatible_tables(schema_name, tab_a, tab_b, ensure_columns=False) # get new columns, changes in the column data type or other properties are not allowed tab_a_columns = tab_a["columns"] @@ -474,18 +466,6 @@ def diff_table( for col_b_name, col_b in tab_b["columns"].items(): if col_b_name in tab_a_columns: col_a = tab_a_columns[col_b_name] - # we do not support changing data types of columns - if is_complete_column(col_a) and is_complete_column(col_b): - if not compare_complete_columns(tab_a_columns[col_b_name], col_b): - # attempt to update to incompatible columns - raise CannotCoerceColumnException( - schema_name, - table_name, - col_b_name, - col_b["data_type"], - tab_a_columns[col_b_name]["data_type"], - None, - ) # all other properties can change merged_column = merge_column(copy(col_a), col_b) if merged_column != col_a: @@ -494,6 +474,8 @@ def diff_table( new_columns.append(col_b) # return partial table containing only name and properties that differ (column, filters etc.) + table_name = tab_a["name"] + partial_table: TPartialTableSchema = { "name": table_name, "columns": {} if new_columns is None else {c["name"]: c for c in new_columns}, @@ -519,6 +501,50 @@ def diff_table( return partial_table +def ensure_compatible_tables( + schema_name: str, tab_a: TTableSchema, tab_b: TPartialTableSchema, ensure_columns: bool = True +) -> None: + """Ensures that `tab_a` and `tab_b` can be merged without conflicts. Conflicts are detected when + + - tables have different names + - nested tables have different parents + - tables have any column with incompatible types + + Note: all the identifiers must be already normalized + + """ + if tab_a["name"] != tab_b["name"]: + raise TablePropertiesConflictException( + schema_name, tab_a["name"], "name", tab_a["name"], tab_b["name"] + ) + table_name = tab_a["name"] + # check if table properties can be merged + if tab_a.get("parent") != tab_b.get("parent"): + raise TablePropertiesConflictException( + schema_name, table_name, "parent", tab_a.get("parent"), tab_b.get("parent") + ) + + if not ensure_columns: + return + + tab_a_columns = tab_a["columns"] + for col_b_name, col_b in tab_b["columns"].items(): + if col_b_name in tab_a_columns: + col_a = tab_a_columns[col_b_name] + # we do not support changing data types of columns + if is_complete_column(col_a) and is_complete_column(col_b): + if not compare_complete_columns(tab_a_columns[col_b_name], col_b): + # attempt to update to incompatible columns + raise CannotCoerceColumnException( + schema_name, + table_name, + col_b_name, + col_b["data_type"], + tab_a_columns[col_b_name]["data_type"], + None, + ) + + # def compare_tables(tab_a: TTableSchema, tab_b: TTableSchema) -> bool: # try: # table_name = tab_a["name"] diff --git a/dlt/normalize/normalize.py b/dlt/normalize/normalize.py index 32db5034b4..1d81d70b10 100644 --- a/dlt/normalize/normalize.py +++ b/dlt/normalize/normalize.py @@ -20,7 +20,7 @@ LoadStorage, ParsedLoadJobFileName, ) -from dlt.common.schema import TSchemaUpdate, Schema +from dlt.common.schema import Schema from dlt.common.schema.exceptions import CannotCoerceColumnException from dlt.common.pipeline import ( NormalizeInfo, @@ -34,7 +34,7 @@ from dlt.normalize.configuration import NormalizeConfiguration from dlt.normalize.exceptions import NormalizeJobFailed from dlt.normalize.worker import w_normalize_files, group_worker_files, TWorkerRV -from dlt.normalize.validate import verify_normalized_table +from dlt.normalize.validate import validate_and_update_schema, verify_normalized_table # normalize worker wrapping function signature @@ -80,16 +80,6 @@ def create_storages(self) -> None: config=self.config._load_storage_config, ) - def update_schema(self, schema: Schema, schema_updates: List[TSchemaUpdate]) -> None: - for schema_update in schema_updates: - for table_name, table_updates in schema_update.items(): - logger.info( - f"Updating schema for table {table_name} with {len(table_updates)} deltas" - ) - for partial_table in table_updates: - # merge columns where we expect identifiers to be normalized - schema.update_table(partial_table, normalize_identifiers=False) - def map_parallel(self, schema: Schema, load_id: str, files: Sequence[str]) -> TWorkerRV: workers: int = getattr(self.pool, "_max_workers", 1) chunk_files = group_worker_files(files, workers) @@ -123,7 +113,7 @@ def map_parallel(self, schema: Schema, load_id: str, files: Sequence[str]) -> TW result: TWorkerRV = pending.result() try: # gather schema from all manifests, validate consistency and combine - self.update_schema(schema, result[0]) + validate_and_update_schema(schema, result[0]) summary.schema_updates.extend(result.schema_updates) summary.file_metrics.extend(result.file_metrics) # update metrics @@ -162,7 +152,7 @@ def map_single(self, schema: Schema, load_id: str, files: Sequence[str]) -> TWor load_id, files, ) - self.update_schema(schema, result.schema_updates) + validate_and_update_schema(schema, result.schema_updates) self.collector.update("Files", len(result.file_metrics)) self.collector.update( "Items", sum(result.file_metrics, EMPTY_DATA_WRITER_METRICS).items_count @@ -237,23 +227,11 @@ def spool_schema_files(self, load_id: str, schema: Schema, files: Sequence[str]) self.load_storage.import_extracted_package( load_id, self.normalize_storage.extracted_packages ) - logger.info(f"Created new load package {load_id} on loading volume") - try: - # process parallel - self.spool_files( - load_id, schema.clone(update_normalizers=True), self.map_parallel, files - ) - except CannotCoerceColumnException as exc: - # schema conflicts resulting from parallel executing - logger.warning( - f"Parallel schema update conflict, switching to single thread ({str(exc)}" - ) - # start from scratch - self.load_storage.new_packages.delete_package(load_id) - self.load_storage.import_extracted_package( - load_id, self.normalize_storage.extracted_packages - ) - self.spool_files(load_id, schema.clone(update_normalizers=True), self.map_single, files) + logger.info(f"Created new load package {load_id} on loading volume with ") + # get number of workers with default == 1 if not set (ie. NullExecutor) + workers: int = getattr(self.pool, "_max_workers", 1) + map_f: TMapFuncType = self.map_parallel if workers > 1 else self.map_single + self.spool_files(load_id, schema.clone(update_normalizers=True), map_f, files) return load_id diff --git a/dlt/normalize/validate.py b/dlt/normalize/validate.py index 648deb5da9..868ba3115b 100644 --- a/dlt/normalize/validate.py +++ b/dlt/normalize/validate.py @@ -1,7 +1,10 @@ +from typing import List + from dlt.common.destination.capabilities import DestinationCapabilitiesContext from dlt.common.schema import Schema -from dlt.common.schema.typing import TTableSchema +from dlt.common.schema.typing import TTableSchema, TSchemaUpdate from dlt.common.schema.utils import ( + ensure_compatible_tables, find_incomplete_columns, get_first_column_name_with_prop, is_nested_table, @@ -10,6 +13,21 @@ from dlt.common import logger +def validate_and_update_schema(schema: Schema, schema_updates: List[TSchemaUpdate]) -> None: + """Updates `schema` tables with partial tables in `schema_updates`""" + for schema_update in schema_updates: + for table_name, table_updates in schema_update.items(): + logger.info(f"Updating schema for table {table_name} with {len(table_updates)} deltas") + for partial_table in table_updates: + # ensure updates will pass + if existing_table := schema.tables.get(partial_table["name"]): + ensure_compatible_tables(schema.name, existing_table, partial_table) + + for partial_table in table_updates: + # merge columns where we expect identifiers to be normalized + schema.update_table(partial_table, normalize_identifiers=False) + + def verify_normalized_table( schema: Schema, table: TTableSchema, capabilities: DestinationCapabilitiesContext ) -> None: diff --git a/docs/website/docs/reference/command-line-interface.md b/docs/website/docs/reference/command-line-interface.md index 825d33d548..2af750f43c 100644 --- a/docs/website/docs/reference/command-line-interface.md +++ b/docs/website/docs/reference/command-line-interface.md @@ -20,9 +20,22 @@ This command creates a new dlt pipeline script that loads data from `source` to This command can be used several times in the same folder to add more sources, destinations, and pipelines. It will also update the verified source code to the newest version if run again with an existing `source` name. You are warned if files will be overwritten or if the `dlt` version needs an upgrade to run a particular pipeline. +### Ejecting source code of the core sources like `sql_database`. +We merged a few sources to the core library. You can still eject source code and hack them with the `--eject` flag: +```sh +dlt init sql_database duckdb --eject +``` +will copy the source code of `sql_database` to your project. Remember to modify the pipeline example script to import from the local folder! + ### Specify your own "verified sources" repository You can use the `--location ` option to specify your own repository with sources. Typically, you would [fork ours](https://github.com/dlt-hub/verified-sources) and start customizing and adding sources, e.g., to use them for your team or organization. You can also specify a branch with `--branch `, e.g., to test a version being developed. +### Using dlt 0.5.x sources +Use `--branch 0.5` if you are still on `dlt` `0.5.x` ie. +```sh +dlt init --branch 0.5 +``` + ### List all sources ```sh dlt init --list-sources diff --git a/tests/cli/test_init_command.py b/tests/cli/test_init_command.py index 8e1affd164..d81cd8c858 100644 --- a/tests/cli/test_init_command.py +++ b/tests/cli/test_init_command.py @@ -262,6 +262,21 @@ def test_init_all_sources_isolated(cloned_init_repo: FileStorage) -> None: assert_index_version_constraint(files, candidate) +def test_init_core_sources_ejected(cloned_init_repo: FileStorage) -> None: + repo_dir = get_repo_dir(cloned_init_repo) + # ensure we test both sources form verified sources and core sources + source_candidates = set(CORE_SOURCES) + for candidate in source_candidates: + clean_test_storage() + repo_dir = get_repo_dir(cloned_init_repo) + files = get_project_files(clear_all_sources=False) + with set_working_dir(files.storage_path): + init_command.init_command(candidate, "bigquery", repo_dir, eject_source=True) + assert_requirements_txt(files, "bigquery") + # check if files copied + assert files.has_folder(candidate) + + @pytest.mark.parametrize("destination_name", IMPLEMENTED_DESTINATIONS) def test_init_all_destinations( destination_name: str, project_files: FileStorage, repo_dir: str @@ -279,25 +294,6 @@ def test_custom_destination_note(repo_dir: str, project_files: FileStorage): assert "to add a destination function that will consume your data" in _out -@pytest.mark.parametrize("omit", [True, False]) -# this will break if we have new core sources that are not in verified sources anymore -@pytest.mark.parametrize("source", set(CORE_SOURCES) - {"rest_api"}) -def test_omit_core_sources( - source: str, omit: bool, project_files: FileStorage, repo_dir: str -) -> None: - with io.StringIO() as buf, contextlib.redirect_stdout(buf): - init_command.init_command(source, "destination", repo_dir, omit_core_sources=omit) - _out = buf.getvalue() - - # check messaging - assert ("Omitting dlt core sources" in _out) == omit - assert ("will no longer be copied from the" in _out) == (not omit) - - # if we omit core sources, there will be a folder with the name of the source from the verified sources repo - assert project_files.has_folder(source) == omit - assert (f"dlt.sources.{source}" in project_files.load(f"{source}_pipeline.py")) == (not omit) - - def test_init_code_update_index_diff(repo_dir: str, project_files: FileStorage) -> None: sources_storage = FileStorage(os.path.join(repo_dir, SOURCES_MODULE_NAME)) new_content = '"""New docstrings"""' diff --git a/tests/common/schema/test_inference.py b/tests/common/schema/test_inference.py index 7f06cdb71e..adbb34b1f0 100644 --- a/tests/common/schema/test_inference.py +++ b/tests/common/schema/test_inference.py @@ -441,27 +441,6 @@ def test_update_schema_table_prop_conflict(schema: Schema) -> None: assert exc_val.value.val2 == "tab_parent" -def test_update_schema_column_conflict(schema: Schema) -> None: - tab1 = utils.new_table( - "tab1", - write_disposition="append", - columns=[ - {"name": "col1", "data_type": "text", "nullable": False}, - ], - ) - schema.update_table(tab1) - tab1_u1 = deepcopy(tab1) - # simulate column that had other datatype inferred - tab1_u1["columns"]["col1"]["data_type"] = "bool" - with pytest.raises(CannotCoerceColumnException) as exc_val: - schema.update_table(tab1_u1) - assert exc_val.value.column_name == "col1" - assert exc_val.value.from_type == "bool" - assert exc_val.value.to_type == "text" - # whole column mismatch - assert exc_val.value.coerced_value is None - - def _add_preferred_types(schema: Schema) -> None: schema._settings["preferred_types"] = {} schema._settings["preferred_types"][TSimpleRegex("timestamp")] = "timestamp" diff --git a/tests/common/schema/test_merges.py b/tests/common/schema/test_merges.py index 8e0c350e7c..b76fe944b5 100644 --- a/tests/common/schema/test_merges.py +++ b/tests/common/schema/test_merges.py @@ -353,7 +353,7 @@ def test_diff_tables() -> None: assert "test" in partial["columns"] -def test_diff_tables_conflicts() -> None: +def test_tables_conflicts() -> None: # conflict on parents table: TTableSchema = { # type: ignore[typeddict-unknown-key] "name": "table", @@ -366,6 +366,8 @@ def test_diff_tables_conflicts() -> None: other = utils.new_table("table") with pytest.raises(TablePropertiesConflictException) as cf_ex: utils.diff_table("schema", table, other) + with pytest.raises(TablePropertiesConflictException) as cf_ex: + utils.ensure_compatible_tables("schema", table, other) assert cf_ex.value.table_name == "table" assert cf_ex.value.prop_name == "parent" @@ -373,6 +375,8 @@ def test_diff_tables_conflicts() -> None: other = utils.new_table("other_name") with pytest.raises(TablePropertiesConflictException) as cf_ex: utils.diff_table("schema", table, other) + with pytest.raises(TablePropertiesConflictException) as cf_ex: + utils.ensure_compatible_tables("schema", table, other) assert cf_ex.value.table_name == "table" assert cf_ex.value.prop_name == "name" @@ -380,7 +384,10 @@ def test_diff_tables_conflicts() -> None: changed = deepcopy(table) changed["columns"]["test"]["data_type"] = "bigint" with pytest.raises(CannotCoerceColumnException): - utils.diff_table("schema", table, changed) + utils.ensure_compatible_tables("schema", table, changed) + # but diff now accepts different data types + merged_table = utils.diff_table("schema", table, changed) + assert merged_table["columns"]["test"]["data_type"] == "bigint" def test_merge_tables() -> None: diff --git a/tests/load/pipeline/test_pipelines.py b/tests/load/pipeline/test_pipelines.py index 9190225a8c..b998b78471 100644 --- a/tests/load/pipeline/test_pipelines.py +++ b/tests/load/pipeline/test_pipelines.py @@ -10,7 +10,6 @@ from dlt.common.pipeline import SupportsPipeline from dlt.common.destination import Destination from dlt.common.destination.reference import WithStagingDataset -from dlt.common.schema.exceptions import CannotCoerceColumnException from dlt.common.schema.schema import Schema from dlt.common.schema.typing import VERSION_TABLE_NAME from dlt.common.schema.utils import new_table diff --git a/tests/load/pipeline/test_postgres.py b/tests/load/pipeline/test_postgres.py index 29ad21941e..e09582f8a8 100644 --- a/tests/load/pipeline/test_postgres.py +++ b/tests/load/pipeline/test_postgres.py @@ -127,177 +127,6 @@ def test_pipeline_explicit_destination_credentials( ) -# do not remove - it allows us to filter tests by destination -@pytest.mark.parametrize( - "destination_config", - destinations_configs(default_sql_configs=True, subset=["postgres"]), - ids=lambda x: x.name, -) -def test_pipeline_with_sources_sharing_schema( - destination_config: DestinationTestConfiguration, -) -> None: - schema = Schema("shared") - - @dlt.source(schema=schema, max_table_nesting=1) - def source_1(): - @dlt.resource(primary_key="user_id") - def gen1(): - dlt.current.source_state()["source_1"] = True - dlt.current.resource_state()["source_1"] = True - yield {"id": "Y", "user_id": "user_y"} - - @dlt.resource(columns={"col": {"data_type": "bigint"}}) - def conflict(): - yield "conflict" - - return gen1, conflict - - @dlt.source(schema=schema, max_table_nesting=2) - def source_2(): - @dlt.resource(primary_key="id") - def gen1(): - dlt.current.source_state()["source_2"] = True - dlt.current.resource_state()["source_2"] = True - yield {"id": "X", "user_id": "user_X"} - - def gen2(): - yield from "CDE" - - @dlt.resource(columns={"col": {"data_type": "bool"}}, selected=False) - def conflict(): - yield "conflict" - - return gen2, gen1, conflict - - # all selected tables with hints should be there - discover_1 = source_1().discover_schema() - assert "gen1" in discover_1.tables - assert discover_1.tables["gen1"]["columns"]["user_id"]["primary_key"] is True - assert "data_type" not in discover_1.tables["gen1"]["columns"]["user_id"] - assert "conflict" in discover_1.tables - assert discover_1.tables["conflict"]["columns"]["col"]["data_type"] == "bigint" - - discover_2 = source_2().discover_schema() - assert "gen1" in discover_2.tables - assert "gen2" in discover_2.tables - # conflict deselected - assert "conflict" not in discover_2.tables - - p = dlt.pipeline(pipeline_name="multi", destination="duckdb", dev_mode=True) - p.extract([source_1(), source_2()], table_format=destination_config.table_format) - default_schema = p.default_schema - gen1_table = default_schema.tables["gen1"] - assert "user_id" in gen1_table["columns"] - assert "id" in gen1_table["columns"] - assert "conflict" in default_schema.tables - assert "gen2" in default_schema.tables - p.normalize(loader_file_format=destination_config.file_format) - assert "gen2" in default_schema.tables - p.load() - table_names = [t["name"] for t in default_schema.data_tables()] - counts = load_table_counts(p, *table_names) - assert counts == {"gen1": 2, "gen2": 3, "conflict": 1} - # both sources share the same state - assert p.state["sources"] == { - "shared": { - "source_1": True, - "resources": {"gen1": {"source_1": True, "source_2": True}}, - "source_2": True, - } - } - drop_active_pipeline_data() - - # same pipeline but enable conflict - p = dlt.pipeline(pipeline_name="multi", destination="duckdb", dev_mode=True) - with pytest.raises(PipelineStepFailed) as py_ex: - p.extract([source_1(), source_2().with_resources("conflict")]) - assert isinstance(py_ex.value.__context__, CannotCoerceColumnException) - - -# do not remove - it allows us to filter tests by destination -@pytest.mark.parametrize( - "destination_config", - destinations_configs(default_sql_configs=True, subset=["postgres"]), - ids=lambda x: x.name, -) -def test_many_pipelines_single_dataset(destination_config: DestinationTestConfiguration) -> None: - schema = Schema("shared") - - @dlt.source(schema=schema, max_table_nesting=1) - def source_1(): - @dlt.resource(primary_key="user_id") - def gen1(): - dlt.current.source_state()["source_1"] = True - dlt.current.resource_state()["source_1"] = True - yield {"id": "Y", "user_id": "user_y"} - - return gen1 - - @dlt.source(schema=schema, max_table_nesting=2) - def source_2(): - @dlt.resource(primary_key="id") - def gen1(): - dlt.current.source_state()["source_2"] = True - dlt.current.resource_state()["source_2"] = True - yield {"id": "X", "user_id": "user_X"} - - def gen2(): - yield from "CDE" - - return gen2, gen1 - - # load source_1 to common dataset - p = dlt.pipeline( - pipeline_name="source_1_pipeline", destination="duckdb", dataset_name="shared_dataset" - ) - p.run(source_1(), credentials="duckdb:///_storage/test_quack.duckdb") - counts = load_table_counts(p, *p.default_schema.tables.keys()) - assert counts.items() >= {"gen1": 1, "_dlt_pipeline_state": 1, "_dlt_loads": 1}.items() - p._wipe_working_folder() - p.deactivate() - - p = dlt.pipeline( - pipeline_name="source_2_pipeline", destination="duckdb", dataset_name="shared_dataset" - ) - p.run(source_2(), credentials="duckdb:///_storage/test_quack.duckdb") - # table_names = [t["name"] for t in p.default_schema.data_tables()] - counts = load_table_counts(p, *p.default_schema.tables.keys()) - # gen1: one record comes from source_1, 1 record from source_2 - assert counts.items() >= {"gen1": 2, "_dlt_pipeline_state": 2, "_dlt_loads": 2}.items() - # assert counts == {'gen1': 2, 'gen2': 3} - p._wipe_working_folder() - p.deactivate() - - # restore from destination, check state - p = dlt.pipeline( - pipeline_name="source_1_pipeline", - destination=dlt.destinations.duckdb(credentials="duckdb:///_storage/test_quack.duckdb"), - dataset_name="shared_dataset", - ) - p.sync_destination() - # we have our separate state - assert p.state["sources"]["shared"] == { - "source_1": True, - "resources": {"gen1": {"source_1": True}}, - } - # but the schema was common so we have the earliest one - assert "gen2" in p.default_schema.tables - p._wipe_working_folder() - p.deactivate() - - p = dlt.pipeline( - pipeline_name="source_2_pipeline", - destination=dlt.destinations.duckdb(credentials="duckdb:///_storage/test_quack.duckdb"), - dataset_name="shared_dataset", - ) - p.sync_destination() - # we have our separate state - assert p.state["sources"]["shared"] == { - "source_2": True, - "resources": {"gen1": {"source_2": True}}, - } - - # TODO: uncomment and finalize when we implement encoding for psycopg2 # @pytest.mark.parametrize( # "destination_config", diff --git a/tests/normalize/test_normalize.py b/tests/normalize/test_normalize.py index 7463184be7..84e22af9ff 100644 --- a/tests/normalize/test_normalize.py +++ b/tests/normalize/test_normalize.py @@ -1,3 +1,4 @@ +from copy import deepcopy import pytest from fnmatch import fnmatch from typing import Dict, Iterator, List, Sequence, Tuple @@ -5,6 +6,7 @@ from dlt.common import json from dlt.common.destination.capabilities import TLoaderFileFormat +from dlt.common.schema.exceptions import CannotCoerceColumnException from dlt.common.schema.schema import Schema from dlt.common.schema.utils import new_table from dlt.common.storages.exceptions import SchemaNotFoundError @@ -16,6 +18,7 @@ from dlt.extract.extract import ExtractStorage from dlt.normalize import Normalize +from dlt.normalize.validate import validate_and_update_schema from dlt.normalize.worker import group_worker_files from dlt.normalize.exceptions import NormalizeJobFailed @@ -284,6 +287,8 @@ def test_multiprocessing_row_counting( extract_cases(raw_normalize, ["github.events.load_page_1_duck"]) # use real process pool in tests with ProcessPoolExecutor(max_workers=4) as p: + # test if we get correct number of workers + assert getattr(p, "_max_workers", None) == 4 raw_normalize.run(p) # get step info step_info = raw_normalize.get_step_info(MockPipeline("multiprocessing_pipeline", True)) # type: ignore[abstract] @@ -712,6 +717,71 @@ def assert_timestamp_data_type(load_storage: LoadStorage, data_type: TDataType) assert event_schema.get_table_columns("event")["timestamp"]["data_type"] == data_type +def test_update_schema_column_conflict(rasa_normalize: Normalize) -> None: + extract_cases( + rasa_normalize, + [ + "event.event.many_load_2", + "event.event.user_load_1", + ], + ) + extract_cases( + rasa_normalize, + [ + "ethereum.blocks.9c1d9b504ea240a482b007788d5cd61c_2", + ], + ) + # use real process pool in tests + with ProcessPoolExecutor(max_workers=4) as p: + rasa_normalize.run(p) + + schema = rasa_normalize.schema_storage.load_schema("event") + tab1 = new_table( + "event_user", + write_disposition="append", + columns=[ + {"name": "col1", "data_type": "text", "nullable": False}, + ], + ) + validate_and_update_schema(schema, [{"event_user": [deepcopy(tab1)]}]) + assert schema.tables["event_user"]["columns"]["col1"]["data_type"] == "text" + + tab1["columns"]["col1"]["data_type"] = "bool" + tab1["columns"]["col2"] = {"name": "col2", "data_type": "text", "nullable": False} + with pytest.raises(CannotCoerceColumnException) as exc_val: + validate_and_update_schema(schema, [{"event_user": [deepcopy(tab1)]}]) + assert exc_val.value.column_name == "col1" + assert exc_val.value.from_type == "bool" + assert exc_val.value.to_type == "text" + # whole column mismatch + assert exc_val.value.coerced_value is None + # make sure col2 is not added + assert "col2" not in schema.tables["event_user"]["columns"] + + # add two updates that are conflicting + tab2 = new_table( + "event_slot", + write_disposition="append", + columns=[ + {"name": "col1", "data_type": "text", "nullable": False}, + {"name": "col2", "data_type": "text", "nullable": False}, + ], + ) + tab3 = new_table( + "event_slot", + write_disposition="append", + columns=[ + {"name": "col1", "data_type": "bool", "nullable": False}, + ], + ) + with pytest.raises(CannotCoerceColumnException) as exc_val: + validate_and_update_schema( + schema, [{"event_slot": [deepcopy(tab2)]}, {"event_slot": [deepcopy(tab3)]}] + ) + # col2 is added from first update + assert "col2" in schema.tables["event_slot"]["columns"] + + def test_removal_of_normalizer_schema_section_and_add_seen_data(raw_normalize: Normalize) -> None: extract_cases( raw_normalize, diff --git a/tests/pipeline/test_import_export_schema.py b/tests/pipeline/test_import_export_schema.py index eb36d36ba3..5eb9c664d0 100644 --- a/tests/pipeline/test_import_export_schema.py +++ b/tests/pipeline/test_import_export_schema.py @@ -1,4 +1,4 @@ -import dlt, os, pytest +import dlt, os from dlt.common.utils import uniq_id @@ -6,8 +6,6 @@ from tests.utils import TEST_STORAGE_ROOT from dlt.common.schema import Schema from dlt.common.storages.schema_storage import SchemaStorage -from dlt.common.schema.exceptions import CannotCoerceColumnException -from dlt.pipeline.exceptions import PipelineStepFailed from dlt.destinations import dummy diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index 2d72e23462..95d464d48a 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -52,7 +52,7 @@ from dlt.pipeline.pipeline import Pipeline from tests.common.utils import TEST_SENTRY_DSN -from tests.utils import TEST_STORAGE_ROOT +from tests.utils import TEST_STORAGE_ROOT, load_table_counts from tests.extract.utils import expect_extracted_file from tests.pipeline.utils import ( assert_data_table_counts, @@ -3011,3 +3011,171 @@ def test_push_table_with_upfront_schema() -> None: copy_pipeline = dlt.pipeline(pipeline_name="push_table_copy_pipeline", destination="duckdb") info = copy_pipeline.run(data, table_name="events", schema=copy_schema) assert copy_pipeline.default_schema.version_hash != infer_hash + + +def test_pipeline_with_sources_sharing_schema() -> None: + schema = Schema("shared") + + @dlt.source(schema=schema, max_table_nesting=1) + def source_1(): + @dlt.resource(primary_key="user_id") + def gen1(): + dlt.current.source_state()["source_1"] = True + dlt.current.resource_state()["source_1"] = True + yield {"id": "Y", "user_id": "user_y"} + + @dlt.resource(columns={"value": {"data_type": "bool"}}) + def conflict(): + yield True + + return gen1, conflict + + @dlt.source(schema=schema, max_table_nesting=2) + def source_2(): + @dlt.resource(primary_key="id") + def gen1(): + dlt.current.source_state()["source_2"] = True + dlt.current.resource_state()["source_2"] = True + yield {"id": "X", "user_id": "user_X"} + + def gen2(): + yield from "CDE" + + @dlt.resource(columns={"value": {"data_type": "text"}}, selected=False) + def conflict(): + yield "indeed" + + return gen2, gen1, conflict + + # all selected tables with hints should be there + discover_1 = source_1().discover_schema() + assert "gen1" in discover_1.tables + assert discover_1.tables["gen1"]["columns"]["user_id"]["primary_key"] is True + assert "data_type" not in discover_1.tables["gen1"]["columns"]["user_id"] + assert "conflict" in discover_1.tables + assert discover_1.tables["conflict"]["columns"]["value"]["data_type"] == "bool" + + discover_2 = source_2().discover_schema() + assert "gen1" in discover_2.tables + assert "gen2" in discover_2.tables + # conflict deselected + assert "conflict" not in discover_2.tables + + p = dlt.pipeline(pipeline_name="multi", destination="duckdb", dev_mode=True) + p.extract([source_1(), source_2()]) + default_schema = p.default_schema + gen1_table = default_schema.tables["gen1"] + assert "user_id" in gen1_table["columns"] + assert "id" in gen1_table["columns"] + assert "conflict" in default_schema.tables + assert "gen2" in default_schema.tables + p.normalize() + assert "gen2" in default_schema.tables + assert default_schema.tables["conflict"]["columns"]["value"]["data_type"] == "bool" + p.load() + table_names = [t["name"] for t in default_schema.data_tables()] + counts = load_table_counts(p, *table_names) + assert counts == {"gen1": 2, "gen2": 3, "conflict": 1} + # both sources share the same state + assert p.state["sources"] == { + "shared": { + "source_1": True, + "resources": {"gen1": {"source_1": True, "source_2": True}}, + "source_2": True, + } + } + + # same pipeline but enable conflict + p.extract([source_2().with_resources("conflict")]) + p.normalize() + assert default_schema.tables["conflict"]["columns"]["value"]["data_type"] == "text" + with pytest.raises(PipelineStepFailed): + # will generate failed job on type that does not match + p.load() + counts = load_table_counts(p, "conflict") + assert counts == {"conflict": 1} + + # alter table in duckdb + with p.sql_client() as client: + client.execute_sql("ALTER TABLE conflict ALTER value TYPE VARCHAR;") + p.run([source_2().with_resources("conflict")]) + counts = load_table_counts(p, "conflict") + assert counts == {"conflict": 2} + + +def test_many_pipelines_single_dataset() -> None: + schema = Schema("shared") + + @dlt.source(schema=schema, max_table_nesting=1) + def source_1(): + @dlt.resource(primary_key="user_id") + def gen1(): + dlt.current.source_state()["source_1"] = True + dlt.current.resource_state()["source_1"] = True + yield {"id": "Y", "user_id": "user_y"} + + return gen1 + + @dlt.source(schema=schema, max_table_nesting=2) + def source_2(): + @dlt.resource(primary_key="id") + def gen1(): + dlt.current.source_state()["source_2"] = True + dlt.current.resource_state()["source_2"] = True + yield {"id": "X", "user_id": "user_X"} + + def gen2(): + yield from "CDE" + + return gen2, gen1 + + # load source_1 to common dataset + p = dlt.pipeline( + pipeline_name="source_1_pipeline", destination="duckdb", dataset_name="shared_dataset" + ) + p.run(source_1(), credentials="duckdb:///_storage/test_quack.duckdb") + counts = load_table_counts(p, *p.default_schema.tables.keys()) + assert counts.items() >= {"gen1": 1, "_dlt_pipeline_state": 1, "_dlt_loads": 1}.items() + p._wipe_working_folder() + p.deactivate() + + p = dlt.pipeline( + pipeline_name="source_2_pipeline", destination="duckdb", dataset_name="shared_dataset" + ) + p.run(source_2(), credentials="duckdb:///_storage/test_quack.duckdb") + # table_names = [t["name"] for t in p.default_schema.data_tables()] + counts = load_table_counts(p, *p.default_schema.tables.keys()) + # gen1: one record comes from source_1, 1 record from source_2 + assert counts.items() >= {"gen1": 2, "_dlt_pipeline_state": 2, "_dlt_loads": 2}.items() + # assert counts == {'gen1': 2, 'gen2': 3} + p._wipe_working_folder() + p.deactivate() + + # restore from destination, check state + p = dlt.pipeline( + pipeline_name="source_1_pipeline", + destination=dlt.destinations.duckdb(credentials="duckdb:///_storage/test_quack.duckdb"), + dataset_name="shared_dataset", + ) + p.sync_destination() + # we have our separate state + assert p.state["sources"]["shared"] == { + "source_1": True, + "resources": {"gen1": {"source_1": True}}, + } + # but the schema was common so we have the earliest one + assert "gen2" in p.default_schema.tables + p._wipe_working_folder() + p.deactivate() + + p = dlt.pipeline( + pipeline_name="source_2_pipeline", + destination=dlt.destinations.duckdb(credentials="duckdb:///_storage/test_quack.duckdb"), + dataset_name="shared_dataset", + ) + p.sync_destination() + # we have our separate state + assert p.state["sources"]["shared"] == { + "source_2": True, + "resources": {"gen1": {"source_2": True}}, + } From 268768f78bd7ea7b2df8ca0722faa72d4d4614c5 Mon Sep 17 00:00:00 2001 From: David Scharf Date: Mon, 16 Dec 2024 21:16:40 +0100 Subject: [PATCH 17/23] convert add_limit to pipe step based limiting (#2131) * convert add_limit to step based limiting * prevent late arriving items to be forwarded from limit add some convenience methods for pipe step management * added a few more tests for limit * add more limit functions from branch * remove rate-limiting * fix limiting bug and update docs * revert back to inserting validator step at the same position if replaced * make time limit tests more lenient for mac os tests * tmp * add test for testing incremental with limit * improve limit tests with parallelized case * add backfill example with sql_database * fix linting * remove extra file * only wrap iterators on demand * move items transform steps into extra file --- dlt/extract/exceptions.py | 1 - dlt/extract/hints.py | 3 +- dlt/extract/incremental/__init__.py | 3 +- dlt/extract/items.py | 119 +----------- dlt/extract/items_transform.py | 179 ++++++++++++++++++ dlt/extract/pipe.py | 20 +- dlt/extract/pipe_iterator.py | 8 +- dlt/extract/resource.py | 109 ++++------- dlt/extract/utils.py | 11 ++ dlt/extract/validation.py | 3 +- dlt/sources/helpers/transform.py | 2 +- docs/examples/backfill_in_chunks/__init__.py | 0 .../backfill_in_chunks/backfill_in_chunks.py | 85 +++++++++ docs/website/docs/general-usage/resource.md | 17 +- docs/website/docs/general-usage/source.md | 14 +- tests/extract/test_extract_pipe.py | 3 +- tests/extract/test_incremental.py | 58 +++++- tests/extract/test_sources.py | 78 +++++++- tests/extract/test_validation.py | 2 +- tests/extract/utils.py | 2 +- 20 files changed, 505 insertions(+), 212 deletions(-) create mode 100644 dlt/extract/items_transform.py create mode 100644 docs/examples/backfill_in_chunks/__init__.py create mode 100644 docs/examples/backfill_in_chunks/backfill_in_chunks.py diff --git a/dlt/extract/exceptions.py b/dlt/extract/exceptions.py index f4d2b1f302..e832833428 100644 --- a/dlt/extract/exceptions.py +++ b/dlt/extract/exceptions.py @@ -3,7 +3,6 @@ from dlt.common.exceptions import DltException from dlt.common.utils import get_callable_name -from dlt.extract.items import ValidateItem, TDataItems class ExtractorException(DltException): diff --git a/dlt/extract/hints.py b/dlt/extract/hints.py index 000e5c4cdb..22a0062acf 100644 --- a/dlt/extract/hints.py +++ b/dlt/extract/hints.py @@ -37,7 +37,8 @@ InconsistentTableTemplate, ) from dlt.extract.incremental import Incremental, TIncrementalConfig -from dlt.extract.items import TFunHintTemplate, TTableHintTemplate, TableNameMeta, ValidateItem +from dlt.extract.items import TFunHintTemplate, TTableHintTemplate, TableNameMeta +from dlt.extract.items_transform import ValidateItem from dlt.extract.utils import ensure_table_schema_columns, ensure_table_schema_columns_hint from dlt.extract.validation import create_item_validator diff --git a/dlt/extract/incremental/__init__.py b/dlt/extract/incremental/__init__.py index 5e7bae49c6..ce06292864 100644 --- a/dlt/extract/incremental/__init__.py +++ b/dlt/extract/incremental/__init__.py @@ -44,7 +44,8 @@ IncrementalArgs, TIncrementalRange, ) -from dlt.extract.items import SupportsPipe, TTableHintTemplate, ItemTransform +from dlt.extract.items import SupportsPipe, TTableHintTemplate +from dlt.extract.items_transform import ItemTransform from dlt.extract.incremental.transform import ( JsonIncremental, ArrowIncremental, diff --git a/dlt/extract/items.py b/dlt/extract/items.py index 888787e6b7..ad7447c163 100644 --- a/dlt/extract/items.py +++ b/dlt/extract/items.py @@ -1,21 +1,16 @@ -import inspect from abc import ABC, abstractmethod from typing import ( Any, Callable, - ClassVar, - Generic, Iterator, Iterable, Literal, Optional, Protocol, - TypeVar, Union, Awaitable, TYPE_CHECKING, NamedTuple, - Generator, ) from concurrent.futures import Future @@ -28,7 +23,6 @@ TDynHintType, ) - TDecompositionStrategy = Literal["none", "scc"] TDeferredDataItems = Callable[[], TDataItems] TAwaitableDataItems = Awaitable[TDataItems] @@ -113,6 +107,10 @@ def gen(self) -> TPipeStep: """A data generating step""" ... + def replace_gen(self, gen: TPipeStep) -> None: + """Replaces data generating step. Assumes that you know what are you doing""" + ... + def __getitem__(self, i: int) -> TPipeStep: """Get pipe step at index""" ... @@ -129,112 +127,3 @@ def has_parent(self) -> bool: def close(self) -> None: """Closes pipe generator""" ... - - -ItemTransformFunctionWithMeta = Callable[[TDataItem, str], TAny] -ItemTransformFunctionNoMeta = Callable[[TDataItem], TAny] -ItemTransformFunc = Union[ItemTransformFunctionWithMeta[TAny], ItemTransformFunctionNoMeta[TAny]] - - -class ItemTransform(ABC, Generic[TAny]): - _f_meta: ItemTransformFunctionWithMeta[TAny] = None - _f: ItemTransformFunctionNoMeta[TAny] = None - - placement_affinity: ClassVar[float] = 0 - """Tell how strongly an item sticks to start (-1) or end (+1) of pipe.""" - - def __init__(self, transform_f: ItemTransformFunc[TAny]) -> None: - # inspect the signature - sig = inspect.signature(transform_f) - # TODO: use TypeGuard here to get rid of type ignore - if len(sig.parameters) == 1: - self._f = transform_f # type: ignore - else: # TODO: do better check - self._f_meta = transform_f # type: ignore - - def bind(self: "ItemTransform[TAny]", pipe: SupportsPipe) -> "ItemTransform[TAny]": - return self - - @abstractmethod - def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: - """Transforms `item` (a list of TDataItem or a single TDataItem) and returns or yields TDataItems. Returns None to consume item (filter out)""" - pass - - -class FilterItem(ItemTransform[bool]): - # mypy needs those to type correctly - _f_meta: ItemTransformFunctionWithMeta[bool] - _f: ItemTransformFunctionNoMeta[bool] - - def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: - if isinstance(item, list): - # preserve empty lists - if len(item) == 0: - return item - - if self._f_meta: - item = [i for i in item if self._f_meta(i, meta)] - else: - item = [i for i in item if self._f(i)] - if not item: - # item was fully consumed by the filter - return None - return item - else: - if self._f_meta: - return item if self._f_meta(item, meta) else None - else: - return item if self._f(item) else None - - -class MapItem(ItemTransform[TDataItem]): - # mypy needs those to type correctly - _f_meta: ItemTransformFunctionWithMeta[TDataItem] - _f: ItemTransformFunctionNoMeta[TDataItem] - - def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: - if isinstance(item, list): - if self._f_meta: - return [self._f_meta(i, meta) for i in item] - else: - return [self._f(i) for i in item] - else: - if self._f_meta: - return self._f_meta(item, meta) - else: - return self._f(item) - - -class YieldMapItem(ItemTransform[Iterator[TDataItem]]): - # mypy needs those to type correctly - _f_meta: ItemTransformFunctionWithMeta[TDataItem] - _f: ItemTransformFunctionNoMeta[TDataItem] - - def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: - if isinstance(item, list): - for i in item: - if self._f_meta: - yield from self._f_meta(i, meta) - else: - yield from self._f(i) - else: - if self._f_meta: - yield from self._f_meta(item, meta) - else: - yield from self._f(item) - - -class ValidateItem(ItemTransform[TDataItem]): - """Base class for validators of data items. - - Subclass should implement the `__call__` method to either return the data item(s) or raise `extract.exceptions.ValidationError`. - See `PydanticValidator` for possible implementation. - """ - - placement_affinity: ClassVar[float] = 0.9 # stick to end but less than incremental - - table_name: str - - def bind(self, pipe: SupportsPipe) -> ItemTransform[TDataItem]: - self.table_name = pipe.name - return self diff --git a/dlt/extract/items_transform.py b/dlt/extract/items_transform.py new file mode 100644 index 0000000000..12375640bc --- /dev/null +++ b/dlt/extract/items_transform.py @@ -0,0 +1,179 @@ +import inspect +import time + +from abc import ABC, abstractmethod +from typing import ( + Any, + Callable, + ClassVar, + Generic, + Iterator, + Optional, + Union, +) +from concurrent.futures import Future + +from dlt.common.typing import ( + TAny, + TDataItem, + TDataItems, +) + +from dlt.extract.utils import ( + wrap_iterator, +) + +from dlt.extract.items import SupportsPipe + + +ItemTransformFunctionWithMeta = Callable[[TDataItem, str], TAny] +ItemTransformFunctionNoMeta = Callable[[TDataItem], TAny] +ItemTransformFunc = Union[ItemTransformFunctionWithMeta[TAny], ItemTransformFunctionNoMeta[TAny]] + + +class ItemTransform(ABC, Generic[TAny]): + _f_meta: ItemTransformFunctionWithMeta[TAny] = None + _f: ItemTransformFunctionNoMeta[TAny] = None + + placement_affinity: ClassVar[float] = 0 + """Tell how strongly an item sticks to start (-1) or end (+1) of pipe.""" + + def __init__(self, transform_f: ItemTransformFunc[TAny]) -> None: + # inspect the signature + sig = inspect.signature(transform_f) + # TODO: use TypeGuard here to get rid of type ignore + if len(sig.parameters) == 1: + self._f = transform_f # type: ignore + else: # TODO: do better check + self._f_meta = transform_f # type: ignore + + def bind(self: "ItemTransform[TAny]", pipe: SupportsPipe) -> "ItemTransform[TAny]": + return self + + @abstractmethod + def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: + """Transforms `item` (a list of TDataItem or a single TDataItem) and returns or yields TDataItems. Returns None to consume item (filter out)""" + pass + + +class FilterItem(ItemTransform[bool]): + # mypy needs those to type correctly + _f_meta: ItemTransformFunctionWithMeta[bool] + _f: ItemTransformFunctionNoMeta[bool] + + def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: + if isinstance(item, list): + # preserve empty lists + if len(item) == 0: + return item + + if self._f_meta: + item = [i for i in item if self._f_meta(i, meta)] + else: + item = [i for i in item if self._f(i)] + if not item: + # item was fully consumed by the filter + return None + return item + else: + if self._f_meta: + return item if self._f_meta(item, meta) else None + else: + return item if self._f(item) else None + + +class MapItem(ItemTransform[TDataItem]): + # mypy needs those to type correctly + _f_meta: ItemTransformFunctionWithMeta[TDataItem] + _f: ItemTransformFunctionNoMeta[TDataItem] + + def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: + if isinstance(item, list): + if self._f_meta: + return [self._f_meta(i, meta) for i in item] + else: + return [self._f(i) for i in item] + else: + if self._f_meta: + return self._f_meta(item, meta) + else: + return self._f(item) + + +class YieldMapItem(ItemTransform[Iterator[TDataItem]]): + # mypy needs those to type correctly + _f_meta: ItemTransformFunctionWithMeta[TDataItem] + _f: ItemTransformFunctionNoMeta[TDataItem] + + def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: + if isinstance(item, list): + for i in item: + if self._f_meta: + yield from self._f_meta(i, meta) + else: + yield from self._f(i) + else: + if self._f_meta: + yield from self._f_meta(item, meta) + else: + yield from self._f(item) + + +class ValidateItem(ItemTransform[TDataItem]): + """Base class for validators of data items. + + Subclass should implement the `__call__` method to either return the data item(s) or raise `extract.exceptions.ValidationError`. + See `PydanticValidator` for possible implementation. + """ + + placement_affinity: ClassVar[float] = 0.9 # stick to end but less than incremental + + table_name: str + + def bind(self, pipe: SupportsPipe) -> ItemTransform[TDataItem]: + self.table_name = pipe.name + return self + + +class LimitItem(ItemTransform[TDataItem]): + placement_affinity: ClassVar[float] = 1.1 # stick to end right behind incremental + + def __init__(self, max_items: Optional[int], max_time: Optional[float]) -> None: + self.max_items = max_items if max_items is not None else -1 + self.max_time = max_time + + def bind(self, pipe: SupportsPipe) -> "LimitItem": + # we also wrap iterators to make them stoppable + if isinstance(pipe.gen, Iterator): + pipe.replace_gen(wrap_iterator(pipe.gen)) + + self.gen = pipe.gen + self.count = 0 + self.exhausted = False + self.start_time = time.time() + + return self + + def __call__(self, item: TDataItems, meta: Any = None) -> Optional[TDataItems]: + self.count += 1 + + # detect when the limit is reached, max time or yield count + if ( + (self.count == self.max_items) + or (self.max_time and time.time() - self.start_time > self.max_time) + or self.max_items == 0 + ): + self.exhausted = True + if inspect.isgenerator(self.gen): + self.gen.close() + + # if max items is not 0, we return the last item + # otherwise never return anything + if self.max_items != 0: + return item + + # do not return any late arriving items + if self.exhausted: + return None + + return item diff --git a/dlt/extract/pipe.py b/dlt/extract/pipe.py index 02b52c4623..e70365b4f4 100644 --- a/dlt/extract/pipe.py +++ b/dlt/extract/pipe.py @@ -27,12 +27,12 @@ UnclosablePipe, ) from dlt.extract.items import ( - ItemTransform, ResolvablePipeItem, SupportsPipe, TPipeStep, TPipedDataItems, ) +from dlt.extract.items_transform import ItemTransform from dlt.extract.utils import ( check_compat_transformer, simulate_func_call, @@ -122,7 +122,23 @@ def steps(self) -> List[TPipeStep]: def find(self, *step_type: AnyType) -> int: """Finds a step with object of type `step_type`""" - return next((i for i, v in enumerate(self._steps) if isinstance(v, step_type)), -1) + found = self.find_all(step_type) + return found[0] if found else -1 + + def find_all(self, *step_type: AnyType) -> List[int]: + """Finds all steps with object of type `step_type`""" + return [i for i, v in enumerate(self._steps) if isinstance(v, step_type)] + + def get_by_type(self, *step_type: AnyType) -> TPipeStep: + """Gets first step found with object of type `step_type`""" + return next((v for v in self._steps if isinstance(v, step_type)), None) + + def remove_by_type(self, *step_type: AnyType) -> int: + """Deletes first step found with object of type `step_type`, returns previous index""" + step_index = self.find(*step_type) + if step_index >= 0: + self.remove_step(step_index) + return step_index def __getitem__(self, i: int) -> TPipeStep: return self._steps[i] diff --git a/dlt/extract/pipe_iterator.py b/dlt/extract/pipe_iterator.py index 465040f9f4..38641c0626 100644 --- a/dlt/extract/pipe_iterator.py +++ b/dlt/extract/pipe_iterator.py @@ -24,7 +24,11 @@ ) from dlt.common.configuration.container import Container from dlt.common.exceptions import PipelineException -from dlt.common.pipeline import unset_current_pipe_name, set_current_pipe_name +from dlt.common.pipeline import ( + unset_current_pipe_name, + set_current_pipe_name, + get_current_pipe_name, +) from dlt.common.utils import get_callable_name from dlt.extract.exceptions import ( @@ -180,7 +184,6 @@ def __next__(self) -> PipeItem: item = pipe_item.item # if item is iterator, then add it as a new source if isinstance(item, Iterator): - # print(f"adding iterable {item}") self._sources.append( SourcePipeItem(item, pipe_item.step, pipe_item.pipe, pipe_item.meta) ) @@ -291,7 +294,6 @@ def _get_source_item(self) -> ResolvablePipeItem: first_evaluated_index = self._current_source_index # always go round robin if None was returned or item is to be run as future self._current_source_index = (self._current_source_index - 1) % sources_count - except StopIteration: # remove empty iterator and try another source self._sources.pop(self._current_source_index) diff --git a/dlt/extract/resource.py b/dlt/extract/resource.py index 42e3905162..366e6e1a88 100644 --- a/dlt/extract/resource.py +++ b/dlt/extract/resource.py @@ -2,7 +2,7 @@ from functools import partial from typing import ( AsyncIterable, - AsyncIterator, + cast, ClassVar, Callable, Iterable, @@ -34,13 +34,16 @@ from dlt.extract.items import ( DataItemWithMeta, - ItemTransformFunc, - ItemTransformFunctionWithMeta, TableNameMeta, +) +from dlt.extract.items_transform import ( FilterItem, MapItem, YieldMapItem, ValidateItem, + LimitItem, + ItemTransformFunc, + ItemTransformFunctionWithMeta, ) from dlt.extract.pipe_iterator import ManagedPipeIterator from dlt.extract.pipe import Pipe, TPipeStep @@ -214,29 +217,22 @@ def requires_args(self) -> bool: return True @property - def incremental(self) -> IncrementalResourceWrapper: + def incremental(self) -> Optional[IncrementalResourceWrapper]: """Gets incremental transform if it is in the pipe""" - incremental: IncrementalResourceWrapper = None - step_no = self._pipe.find(IncrementalResourceWrapper, Incremental) - if step_no >= 0: - incremental = self._pipe.steps[step_no] # type: ignore - return incremental + return cast( + Optional[IncrementalResourceWrapper], + self._pipe.get_by_type(IncrementalResourceWrapper, Incremental), + ) @property def validator(self) -> Optional[ValidateItem]: """Gets validator transform if it is in the pipe""" - validator: ValidateItem = None - step_no = self._pipe.find(ValidateItem) - if step_no >= 0: - validator = self._pipe.steps[step_no] # type: ignore[assignment] - return validator + return cast(Optional[ValidateItem], self._pipe.get_by_type(ValidateItem)) @validator.setter def validator(self, validator: Optional[ValidateItem]) -> None: """Add/remove or replace the validator in pipe""" - step_no = self._pipe.find(ValidateItem) - if step_no >= 0: - self._pipe.remove_step(step_no) + step_no = self._pipe.remove_by_type(ValidateItem) if validator: self.add_step(validator, insert_at=step_no if step_no >= 0 else None) @@ -347,72 +343,37 @@ def add_filter( self._pipe.insert_step(FilterItem(item_filter), insert_at) return self - def add_limit(self: TDltResourceImpl, max_items: int) -> TDltResourceImpl: # noqa: A003 + def add_limit( + self: TDltResourceImpl, + max_items: Optional[int] = None, + max_time: Optional[float] = None, + ) -> TDltResourceImpl: # noqa: A003 """Adds a limit `max_items` to the resource pipe. - This mutates the encapsulated generator to stop after `max_items` items are yielded. This is useful for testing and debugging. + This mutates the encapsulated generator to stop after `max_items` items are yielded. This is useful for testing and debugging. - Notes: - 1. Transformers won't be limited. They should process all the data they receive fully to avoid inconsistencies in generated datasets. - 2. Each yielded item may contain several records. `add_limit` only limits the "number of yields", not the total number of records. - 3. Async resources with a limit added may occasionally produce one item more than the limit on some runs. This behavior is not deterministic. + Notes: + 1. Transformers won't be limited. They should process all the data they receive fully to avoid inconsistencies in generated datasets. + 2. Each yielded item may contain several records. `add_limit` only limits the "number of yields", not the total number of records. + 3. Async resources with a limit added may occasionally produce one item more than the limit on some runs. This behavior is not deterministic. Args: - max_items (int): The maximum number of items to yield - Returns: - "DltResource": returns self + max_items (int): The maximum number of items to yield, set to None for no limit + max_time (float): The maximum number of seconds for this generator to run after it was opened, set to None for no limit + Returns: + "DltResource": returns self """ - # make sure max_items is a number, to allow "None" as value for unlimited - if max_items is None: - max_items = -1 - - def _gen_wrap(gen: TPipeStep) -> TPipeStep: - """Wrap a generator to take the first `max_items` records""" - - # zero items should produce empty generator - if max_items == 0: - return - - count = 0 - is_async_gen = False - if callable(gen): - gen = gen() # type: ignore - - # wrap async gen already here - if isinstance(gen, AsyncIterator): - gen = wrap_async_iterator(gen) - is_async_gen = True - - try: - for i in gen: # type: ignore # TODO: help me fix this later - yield i - if i is not None: - count += 1 - # async gen yields awaitable so we must count one awaitable more - # so the previous one is evaluated and yielded. - # new awaitable will be cancelled - if count == max_items + int(is_async_gen): - return - finally: - if inspect.isgenerator(gen): - gen.close() - return - - # transformers should be limited by their input, so we only limit non-transformers - if not self.is_transformer: - gen = self._pipe.gen - # wrap gen directly - if inspect.isgenerator(gen): - self._pipe.replace_gen(_gen_wrap(gen)) - else: - # keep function as function to not evaluate generators before pipe starts - self._pipe.replace_gen(partial(_gen_wrap, gen)) - else: + if self.is_transformer: logger.warning( f"Setting add_limit to a transformer {self.name} has no effect. Set the limit on" " the top level resource." ) + else: + # remove existing limit if any + self._pipe.remove_by_type(LimitItem) + self.add_step(LimitItem(max_items=max_items, max_time=max_time)) + return self def parallelize(self: TDltResourceImpl) -> TDltResourceImpl: @@ -445,9 +406,7 @@ def add_step( return self def _remove_incremental_step(self) -> None: - step_no = self._pipe.find(Incremental, IncrementalResourceWrapper) - if step_no >= 0: - self._pipe.remove_step(step_no) + self._pipe.remove_by_type(Incremental, IncrementalResourceWrapper) def set_incremental( self, diff --git a/dlt/extract/utils.py b/dlt/extract/utils.py index 68570d0995..0bcd13155e 100644 --- a/dlt/extract/utils.py +++ b/dlt/extract/utils.py @@ -183,6 +183,17 @@ def check_compat_transformer(name: str, f: AnyFun, sig: inspect.Signature) -> in return meta_arg +def wrap_iterator(gen: Iterator[TDataItems]) -> Iterator[TDataItems]: + """Wraps an iterator into a generator""" + if inspect.isgenerator(gen): + return gen + + def wrapped_gen() -> Iterator[TDataItems]: + yield from gen + + return wrapped_gen() + + def wrap_async_iterator( gen: AsyncIterator[TDataItems], ) -> Generator[Awaitable[TDataItems], None, None]: diff --git a/dlt/extract/validation.py b/dlt/extract/validation.py index 4cd321b88c..d9fe70a90b 100644 --- a/dlt/extract/validation.py +++ b/dlt/extract/validation.py @@ -8,7 +8,8 @@ from dlt.common.typing import TDataItems from dlt.common.schema.typing import TAnySchemaColumns, TSchemaContract, TSchemaEvolutionMode -from dlt.extract.items import TTableHintTemplate, ValidateItem +from dlt.extract.items import TTableHintTemplate +from dlt.extract.items_transform import ValidateItem _TPydanticModel = TypeVar("_TPydanticModel", bound=PydanticBaseModel) diff --git a/dlt/sources/helpers/transform.py b/dlt/sources/helpers/transform.py index 32843e2aa2..45738fe4fb 100644 --- a/dlt/sources/helpers/transform.py +++ b/dlt/sources/helpers/transform.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Sequence, Union from dlt.common.typing import TDataItem -from dlt.extract.items import ItemTransformFunctionNoMeta +from dlt.extract.items_transform import ItemTransformFunctionNoMeta import jsonpath_ng diff --git a/docs/examples/backfill_in_chunks/__init__.py b/docs/examples/backfill_in_chunks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/backfill_in_chunks/backfill_in_chunks.py b/docs/examples/backfill_in_chunks/backfill_in_chunks.py new file mode 100644 index 0000000000..a758d67f7b --- /dev/null +++ b/docs/examples/backfill_in_chunks/backfill_in_chunks.py @@ -0,0 +1,85 @@ +""" +--- +title: Backfilling in chunks +description: Learn how to backfill in chunks of defined size +keywords: [incremental loading, backfilling, chunks,example] +--- + +In this example, you'll find a Python script that will load from a sql_database source in chunks of defined size. This is useful for backfilling in multiple pipeline runs as +opposed to backfilling in one very large pipeline run which may fail due to memory issues on ephemeral storage or just take a very long time to complete without seeing any +progress in the destination. + +We'll learn how to: + +- Connect to a mysql database with the sql_database source +- Select one table to load and apply incremental loading hints as well as the primary key +- Set the chunk size and limit the number of chunks to load in one pipeline run +- Create a pipeline and backfill the table in the defined chunks +- Use the datasets accessor to inspect and assert the load progress + +""" + +import pandas as pd + +import dlt +from dlt.sources.sql_database import sql_database + + +if __name__ == "__main__": + # NOTE: this is a live table in the rfam database, so the number of final rows may change + TOTAL_TABLE_ROWS = 4178 + RFAM_CONNECTION_STRING = "mysql+pymysql://rfamro@mysql-rfam-public.ebi.ac.uk:4497/Rfam" + + # create sql database source that only loads the family table in chunks of 1000 rows + source = sql_database(RFAM_CONNECTION_STRING, table_names=["family"], chunk_size=1000) + + # we apply some hints to the table, we know the rfam_id is unique and that we can order + # and load incrementally on the created datetime column + source.family.apply_hints( + primary_key="rfam_id", + incremental=dlt.sources.incremental( + cursor_path="created", initial_value=None, row_order="asc" + ), + ) + + # with limit we can limit the number of chunks to load, with a chunk size of 1000 and a limit of 1 + # we will load 1000 rows per pipeline run + source.add_limit(1) + + # create pipeline + pipeline = dlt.pipeline( + pipeline_name="rfam", destination="duckdb", dataset_name="rfam_data", dev_mode=True + ) + + def _assert_unique_row_count(df: pd.DataFrame, num_rows: int) -> None: + """Assert that a dataframe has the correct number of unique rows""" + # NOTE: this check is dependent on reading the full table back from the destination into memory, + # so it is only useful for testing before you do a large backfill. + assert len(df) == num_rows + assert len(set(df.rfam_id.tolist())) == num_rows + + # after the first run, the family table in the destination should contain the first 1000 rows + pipeline.run(source) + _assert_unique_row_count(pipeline.dataset().family.df(), 1000) + + # after the second run, the family table in the destination should contain 1999 rows + # there is some overlap on the incremental to prevent skipping rows + pipeline.run(source) + _assert_unique_row_count(pipeline.dataset().family.df(), 1999) + + # ... + pipeline.run(source) + _assert_unique_row_count(pipeline.dataset().family.df(), 2998) + + # ... + pipeline.run(source) + _assert_unique_row_count(pipeline.dataset().family.df(), 3997) + + # the final run will load all the rows until the end of the table + pipeline.run(source) + _assert_unique_row_count(pipeline.dataset().family.df(), TOTAL_TABLE_ROWS) + + # NOTE: in a production environment you will likely: + # * be using much larger chunk sizes and limits + # * run the pipeline in a loop to load all the rows + # * and programmatically check if the table is fully loaded and abort the loop if this is the case. diff --git a/docs/website/docs/general-usage/resource.md b/docs/website/docs/general-usage/resource.md index 199eaf9b5d..b8d51caf75 100644 --- a/docs/website/docs/general-usage/resource.md +++ b/docs/website/docs/general-usage/resource.md @@ -405,11 +405,26 @@ dlt.pipeline(destination="duckdb").run(my_resource().add_limit(10)) The code above will extract `15*10=150` records. This is happening because in each iteration, 15 records are yielded, and we're limiting the number of iterations to 10. ::: -Some constraints of `add_limit` include: +Altenatively you can also apply a time limit to the resource. The code below will run the extraction for 10 seconds and extract how ever many items are yielded in that time. In combination with incrementals, this can be useful for batched loading or for loading on machines that have a run time limit. + +```py +dlt.pipeline(destination="duckdb").run(my_resource().add_limit(max_time=10)) +``` + +You can also apply a combination of both limits. In this case the extraction will stop as soon as either limit is reached. + +```py +dlt.pipeline(destination="duckdb").run(my_resource().add_limit(max_items=10, max_time=10)) +``` + + +Some notes about the `add_limit`: 1. `add_limit` does not skip any items. It closes the iterator/generator that produces data after the limit is reached. 2. You cannot limit transformers. They should process all the data they receive fully to avoid inconsistencies in generated datasets. 3. Async resources with a limit added may occasionally produce one item more than the limit on some runs. This behavior is not deterministic. +4. Calling add limit on a resource will replace any previously set limits settings. +5. For time-limited resources, the timer starts when the first item is processed. When resources are processed sequentially (FIFO mode), each resource's time limit applies also sequentially. In the default round robin mode, the time limits will usually run concurrently. :::tip If you are parameterizing the value of `add_limit` and sometimes need it to be disabled, you can set `None` or `-1` to disable the limiting. diff --git a/docs/website/docs/general-usage/source.md b/docs/website/docs/general-usage/source.md index 87c07a3e44..9c6c2aac13 100644 --- a/docs/website/docs/general-usage/source.md +++ b/docs/website/docs/general-usage/source.md @@ -107,8 +107,20 @@ load_info = pipeline.run(pipedrive_source().add_limit(10)) print(load_info) ``` +You can also apply a time limit to the source: + +```py +pipeline.run(pipedrive_source().add_limit(max_time=10)) +``` + +Or limit by both, the limit that is reached first will stop the extraction: + +```py +pipeline.run(pipedrive_source().add_limit(max_items=10, max_time=10)) +``` + :::note -Note that `add_limit` **does not limit the number of records** but rather the "number of yields". `dlt` will close the iterator/generator that produces data after the limit is reached. +Note that `add_limit` **does not limit the number of records** but rather the "number of yields". `dlt` will close the iterator/generator that produces data after the limit is reached. Please read in more detail about the `add_limit` on the resource page. ::: Find more on sampling data [here](resource.md#sample-from-large-data). diff --git a/tests/extract/test_extract_pipe.py b/tests/extract/test_extract_pipe.py index d40639a594..659888269a 100644 --- a/tests/extract/test_extract_pipe.py +++ b/tests/extract/test_extract_pipe.py @@ -10,7 +10,8 @@ from dlt.common import sleep from dlt.common.typing import TDataItems from dlt.extract.exceptions import CreatePipeException, ResourceExtractionError, UnclosablePipe -from dlt.extract.items import DataItemWithMeta, FilterItem, MapItem, YieldMapItem +from dlt.extract.items import DataItemWithMeta +from dlt.extract.items_transform import FilterItem, MapItem, YieldMapItem from dlt.extract.pipe import Pipe from dlt.extract.pipe_iterator import PipeIterator, ManagedPipeIterator, PipeItem diff --git a/tests/extract/test_incremental.py b/tests/extract/test_incremental.py index d63dac93f2..9ad7d28e88 100644 --- a/tests/extract/test_incremental.py +++ b/tests/extract/test_incremental.py @@ -7,6 +7,7 @@ from time import sleep from typing import Any, Optional, Literal, Sequence, Dict, Iterable from unittest import mock +import itertools import duckdb import pyarrow as pa @@ -35,7 +36,7 @@ IncrementalPrimaryKeyMissing, ) from dlt.extract.incremental.lag import apply_lag -from dlt.extract.items import ValidateItem +from dlt.extract.items_transform import ValidateItem from dlt.extract.resource import DltResource from dlt.pipeline.exceptions import PipelineStepFailed from dlt.sources.helpers.transform import take_first @@ -3960,3 +3961,58 @@ def some_data( # Includes values 5-10 inclusive assert items == expected_items + + +@pytest.mark.parametrize("offset_by_last_value", [True, False]) +def test_incremental_and_limit(offset_by_last_value: bool): + resource_called = 0 + + # here we check incremental and limit when incremental once when last value cannot be used + # to offset the source, and once when it can. + + @dlt.resource( + table_name="items", + ) + def resource( + incremental=dlt.sources.incremental(cursor_path="id", initial_value=-1, row_order="asc") + ): + range_iterator = ( + range(incremental.start_value + 1, 1000) if offset_by_last_value else range(1000) + ) + for i in range_iterator: + nonlocal resource_called + resource_called += 1 + yield { + "id": i, + "value": str(i), + } + + resource.add_limit(10) + + p = dlt.pipeline(pipeline_name="incremental_limit", destination="duckdb", dev_mode=True) + + p.run(resource()) + + # check we have the right number of items + assert len(p.dataset().items.df()) == 10 + assert resource_called == 10 + # check that we have items 0-9 + assert p.dataset().items.df().id.tolist() == list(range(10)) + + # run the next ten + p.run(resource()) + + # check we have the right number of items + assert len(p.dataset().items.df()) == 20 + assert resource_called == 20 if offset_by_last_value else 30 + # check that we have items 0-19 + assert p.dataset().items.df().id.tolist() == list(range(20)) + + # run the next batch + p.run(resource()) + + # check we have the right number of items + assert len(p.dataset().items.df()) == 30 + assert resource_called == 30 if offset_by_last_value else 60 + # check that we have items 0-29 + assert p.dataset().items.df().id.tolist() == list(range(30)) diff --git a/tests/extract/test_sources.py b/tests/extract/test_sources.py index 3d021d5d10..86646e6369 100644 --- a/tests/extract/test_sources.py +++ b/tests/extract/test_sources.py @@ -1,4 +1,6 @@ import itertools +import time + from typing import Iterator import pytest @@ -837,7 +839,7 @@ def test_limit_infinite_counter() -> None: @pytest.mark.parametrize("limit", (None, -1, 0, 10)) def test_limit_edge_cases(limit: int) -> None: - r = dlt.resource(range(20), name="infinity").add_limit(limit) # type: ignore + r = dlt.resource(range(20), name="resource").add_limit(limit) # type: ignore @dlt.resource() async def r_async(): @@ -845,22 +847,62 @@ async def r_async(): await asyncio.sleep(0.01) yield i + @dlt.resource(parallelized=True) + def parallelized_resource(): + for i in range(20): + yield i + sync_list = list(r) async_list = list(r_async().add_limit(limit)) + parallelized_list = list(parallelized_resource().add_limit(limit)) + + # all lists should be the same + assert sync_list == async_list == parallelized_list if limit == 10: assert sync_list == list(range(10)) - # we have edge cases where the async list will have one extra item - # possibly due to timing issues, maybe some other implementation problem - assert (async_list == list(range(10))) or (async_list == list(range(11))) elif limit in [None, -1]: - assert sync_list == async_list == list(range(20)) + assert sync_list == list(range(20)) elif limit == 0: - assert sync_list == async_list == [] + assert sync_list == [] else: raise AssertionError(f"Unexpected limit: {limit}") +def test_various_limit_setups() -> None: + # basic test + r = dlt.resource([1, 2, 3, 4, 5], name="test").add_limit(3) + assert list(r) == [1, 2, 3] + + # yield map test + r = ( + dlt.resource([1, 2, 3, 4, 5], name="test") + .add_map(lambda i: str(i) * i, 1) + .add_yield_map(lambda i: (yield from i)) + .add_limit(3) + ) + # limit is applied at the end + assert list(r) == ["1", "2", "2"] # "3" ,"3" ,"3" ,"4" ,"4" ,"4" ,"4", ...] + + # nested lists test (limit only applied to yields, not actual items) + r = dlt.resource([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], name="test").add_limit(3) + assert list(r) == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + # transformer test + r = dlt.resource([1, 2, 3, 4, 5], name="test").add_limit(4) + t = dlt.transformer(lambda i: i * 2, name="test") + assert list(r) == [1, 2, 3, 4] + assert list(r | t) == [2, 4, 6, 8] + + # adding limit to transformer is disregarded + t = t.add_limit(2) + assert list(r | t) == [2, 4, 6, 8] + + # limits are fully replaced (more genereous limit applied later takes precedence) + r = dlt.resource([1, 2, 3, 4, 5], name="test").add_limit(3).add_limit(4) + assert list(r) == [1, 2, 3, 4] + + def test_limit_source() -> None: def mul_c(item): yield from "A" * (item + 2) @@ -876,6 +918,30 @@ def infinite_source(): assert list(infinite_source().add_limit(2)) == ["A", "A", 0, "A", "A", "A", 1] * 3 +def test_limit_max_time() -> None: + @dlt.resource() + def r(): + for i in range(100): + time.sleep(0.1) + yield i + + @dlt.resource() + async def r_async(): + for i in range(100): + await asyncio.sleep(0.1) + yield i + + sync_list = list(r().add_limit(max_time=1)) + async_list = list(r_async().add_limit(max_time=1)) + + # we should have extracted 10 items within 1 second, sleep is included in the resource + # we allow for some variance in the number of items, as the sleep is not super precise + # on mac os we even sometimes just get 4 items... + allowed_results = [list(range(i)) for i in [12, 11, 10, 9, 8, 7, 6, 5, 4]] + assert sync_list in allowed_results + assert async_list in allowed_results + + def test_source_state() -> None: @dlt.source def test_source(expected_state): diff --git a/tests/extract/test_validation.py b/tests/extract/test_validation.py index 138589bb06..3800f333f6 100644 --- a/tests/extract/test_validation.py +++ b/tests/extract/test_validation.py @@ -10,7 +10,7 @@ from dlt.common.libs.pydantic import BaseModel from dlt.extract import DltResource -from dlt.extract.items import ValidateItem +from dlt.extract.items_transform import ValidateItem from dlt.extract.validation import PydanticValidator from dlt.extract.exceptions import ResourceExtractionError from dlt.pipeline.exceptions import PipelineStepFailed diff --git a/tests/extract/utils.py b/tests/extract/utils.py index 7364ef7243..f1de3de093 100644 --- a/tests/extract/utils.py +++ b/tests/extract/utils.py @@ -6,7 +6,7 @@ from dlt.common.typing import TDataItem, TDataItems from dlt.extract.extract import ExtractStorage -from dlt.extract.items import ItemTransform +from dlt.extract.items_transform import ItemTransform from tests.utils import TestDataItemFormat From fc0bb38af7e40a4a3601f061c0ebd88467b2c9d4 Mon Sep 17 00:00:00 2001 From: Violetta Mishechkina Date: Tue, 17 Dec 2024 09:54:57 +0100 Subject: [PATCH 18/23] Update auth info in databricks docs (#2153) --- .../dlt-ecosystem/destinations/databricks.md | 243 ++++++++++++++---- 1 file changed, 199 insertions(+), 44 deletions(-) diff --git a/docs/website/docs/dlt-ecosystem/destinations/databricks.md b/docs/website/docs/dlt-ecosystem/destinations/databricks.md index dd046ce28a..a28a42f761 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/databricks.md +++ b/docs/website/docs/dlt-ecosystem/destinations/databricks.md @@ -52,7 +52,7 @@ If you already have your Databricks workspace set up, you can skip to the [Loade Add a new role assignment and select "Storage Blob Data Contributor" as the role. Under "Members" select "Managed Identity" and add the Databricks Access Connector you created in the previous step. -### 2. Set up a metastore and Unity Catalog and get your access token +### 2. Set up a metastore and Unity Catalog 1. Now go to your Databricks workspace @@ -85,26 +85,34 @@ If you already have your Databricks workspace set up, you can skip to the [Loade Go to "Catalog" and click "Create Catalog". Name your catalog and select the storage location you created in the previous step. -8. Create your access token +## Authentication - Click your email in the top right corner and go to "User Settings". Go to "Developer" -> "Access Tokens". - Generate a new token and save it. You will use it in your `dlt` configuration. +`dlt` currently supports two options for authentication: +1. [OAuth2](#oauth) (recommended) allows you to authenticate to Databricks using a service principal via OAuth2 M2M. +2. [Access token](#access_token) approach using a developer access token. This method may be deprecated in the future by Databricks. -## OAuth M2M (Machine-to-Machine) Authentication +### Using OAuth2 -You can authenticate to Databricks using a service principal via OAuth M2M. This method allows for secure, programmatic access to Databricks resources without requiring a user-managed personal access token. +You can authenticate to Databricks using a service principal via OAuth2 M2M. To enable it: -### Create a Service Principal in Databricks -Follow the instructions in the Databricks documentation to create a service principal and retrieve the client_id and client_secret: +1. Follow the instructions in the Databricks documentation: [Authenticate access to Databricks using OAuth M2M](https://docs.databricks.com/en/dev-tools/auth/oauth-m2m.html) +to create a service principal and retrieve the `client_id` and `client_secret`. -[Authenticate access to Databricks using OAuth M2M](https://docs.databricks.com/en/dev-tools/auth/oauth-m2m.html) +2. Once you have the service principal credentials, update your credentials with any of the options shown below: -Once you have the service principal credentials, update your secrets.toml as shown bellow. + -### Configuration + -Add the following fields to your `.dlt/secrets.toml` file: ```toml +# secrets.toml [destination.databricks.credentials] server_hostname = "MY_DATABRICKS.azuredatabricks.net" http_path = "/sql/1.0/warehouses/12345" @@ -112,6 +120,88 @@ catalog = "my_catalog" client_id = "XXX" client_secret = "XXX" ``` + + + + +```sh +export DESTINATIONS__DATABRICKS__CREDENTIALS__SERVER_HOSTNAME="MY_DATABRICKS.azuredatabricks.net" +export DESTINATIONS__DATABRICKS__CREDENTIALS__HTTP_PATH="/sql/1.0/warehouses/12345" +export DESTINATIONS__DATABRICKS__CREDENTIALS__CATALOG="my_catalog" +export DESTINATIONS__DATABRICKS__CREDENTIALS__CLIENT_ID="XXX" +export DESTINATIONS__DATABRICKS__CREDENTIALS__CLIENT_SECRET="XXX" +``` + + + + +```py +import os + +# Do not set up the secrets directly in the code! +# What you can do is reassign env variables. +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__SERVER_HOSTNAME"] = "MY_DATABRICKS.azuredatabricks.net" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__HTTP_PATH"]="/sql/1.0/warehouses/12345" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__CATALOG"]="my_catalog" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__CLIENT_ID"]=os.environ.get("CLIENT_ID") +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__CLIENT_SECRET"]=os.environ.get("CLIENT_SECRET") +``` + + + +### Using access token + +To create your access token: + +1. Click your email in the top right corner and go to "User Settings". Go to "Developer" -> "Access Tokens". +Generate a new token and save it. +2. Set up credentials in a desired way: + + + + + +```toml +# secrets.toml +[destination.databricks.credentials] +server_hostname = "MY_DATABRICKS.azuredatabricks.net" +http_path = "/sql/1.0/warehouses/12345" +catalog = "my_catalog" +access_token = "XXX" +``` + + + + +```sh +export DESTINATIONS__DATABRICKS__CREDENTIALS__SERVER_HOSTNAME="MY_DATABRICKS.azuredatabricks.net" +export DESTINATIONS__DATABRICKS__CREDENTIALS__HTTP_PATH="/sql/1.0/warehouses/12345" +export DESTINATIONS__DATABRICKS__CREDENTIALS__CATALOG="my_catalog" +export DESTINATIONS__DATABRICKS__CREDENTIALS__ACCESS_TOKEN="XXX" +``` + + + + +```py +import os + +# Do not set up the secrets directly in the code! +# What you can do is reassign env variables. +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__SERVER_HOSTNAME"] = "MY_DATABRICKS.azuredatabricks.net" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__HTTP_PATH"]="/sql/1.0/warehouses/12345" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__CATALOG"]="my_catalog" +os.environ["DESTINATIONS__DATABRICKS__CREDENTIALS__ACCESS_TOKEN"]=os.environ.get("ACCESS_TOKEN") +``` + + ## Loader setup guide @@ -129,9 +219,9 @@ pip install -r requirements.txt This will install dlt with the `databricks` extra, which contains the Databricks Python dbapi client. -**4. Enter your credentials into `.dlt/secrets.toml`.** +**3. Enter your credentials into `.dlt/secrets.toml`.** -This should include your connection parameters and your personal access token. +This should include your connection parameters and your authentication credentials. You can find your server hostname and HTTP path in the Databricks workspace dashboard. Go to "SQL Warehouses", select your warehouse (default is called "Starter Warehouse"), and go to "Connection details". @@ -141,11 +231,14 @@ Example: [destination.databricks.credentials] server_hostname = "MY_DATABRICKS.azuredatabricks.net" http_path = "/sql/1.0/warehouses/12345" -access_token = "MY_ACCESS_TOKEN" # Replace for client_id and client_secret when using OAuth +client_id = "XXX" +client_secret = "XXX" catalog = "my_catalog" ``` -See [staging support](#staging-support) for authentication options when `dlt` copies files from buckets. +You can find other options for specifying credentials in the [Authentication section](#authentication). + +See [Staging support](#staging-support) for authentication options when `dlt` copies files from buckets. ## Write disposition All write dispositions are supported. @@ -155,8 +248,7 @@ To load data into Databricks, you must set up a staging filesystem by configurin dlt will upload the data in Parquet files (or JSONL, if configured) to the bucket and then use `COPY INTO` statements to ingest the data into Databricks. -For more information on staging, see the [staging support](#staging-support) section below. - +For more information on staging, see the [Staging support](#staging-support) section below. ## Supported file formats * [Parquet](../file-formats/parquet.md) supported when staging is enabled. @@ -164,13 +256,13 @@ For more information on staging, see the [staging support](#staging-support) sec The JSONL format has some limitations when used with Databricks: -1. Compression must be disabled to load jsonl files in Databricks. Set `data_writer.disable_compression` to `true` in the dlt config when using this format. +1. Compression must be disabled to load JSONL files in Databricks. Set `data_writer.disable_compression` to `true` in the dlt config when using this format. 2. The following data types are not supported when using the JSONL format with `databricks`: `decimal`, `json`, `date`, `binary`. Use `parquet` if your data contains these types. 3. The `bigint` data type with precision is not supported with the JSONL format. ## Staging support -Databricks supports both Amazon S3, Azure Blob Storage and Google Cloud Storage as staging locations. `dlt` will upload files in Parquet format to the staging location and will instruct Databricks to load data from there. +Databricks supports both Amazon S3, Azure Blob Storage, and Google Cloud Storage as staging locations. `dlt` will upload files in Parquet format to the staging location and will instruct Databricks to load data from there. ### Databricks and Amazon S3 @@ -178,19 +270,50 @@ Please refer to the [S3 documentation](./filesystem.md#aws-s3) for details on co Example to set up Databricks with S3 as a staging destination: + + + + +```toml +# secrets.toml +[destination.filesystem] +bucket_url = "s3://your-bucket-name" + +[destination.filesystem.credentials] +aws_access_key_id="XXX" +aws_secret_access_key="XXX" +``` + + + + +```sh +export DESTINATIONS__FILESYSTEM__BUCKET_URL="s3://your-bucket-name" +export DESTINATIONS__FILESYSTEM__CREDENTIALS__AWS_ACCESS_KEY_ID="XXX" +export DESTINATIONS__FILESYSTEM__CREDENTIALS__AWS_SECRET_ACCESS_KEY="XXX" +``` + + + + ```py -import dlt +import os -# Create a dlt pipeline that will load -# chess player data to the Databricks destination -# via staging on S3 -pipeline = dlt.pipeline( - pipeline_name='chess_pipeline', - destination='databricks', - staging=dlt.destinations.filesystem('s3://your-bucket-name'), # add this to activate the staging location - dataset_name='player_data', -) +# Do not set up the secrets directly in the code! +# What you can do is reassign env variables. +os.environ["DESTINATIONS__FILESYSTEM__BUCKET_URL"] = "s3://your-bucket-name" +os.environ["DESTINATIONS__FILESYSTEM__CREDENTIALS__AWS_ACCESS_KEY_ID"] = os.environ.get("AWS_ACCESS_KEY_ID") +os.environ["DESTINATIONS__FILESYSTEM__CREDENTIALS__AWS_SECRET_ACCESS_KEY"] = os.environ.get("AWS_SECRET_ACCESS_KEY") ``` + + ### Databricks and Azure Blob Storage @@ -209,22 +332,54 @@ dlt is able to adapt the other representation (i.e., `az://container-name/path`) Example to set up Databricks with Azure as a staging destination: + + + + +```toml +# secrets.toml +[destination.filesystem] +bucket_url = "abfss://container_name@storage_account_name.dfs.core.windows.net/path" + +[destination.filesystem.credentials] +azure_storage_account_name="XXX" +azure_storage_account_key="XXX" +``` + + + + +```sh +export DESTINATIONS__FILESYSTEM__BUCKET_URL="abfss://container_name@storage_account_name.dfs.core.windows.net/path" +export DESTINATIONS__FILESYSTEM__CREDENTIALS__AZURE_STORAGE_ACCOUNT_NAME="XXX" +export DESTINATIONS__FILESYSTEM__CREDENTIALS__AZURE_STORAGE_ACCOUNT_KEY="XXX" +``` + + + + ```py -# Create a dlt pipeline that will load -# chess player data to the Databricks destination -# via staging on Azure Blob Storage -pipeline = dlt.pipeline( - pipeline_name='chess_pipeline', - destination='databricks', - staging=dlt.destinations.filesystem('abfss://dlt-ci-data@dltdata.dfs.core.windows.net'), # add this to activate the staging location - dataset_name='player_data' -) +import os + +# Do not set up the secrets directly in the code! +# What you can do is reassign env variables. +os.environ["DESTINATIONS__FILESYSTEM__BUCKET_URL"] = "abfss://container_name@storage_account_name.dfs.core.windows.net/path" +os.environ["DESTINATIONS__FILESYSTEM__CREDENTIALS__AZURE_STORAGE_ACCOUNT_NAME"] = os.environ.get("AZURE_STORAGE_ACCOUNT_NAME") +os.environ["DESTINATIONS__FILESYSTEM__CREDENTIALS__AZURE_STORAGE_ACCOUNT_KEY"] = os.environ.get("AZURE_STORAGE_ACCOUNT_KEY") ``` + + ### Databricks and Google Cloud Storage -In order to load from Google Cloud Storage stage you must set-up the credentials via **named credential**. See below. Databricks does not allow to pass Google Credentials -explicitly in SQL Statements. +In order to load from Google Cloud Storage stage, you must set up the credentials via a **named credential**. See below. Databricks does not allow you to pass Google Credentials explicitly in SQL statements. ### Use external locations and stored credentials `dlt` forwards bucket credentials to the `COPY INTO` SQL command by default. You may prefer to use [external locations or stored credentials instead](https://docs.databricks.com/en/sql/language-manual/sql-ref-external-locations.html#external-location) that are stored on the Databricks side. @@ -235,7 +390,7 @@ If you set up an external location for your staging path, you can tell `dlt` to is_staging_external_location=true ``` -If you set up Databricks credentials named, for example, **credential_x**, you can tell `dlt` to use it: +If you set up Databricks credentials named, for example, **credential_x**, you can tell `dlt` to use them: ```toml [destination.databricks] staging_credentials_name="credential_x" @@ -256,8 +411,8 @@ This destination [integrates with dbt](../transformations/dbt/dbt.md) via [dbt-d ### Syncing of `dlt` state This destination fully supports [dlt state sync](../../general-usage/state#syncing-state-with-destination). -### Databricks User Agent -We enable Databricks to identify that the connection is created by dlt. +### Databricks user agent +We enable Databricks to identify that the connection is created by `dlt`. Databricks will use this user agent identifier to better understand the usage patterns associated with dlt integration. The connection identifier is `dltHub_dlt`. From 80e8cc36ef1dfbfad84286f8baf8ce90ad163ba2 Mon Sep 17 00:00:00 2001 From: Shepard Wang <781728963@qq.com> Date: Tue, 17 Dec 2024 20:21:31 +0800 Subject: [PATCH 19/23] Enable datatime format for negative timezone (#2155) --- dlt/common/time.py | 21 +++++++++++++++++---- tests/common/test_time.py | 22 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/dlt/common/time.py b/dlt/common/time.py index 4ce411baa4..74c32e4ea0 100644 --- a/dlt/common/time.py +++ b/dlt/common/time.py @@ -164,17 +164,30 @@ def detect_datetime_format(value: str) -> Optional[str]: ): "%Y-%m-%dT%H:%M:%S.%fZ", # UTC with fractional seconds re.compile( r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{2}:\d{2}$" - ): "%Y-%m-%dT%H:%M:%S%z", # Timezone offset + ): "%Y-%m-%dT%H:%M:%S%z", # Positive timezone offset re.compile( r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{4}$" - ): "%Y-%m-%dT%H:%M:%S%z", # Timezone without colon - # Full datetime with fractional seconds and timezone + ): "%Y-%m-%dT%H:%M:%S%z", # Positive timezone without colon + # Full datetime with fractional seconds and positive timezone offset re.compile( r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\+\d{2}:\d{2}$" ): "%Y-%m-%dT%H:%M:%S.%f%z", re.compile( r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\+\d{4}$" - ): "%Y-%m-%dT%H:%M:%S.%f%z", # Timezone without colon + ): "%Y-%m-%dT%H:%M:%S.%f%z", # Positive timezone without colon + re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{2}:\d{2}$" + ): "%Y-%m-%dT%H:%M:%S%z", # Negative timezone offset + re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{4}$" + ): "%Y-%m-%dT%H:%M:%S%z", # Negative timezone without colon + # Full datetime with fractional seconds and negative timezone offset + re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+-\d{2}:\d{2}$" + ): "%Y-%m-%dT%H:%M:%S.%f%z", + re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+-\d{4}$" + ): "%Y-%m-%dT%H:%M:%S.%f%z", # Negative Timezone without colon # Datetime without timezone re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$"): "%Y-%m-%dT%H:%M:%S", # No timezone re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$"): "%Y-%m-%dT%H:%M", # Minute precision diff --git a/tests/common/test_time.py b/tests/common/test_time.py index 8c25983d46..9c7a1567e2 100644 --- a/tests/common/test_time.py +++ b/tests/common/test_time.py @@ -132,8 +132,26 @@ def test_datetime_to_timestamp_helpers( [ ("2024-10-20T15:30:00Z", "%Y-%m-%dT%H:%M:%SZ"), # UTC 'Z' ("2024-10-20T15:30:00.123456Z", "%Y-%m-%dT%H:%M:%S.%fZ"), # UTC 'Z' with fractional seconds - ("2024-10-20T15:30:00+02:00", "%Y-%m-%dT%H:%M:%S%z"), # Timezone offset - ("2024-10-20T15:30:00+0200", "%Y-%m-%dT%H:%M:%S%z"), # Timezone without colon + ("2024-10-20T15:30:00+02:00", "%Y-%m-%dT%H:%M:%S%z"), # Positive timezone offset + ("2024-10-20T15:30:00+0200", "%Y-%m-%dT%H:%M:%S%z"), # Positive timezone offset (no colon) + ( + "2024-10-20T15:30:00.123456+02:00", + "%Y-%m-%dT%H:%M:%S.%f%z", + ), # Positive timezone offset with fractional seconds + ( + "2024-10-20T15:30:00.123456+0200", + "%Y-%m-%dT%H:%M:%S.%f%z", + ), # Positive timezone offset with fractional seconds (no colon) + ("2024-10-20T15:30:00-02:00", "%Y-%m-%dT%H:%M:%S%z"), # Negative timezone offset + ("2024-10-20T15:30:00-0200", "%Y-%m-%dT%H:%M:%S%z"), # Negative timezone offset (no colon) + ( + "2024-10-20T15:30:00.123456-02:00", + "%Y-%m-%dT%H:%M:%S.%f%z", + ), # Negative timezone offset with fractional seconds + ( + "2024-10-20T15:30:00.123456-0200", + "%Y-%m-%dT%H:%M:%S.%f%z", + ), # Negative timezone offset with fractional seconds (no colon) ("2024-10-20T15:30:00", "%Y-%m-%dT%H:%M:%S"), # No timezone ("2024-10-20T15:30", "%Y-%m-%dT%H:%M"), # Minute precision ("2024-10-20T15", "%Y-%m-%dT%H"), # Hour precision From 9e70cd2723d7abd7991c998520ffe7d4bc41a4af Mon Sep 17 00:00:00 2001 From: David Scharf Date: Tue, 17 Dec 2024 14:39:25 +0100 Subject: [PATCH 20/23] improve how dlt works page (#2152) * first draft * updates * add image * add bottom list --- .../reference/explainers/how-dlt-works.md | 82 +++++++++++++----- docs/website/static/img/dlt-onepager.png | Bin 0 -> 692945 bytes 2 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 docs/website/static/img/dlt-onepager.png diff --git a/docs/website/docs/reference/explainers/how-dlt-works.md b/docs/website/docs/reference/explainers/how-dlt-works.md index fa73babd03..570bc8cdbe 100644 --- a/docs/website/docs/reference/explainers/how-dlt-works.md +++ b/docs/website/docs/reference/explainers/how-dlt-works.md @@ -6,33 +6,71 @@ keywords: [architecture, extract, normalize, load] # How `dlt` works -`dlt` automatically turns JSON returned by any [source](../../general-usage/glossary.md#source) -(e.g., an API) into a live dataset stored in the -[destination](../../general-usage/glossary.md#destination) of your choice (e.g., Google BigQuery). It -does this by first [extracting](how-dlt-works.md#extract) the JSON data, then -[normalizing](how-dlt-works.md#normalize) it to a schema, and finally [loading](how-dlt-works#load) -it to the location where you will store it. +In a nutshell, `dlt` automatically turns data from a number of available [sources](../../dlt-ecosystem/verified-sources) (e.g., an API, a PostgreSQL database, or Python data structures) into a live dataset stored in a [destination](../../dlt-ecosystem/destinations) of your choice (e.g., Google BigQuery, a Deltalake on Azure, or by pushing the data back via reverse ETL). You can easily implement your own sources, as long as you yield data in a way that is compatible with `dlt`, such as JSON objects, Python lists and dictionaries, pandas dataframes, and arrow tables. `dlt` will be able to automatically compute the schema and move the data to your destination. -![architecture-diagram](/img/architecture-diagram.png) +![architecture-diagram](/img/dlt-onepager.png) -## Extract +## A concrete example -The Python script requests data from an API or a similar -[source](../../general-usage/glossary.md#source). Once this data is received, the script parses the -JSON and provides it to `dlt` as input, which then normalizes that data. +The main building block of `dlt` is the [pipeline](../../general-usage/glossary.md#pipeline), which orchestrates the loading of data from your source into your destination in three discrete steps when you call its `run` method. Consider this intentionally short example: -## Normalize +```py +import dlt -The configurable normalization engine in `dlt` recursively unpacks this nested structure into -relational tables (i.e., inferring data types, linking tables to create nested relationships, -etc.), making it ready to be loaded. This creates a -[schema](../../general-usage/glossary.md#schema), which will automatically evolve to accommodate any future -source data changes (e.g., new fields or tables). +pipeline = dlt.pipeline(pipeline_name="my_pipeline", destination="duckdb") +pipeline.run( + [ + {"id": 1}, + {"id": 2}, + {"id": 3, "nested": [{"id": 1}, {"id": 2}]}, + ], + table_name="items", +) +``` -## Load +This is what happens when the `run` method is executed: -The data is then loaded into your chosen [destination](../../general-usage/glossary.md#destination). -`dlt` uses configurable, idempotent, atomic loads that ensure data safely ends up there. For -example, you don't need to worry about the size of the data you are loading, and if the process is -interrupted, it is safe to retry without creating errors. +1. [Extract](how-dlt-works.md#extract) - Fully extracts the data from your source to your hard drive. In the example above, an implicit source with one resource with 3 items is created and extracted. +2. [Normalize](how-dlt-works.md#normalize) - Inspects and normalizes your data and computes a schema compatible with your destination. For the example above, the normalizer will detect one column `id` of type `int` in one table named `items`, it will furthermore detect a nested list in table items and unnest it into a child table named `items__nested`. +3. [Load](how-dlt-works#load) - Runs schema migrations if necessary on your destination and loads your data into the destination. For the example above, a new dataset on a local duckdb database is created that contains the two tables discovered in the previous steps. +## The three phases + +### Extract + +Extract can be run individually with the `extract` command on the pipeline: + +```py +pipeline.extract(data) +``` + +During the extract phase, `dlt` fully extracts the data from your [sources](../../dlt-ecosystem/verified-sources) to your hard drive into a new [load package](../../general-usage/destination-tables#load-packages-and-load-ids), which will be assigned a unique ID and will contain your raw data as received from your sources. Additionally, you can [supply schema hints](../../general-usage/resource#define-schema) to define the data types of some of the columns or add a primary key and unique indexes. You can also control this phase by [limiting](../../general-usage/resource#sample-from-large-data) the number of items extracted in one run, using [incremental cursor fields](../../general-usage/incremental-loading#incremental-loading-with-a-cursor-field), and by tuning the performance with [parallelization](../../reference/performance#extract). You can also apply filters and maps to [obfuscate](../../general-usage/customising-pipelines/pseudonymizing_columns) or [remove](../../general-usage/customising-pipelines/removing_columns) personal data, and you can use [transformers](../../examples/transformers) to create derivative data. + +### Normalize + +Normalize can be run individually with the `normalize` command on the pipeline. Normalize is dependent on having a completed extract phase and will not do anything if there is no extracted data. + +```py +pipeline.normalize() +``` + +During the normalization phase, `dlt` inspects and normalizes your data and computes a [schema](../../general-usage/schema) corresponding to the input data. The schema will automatically evolve to accommodate any future source data changes like new columns or tables. `dlt` will also unnest nested data structures into child tables and create variant columns if detected values do not match a schema computed during a previous run. The result of the normalization phase is an updated load package that holds your normalized data in a format your destination understands and a full schema which can be used to migrate your data to your destination. You can control the normalization phase, for example, by [defining the allowed nesting level](../../general-usage/source#reduce-the-nesting-level-of-generated-tables) of input data, by [applying schema contracts](../../general-usage/schema-contracts) that govern how the schema might evolve, and how rows that do not fit are treated. Performance settings are [also available](../../reference/performance#normalize). + +### Load + +Load can be run individually with the `load` command on the pipeline. Load is dependent on having a completed normalize phase and will not do anything if there is no normalized data. + +```py +pipeline.load() +``` + +During the loading phase, `dlt` first runs schema migrations as needed on your destination and then loads your data into the destination. `dlt` will load your data in smaller chunks called load jobs to be able to parallelize large loads. If the connection to the destination fails, it is safe to rerun the pipeline, and `dlt` will continue to load all load jobs from the current load package. `dlt` will also create special tables that store the internal dlt schema, information about all load packages, and some state information which, among other things, are used by the incrementals to be able to restore the incremental state from a previous run to another machine. Some ways to control the loading phase are by using different [`write_dispositions`](../../general-usage/incremental-loading#choosing-a-write-disposition) to replace the data in the destination, simply append to it, or merge on certain merge keys that you can configure per table. For some destinations, you can use a remote staging dataset on a bucket provider, and `dlt` even supports modern open table formats like [deltables and iceberg](../../dlt-ecosystem/destinations/delta-iceberg), and [reverse ETL](../../dlt-ecosystem/destinations/destination) is also possible. + +## Other notable `dlt` features + +* `dlt` is simply a Python package, so it will run [everywhere that Python runs](../../walkthroughs/deploy-a-pipeline) — locally, in notebooks, on orchestrators — you name it. +* `dlt` allows you to build and test your data pipelines locally with `duckdb` and then switch out the destination for deployment. +* `dlt` provides a user-friendly interface for [accessing your data in Python](../../general-usage/dataset-access/dataset), using [a Streamlit app](../../general-usage/dataset-access/streamlit), and leveraging [integrations](../../general-usage/dataset-access/ibis-backend) with the fabulous Ibis library. All of this even works on data lakes provided by bucket storage providers. +* `dlt` fully manages schema migrations on your destinations. You don’t even need to know how to use SQL to update your schema. It also supports [schema contracts](../../general-usage/schema-contracts) to govern how the schema might evolve. +* `dlt` offers numerous options for [monitoring and tracing](../../running-in-production/monitoring) what is happening during your loads. +* `dlt` supports you when you need to [transform your data](../../dlt-ecosystem/transformations) after the load, whether with dbt or in Python using Arrow tables and pandas DataFrames. diff --git a/docs/website/static/img/dlt-onepager.png b/docs/website/static/img/dlt-onepager.png new file mode 100644 index 0000000000000000000000000000000000000000..57b5d3f6581ec7903644d5cf7457c8e311dcede4 GIT binary patch literal 692945 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

O-(M*R*@B~DQVPuM9^ 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ˆ?e7f+Lw9EAZgGVLf+ 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Õ^xo2ir^sw_9wHR+~% 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

cRd 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%0A-;Mq21AAd$|`Mbakc=p5c2Fh75$ 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"#R2s!=_6ng2pX0 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 +F{o^3`qsJ*9_svVP zn^y4Q1n)BeBW7QG)(Rx)_QO*Ml32xB(PlrZE2tnSv|7{LN^W{uMD3=C*YSE)`R=j% zB~4*yN(x?lQsQ;GaY+hE?4afptIs~o)?@7Iq)$#eLdLq)(lCs16Aog7Nx5*P3EEON zLu2l|n-ChO%8yOX=r2yJ!6Ui(*n6peW{V}?n8iJHzB@&QKfLK@M-#E$o6+x_5!OVLD*H~OiC)A?4%!aj7qYoYfp~5 zi0iJcwROJ2E5(uTz7YvJRHVkdBWd>bBCf|4dTzpSuY%ApJYu_|vhrF|G6LG#3|kQo zgoUNsP~Z;yL-q6T-wO(YFjiLb+3<7t>}>Z$Um?b`?{v2axA3lmr^P3Ff@$&1))Tz} zA7gBM(zY68ai6pD)zNB(@s-hxNJRovU0xpldtyq#@gl`5-jVw9M?K57HZm6;ytF*E z43A+k-=NfaP)|?PPS~1irm!MDsf5SMiE3(nYxil?EfECF6RIem*?L9dbvBhmq{J`; z=Q-ytbmOaLK9%=$RXNrS(H8)(^VeL$@`!r%7<UhkgG)bQ9gvw%zdLw>lsd=F{EWyIB5b;#*mR_beN}|E= zV2dC7*$w@v2RoMDMMKaXLRgtcfH?D1gO*DnyO#Yav zDTtsKc`j*hZ*9OIK}PT5a+3DQ{pDJ@SBb8pKHNcBUS97hTFRg9HaY=MK9bhNKu1^C z@Oha!qd*7;`z{h3&zE4&-nWT!5HOBd$8Yj+iR8Esd}z~{EB`oJMW6mtGOC1o>5JM@Xi=wegN z+UcG|H9(!AQzCTpk@Gz`GM*~wp2?%L`6~x@(=yg3&ofjZ{792FXb+A)1y~u!C$&@C zO2P1p1sI|%U4+C(P+Zp+hm*RposVMri(y-+WV@a4%*d4c`M2U$PZC$RW5nT|vvHE5 zh#d8f#Kic)l*c>W6|kzy?I#$isdU~Ki=Kr~Z}C2762SOcHgkCVfEQ>Qr5(31joPyL zaNFa1xj{`qs6GIBVW$f1odbOrDm~9I7zbxCuyD22G6k&%ORdeju2+Z64}*C{kvh{&iGheN5jRme#HJHUpj4a(E{*>KnA#LU|S*U9w;TOSG$Kg_e}x zttcx2sGsc}Y-k_2p7&4`o-sW7=rA`~zB|~kv;cdHs_^<@b|TRol|EbRe&uP027oH@ zqm8HyL5YivL(5FBi=Z1|QFQB%uwfq{4+4%KJQ#UKHY0Jy{Rs5iI8>04bj$bKL7-!p zo?3pBx%!GT#%GZOJ)l{#G;N$u*fr9Y#~mj>2z!lNan{hcB+75jL>bM&-fbh2Qh{#KqS9EmFH#uAD$$^ag(k~YKgJNbZN$Wg`MJ) z{B3mGov3+<$|^xK^6ZG!{$xQb(=iF?fyCIxJ)Orgn7Xn)Kn5BG`H{`4yh?_BkI7|1 z=WLAnrwqnO6US6$xyBkQosyOF(Rn22F)ysPwxmV`%z!yDkpPH0uTnl>!mSiS zC9!?lXg7_94R~dl@l#Ui85*HIx7_aCeEizU`c({%GFR0&h1}|3x*&8uTn}2keE~SX z{e6N?ZQXdaRv?Mmh#T9uO&1zG(qeXvo^FedZj1PBdF#*q93aV74BZ)aMrJ`Js(k8u z?=PyN!~|cpuUp(#wC@#QcJ0mk$>eJ5;BY?wM#9GWaZ-ia{p6bx-W_P&O2m88L(4dkYJDjl1sa`&CKM;~GHmY&RK( z+2M>d`<`MJd%h=&eHSPS_me(R7S>$6uUmpcoLkFwE>VUN{60^wl%Hm5uSxJ*=gDxS5LCLg|z%j8ILX_5jBMk1Qf%T zPg*=)sTqkyEzHQzDYd1fMdD$T(F*l$XfD#pbc9$K0ZJ O9bKQZZylk!Q7<z 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

@;|5qWUl|I1B~>e}HvxqzJ~n)1+7 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ä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 Date: Tue, 17 Dec 2024 14:50:36 +0100 Subject: [PATCH 21/23] disconnect after running ibis tests (#2157) --- tests/load/test_read_interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/load/test_read_interfaces.py b/tests/load/test_read_interfaces.py index bca844d2c8..756002ef7f 100644 --- a/tests/load/test_read_interfaces.py +++ b/tests/load/test_read_interfaces.py @@ -778,6 +778,7 @@ def test_ibis_dataset_access(populated_pipeline: Pipeline) -> None: items_table = ibis_connection.table(add_table_prefix(map_i("items")), database=dataset_name) assert items_table.count().to_pandas() == total_records + ibis_connection.disconnect() @pytest.mark.no_load From 19f6cf2aee731dc54409320e123f575e7478c8f9 Mon Sep 17 00:00:00 2001 From: Marcin Rudolf Date: Tue, 17 Dec 2024 15:37:09 +0100 Subject: [PATCH 22/23] 2143 adds snowflake hints --- dlt/destinations/impl/bigquery/bigquery.py | 5 +- .../impl/clickhouse/clickhouse.py | 5 +- .../impl/databricks/databricks.py | 6 -- dlt/destinations/impl/dremio/dremio.py | 6 -- dlt/destinations/impl/duckdb/duck.py | 11 --- dlt/destinations/impl/mssql/mssql.py | 6 +- dlt/destinations/impl/postgres/postgres.py | 12 --- dlt/destinations/impl/redshift/redshift.py | 12 +-- .../impl/snowflake/configuration.py | 18 +++++ dlt/destinations/impl/snowflake/snowflake.py | 45 ++++++++--- dlt/destinations/job_client_impl.py | 39 +++++++--- .../dlt-ecosystem/destinations/snowflake.md | 9 +++ .../bigquery/test_bigquery_table_builder.py | 48 ++++++------ tests/load/dremio/test_dremio_client.py | 8 +- tests/load/snowflake/test_snowflake_client.py | 40 +++++++++- .../snowflake/test_snowflake_table_builder.py | 77 +++++++++++++++---- 16 files changed, 224 insertions(+), 123 deletions(-) diff --git a/dlt/destinations/impl/bigquery/bigquery.py b/dlt/destinations/impl/bigquery/bigquery.py index 2b3927e7c9..10a344f768 100644 --- a/dlt/destinations/impl/bigquery/bigquery.py +++ b/dlt/destinations/impl/bigquery/bigquery.py @@ -401,10 +401,7 @@ def _get_info_schema_columns_query( return query, folded_table_names def _get_column_def_sql(self, column: TColumnSchema, table: PreparedTableSchema = None) -> str: - name = self.sql_client.escape_column_name(column["name"]) - column_def_sql = ( - f"{name} {self.type_mapper.to_destination_type(column, table)} {self._gen_not_null(column.get('nullable', True))}" - ) + column_def_sql = super()._get_column_def_sql(column, table) if column.get(ROUND_HALF_EVEN_HINT, False): column_def_sql += " OPTIONS (rounding_mode='ROUND_HALF_EVEN')" if column.get(ROUND_HALF_AWAY_FROM_ZERO_HINT, False): diff --git a/dlt/destinations/impl/clickhouse/clickhouse.py b/dlt/destinations/impl/clickhouse/clickhouse.py index 3a5f5c3e28..a407e56361 100644 --- a/dlt/destinations/impl/clickhouse/clickhouse.py +++ b/dlt/destinations/impl/clickhouse/clickhouse.py @@ -292,11 +292,10 @@ def _get_table_update_sql( return sql - @staticmethod - def _gen_not_null(v: bool) -> str: + def _gen_not_null(self, v: bool) -> str: # ClickHouse fields are not nullable by default. # We use the `Nullable` modifier instead of NULL / NOT NULL modifiers to cater for ALTER statement. - pass + return "" def _from_db_type( self, ch_t: str, precision: Optional[int], scale: Optional[int] diff --git a/dlt/destinations/impl/databricks/databricks.py b/dlt/destinations/impl/databricks/databricks.py index 2bb68a607e..a83db6ec34 100644 --- a/dlt/destinations/impl/databricks/databricks.py +++ b/dlt/destinations/impl/databricks/databricks.py @@ -264,12 +264,6 @@ def _from_db_type( ) -> TColumnType: return self.type_mapper.from_destination_type(bq_t, precision, scale) - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - name = self.sql_client.escape_column_name(c["name"]) - return ( - f"{name} {self.type_mapper.to_destination_type(c,table)} {self._gen_not_null(c.get('nullable', True))}" - ) - def _get_storage_table_query_columns(self) -> List[str]: fields = super()._get_storage_table_query_columns() fields[2] = ( # Override because this is the only way to get data type with precision diff --git a/dlt/destinations/impl/dremio/dremio.py b/dlt/destinations/impl/dremio/dremio.py index ab23f58ab4..e3a090c824 100644 --- a/dlt/destinations/impl/dremio/dremio.py +++ b/dlt/destinations/impl/dremio/dremio.py @@ -151,12 +151,6 @@ def _from_db_type( ) -> TColumnType: return self.type_mapper.from_destination_type(bq_t, precision, scale) - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - name = self.sql_client.escape_column_name(c["name"]) - return ( - f"{name} {self.type_mapper.to_destination_type(c,table)} {self._gen_not_null(c.get('nullable', True))}" - ) - def _create_merge_followup_jobs( self, table_chain: Sequence[PreparedTableSchema] ) -> List[FollowupJobRequest]: diff --git a/dlt/destinations/impl/duckdb/duck.py b/dlt/destinations/impl/duckdb/duck.py index 3bd4c83e1f..2b3370270b 100644 --- a/dlt/destinations/impl/duckdb/duck.py +++ b/dlt/destinations/impl/duckdb/duck.py @@ -74,17 +74,6 @@ def create_load_job( job = DuckDbCopyJob(file_path) return job - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - hints_str = " ".join( - self.active_hints.get(h, "") - for h in self.active_hints.keys() - if c.get(h, False) is True - ) - column_name = self.sql_client.escape_column_name(c["name"]) - return ( - f"{column_name} {self.type_mapper.to_destination_type(c,table)} {hints_str} {self._gen_not_null(c.get('nullable', True))}" - ) - def _from_db_type( self, pq_t: str, precision: Optional[int], scale: Optional[int] ) -> TColumnType: diff --git a/dlt/destinations/impl/mssql/mssql.py b/dlt/destinations/impl/mssql/mssql.py index 27aebe07f2..7b48a6b551 100644 --- a/dlt/destinations/impl/mssql/mssql.py +++ b/dlt/destinations/impl/mssql/mssql.py @@ -115,11 +115,7 @@ def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = Non else: db_type = self.type_mapper.to_destination_type(c, table) - hints_str = " ".join( - self.active_hints.get(h, "") - for h in self.active_hints.keys() - if c.get(h, False) is True - ) + hints_str = self._get_column_hints_sql(c) column_name = self.sql_client.escape_column_name(c["name"]) return f"{column_name} {db_type} {hints_str} {self._gen_not_null(c.get('nullable', True))}" diff --git a/dlt/destinations/impl/postgres/postgres.py b/dlt/destinations/impl/postgres/postgres.py index 2459ee1dbe..3d54b59f93 100644 --- a/dlt/destinations/impl/postgres/postgres.py +++ b/dlt/destinations/impl/postgres/postgres.py @@ -161,18 +161,6 @@ def create_load_job( job = PostgresCsvCopyJob(file_path) return job - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - hints_ = " ".join( - self.active_hints.get(h, "") - for h in self.active_hints.keys() - if c.get(h, False) is True - ) - column_name = self.sql_client.escape_column_name(c["name"]) - nullability = self._gen_not_null(c.get("nullable", True)) - column_type = self.type_mapper.to_destination_type(c, table) - - return f"{column_name} {column_type} {hints_} {nullability}" - def _create_replace_followup_jobs( self, table_chain: Sequence[PreparedTableSchema] ) -> List[FollowupJobRequest]: diff --git a/dlt/destinations/impl/redshift/redshift.py b/dlt/destinations/impl/redshift/redshift.py index 2335166761..b1aa37ce6a 100644 --- a/dlt/destinations/impl/redshift/redshift.py +++ b/dlt/destinations/impl/redshift/redshift.py @@ -153,6 +153,7 @@ def __init__( capabilities, ) super().__init__(schema, config, sql_client) + self.active_hints = HINT_TO_REDSHIFT_ATTR self.sql_client = sql_client self.config: RedshiftClientConfiguration = config self.type_mapper = self.capabilities.get_type_mapper() @@ -162,17 +163,6 @@ def _create_merge_followup_jobs( ) -> List[FollowupJobRequest]: return [RedshiftMergeJob.from_table_chain(table_chain, self.sql_client)] - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - hints_str = " ".join( - HINT_TO_REDSHIFT_ATTR.get(h, "") - for h in HINT_TO_REDSHIFT_ATTR.keys() - if c.get(h, False) is True - ) - column_name = self.sql_client.escape_column_name(c["name"]) - return ( - f"{column_name} {self.type_mapper.to_destination_type(c,table)} {hints_str} {self._gen_not_null(c.get('nullable', True))}" - ) - def create_load_job( self, table: PreparedTableSchema, file_path: str, load_id: str, restore: bool = False ) -> LoadJob: diff --git a/dlt/destinations/impl/snowflake/configuration.py b/dlt/destinations/impl/snowflake/configuration.py index 4a89a1564b..2e589ea095 100644 --- a/dlt/destinations/impl/snowflake/configuration.py +++ b/dlt/destinations/impl/snowflake/configuration.py @@ -138,6 +138,24 @@ class SnowflakeClientConfiguration(DestinationClientDwhWithStagingConfiguration) query_tag: Optional[str] = None """A tag with placeholders to tag sessions executing jobs""" + create_indexes: bool = False + """Whether UNIQUE or PRIMARY KEY constrains should be created""" + + def __init__( + self, + *, + credentials: SnowflakeCredentials = None, + create_indexes: bool = False, + destination_name: str = None, + environment: str = None, + ) -> None: + super().__init__( + credentials=credentials, + destination_name=destination_name, + environment=environment, + ) + self.create_indexes = create_indexes + def fingerprint(self) -> str: """Returns a fingerprint of host part of a connection string""" if self.credentials and self.credentials.host: diff --git a/dlt/destinations/impl/snowflake/snowflake.py b/dlt/destinations/impl/snowflake/snowflake.py index e5146139f2..786cdc0b77 100644 --- a/dlt/destinations/impl/snowflake/snowflake.py +++ b/dlt/destinations/impl/snowflake/snowflake.py @@ -1,6 +1,7 @@ -from typing import Optional, Sequence, List +from typing import Optional, Sequence, List, Dict, Set from urllib.parse import urlparse, urlunparse +from dlt.common import logger from dlt.common.data_writers.configuration import CsvFormatConfiguration from dlt.common.destination import DestinationCapabilitiesContext from dlt.common.destination.reference import ( @@ -15,13 +16,15 @@ AwsCredentialsWithoutDefaults, AzureCredentialsWithoutDefaults, ) +from dlt.common.schema.utils import get_columns_names_with_prop from dlt.common.storages.configuration import FilesystemConfiguration, ensure_canonical_az_url from dlt.common.storages.file_storage import FileStorage -from dlt.common.schema import TColumnSchema, Schema -from dlt.common.schema.typing import TColumnType +from dlt.common.schema import TColumnSchema, Schema, TColumnHint +from dlt.common.schema.typing import TColumnType, TTableSchema from dlt.common.storages.fsspec_filesystem import AZURE_BLOB_STORAGE_PROTOCOLS, S3_PROTOCOLS from dlt.common.typing import TLoaderFileFormat +from dlt.common.utils import uniq_id from dlt.destinations.job_client_impl import SqlJobClientWithStagingDataset from dlt.destinations.exceptions import LoadJobTerminalException @@ -29,6 +32,8 @@ from dlt.destinations.impl.snowflake.sql_client import SnowflakeSqlClient from dlt.destinations.job_impl import ReferenceFollowupJobRequest +SUPPORTED_HINTS: Dict[TColumnHint, str] = {"unique": "UNIQUE"} + class SnowflakeLoadJob(RunnableLoadJob, HasFollowupJobs): def __init__( @@ -238,6 +243,7 @@ def __init__( self.config: SnowflakeClientConfiguration = config self.sql_client: SnowflakeSqlClient = sql_client # type: ignore self.type_mapper = self.capabilities.get_type_mapper() + self.active_hints = SUPPORTED_HINTS if self.config.create_indexes else {} def create_load_job( self, table: PreparedTableSchema, file_path: str, load_id: str, restore: bool = False @@ -264,6 +270,33 @@ def _make_add_column_sql( "ADD COLUMN\n" + ",\n".join(self._get_column_def_sql(c, table) for c in new_columns) ] + def _get_constraints_sql( + self, table_name: str, new_columns: Sequence[TColumnSchema], generate_alter: bool + ) -> str: + # "primary_key": "PRIMARY KEY" + if self.config.create_indexes: + partial: TTableSchema = { + "name": table_name, + "columns": {c["name"]: c for c in new_columns}, + } + # Add PK constraint if pk_columns exist + pk_columns = get_columns_names_with_prop(partial, "primary_key") + if pk_columns: + if generate_alter: + logger.warning( + f"PRIMARY KEY on {table_name} constraint cannot be added in ALTER TABLE and" + " is ignored" + ) + else: + pk_constraint_name = list( + self._norm_and_escape_columns(f"PK_{table_name}_{uniq_id(4)}") + )[0] + quoted_pk_cols = ", ".join( + self.sql_client.escape_column_name(col) for col in pk_columns + ) + return f",\nCONSTRAINT {pk_constraint_name} PRIMARY KEY ({quoted_pk_cols})" + return "" + def _get_table_update_sql( self, table_name: str, @@ -287,11 +320,5 @@ def _from_db_type( ) -> TColumnType: return self.type_mapper.from_destination_type(bq_t, precision, scale) - def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - name = self.sql_client.escape_column_name(c["name"]) - return ( - f"{name} {self.type_mapper.to_destination_type(c,table)} {self._gen_not_null(c.get('nullable', True))}" - ) - def should_truncate_table_before_load_on_staging_destination(self, table_name: str) -> bool: return self.config.truncate_tables_on_staging_destination_before_load diff --git a/dlt/destinations/job_client_impl.py b/dlt/destinations/job_client_impl.py index d1f211b1e9..888c80c006 100644 --- a/dlt/destinations/job_client_impl.py +++ b/dlt/destinations/job_client_impl.py @@ -7,6 +7,7 @@ from typing import ( Any, ClassVar, + Dict, List, Optional, Sequence, @@ -14,21 +15,18 @@ Type, Iterable, Iterator, - Generator, ) import zlib import re -from contextlib import contextmanager -from contextlib import suppress from dlt.common import pendulum, logger +from dlt.common.destination.capabilities import DataTypeMapper from dlt.common.json import json from dlt.common.schema.typing import ( C_DLT_LOAD_ID, COLUMN_HINTS, TColumnType, TColumnSchemaBase, - TTableFormat, ) from dlt.common.schema.utils import ( get_inherited_table_hint, @@ -40,11 +38,11 @@ from dlt.common.storages import FileStorage from dlt.common.storages.load_package import LoadJobInfo, ParsedLoadJobFileName from dlt.common.schema import TColumnSchema, Schema, TTableSchemaColumns, TSchemaTables +from dlt.common.schema import TColumnHint from dlt.common.destination.reference import ( PreparedTableSchema, StateInfo, StorageSchemaInfo, - SupportsReadableDataset, WithStateSync, DestinationClientConfiguration, DestinationClientDwhConfiguration, @@ -55,9 +53,7 @@ JobClientBase, HasFollowupJobs, CredentialsConfiguration, - SupportsReadableRelation, ) -from dlt.destinations.dataset import ReadableDBAPIDataset from dlt.destinations.exceptions import DatabaseUndefinedRelation from dlt.destinations.job_impl import ( @@ -154,6 +150,8 @@ def __init__( self.state_table_columns = ", ".join( sql_client.escape_column_name(col) for col in state_table_["columns"] ) + self.active_hints: Dict[TColumnHint, str] = {} + self.type_mapper: DataTypeMapper = None super().__init__(schema, config, sql_client.capabilities) self.sql_client = sql_client assert isinstance(config, DestinationClientDwhConfiguration) @@ -569,6 +567,7 @@ def _get_table_update_sql( # build CREATE sql = self._make_create_table(qualified_name, table) + " (\n" sql += ",\n".join([self._get_column_def_sql(c, table) for c in new_columns]) + sql += self._get_constraints_sql(table_name, new_columns, generate_alter) sql += ")" sql_result.append(sql) else: @@ -582,8 +581,16 @@ def _get_table_update_sql( sql_result.extend( [sql_base + col_statement for col_statement in add_column_statements] ) + constraints_sql = self._get_constraints_sql(table_name, new_columns, generate_alter) + if constraints_sql: + sql_result.append(constraints_sql) return sql_result + def _get_constraints_sql( + self, table_name: str, new_columns: Sequence[TColumnSchema], generate_alter: bool + ) -> str: + return "" + def _check_table_update_hints( self, table_name: str, new_columns: Sequence[TColumnSchema], generate_alter: bool ) -> None: @@ -613,12 +620,22 @@ def _check_table_update_hints( " existing tables." ) - @abstractmethod def _get_column_def_sql(self, c: TColumnSchema, table: PreparedTableSchema = None) -> str: - pass + hints_ = self._get_column_hints_sql(c) + column_name = self.sql_client.escape_column_name(c["name"]) + nullability = self._gen_not_null(c.get("nullable", True)) + column_type = self.type_mapper.to_destination_type(c, table) + + return f"{column_name} {column_type} {hints_} {nullability}" + + def _get_column_hints_sql(self, c: TColumnSchema) -> str: + return " ".join( + self.active_hints.get(h, "") + for h in self.active_hints.keys() + if c.get(h, False) is True # use ColumnPropInfos to get default value + ) - @staticmethod - def _gen_not_null(nullable: bool) -> str: + def _gen_not_null(self, nullable: bool) -> str: return "NOT NULL" if not nullable else "" def _create_table_update( diff --git a/docs/website/docs/dlt-ecosystem/destinations/snowflake.md b/docs/website/docs/dlt-ecosystem/destinations/snowflake.md index 07cf822973..28684c39ac 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/snowflake.md +++ b/docs/website/docs/dlt-ecosystem/destinations/snowflake.md @@ -200,6 +200,12 @@ Note that we ignore missing columns `ERROR_ON_COLUMN_COUNT_MISMATCH = FALSE` and ## Supported column hints Snowflake supports the following [column hints](../../general-usage/schema#tables-and-columns): * `cluster` - Creates a cluster column(s). Many columns per table are supported and only when a new table is created. +* `unique` - Creates UNIQUE hint on a Snowflake column, can be added to many columns. ([optional](#additional-destination-options)) +* `primary_key` - Creates PRIMARY KEY on selected column(s), may be compound. ([optional](#additional-destination-options)) + +`unique` and `primary_key` are not enforced and `dlt` does not instruct Snowflake to `RELY` on them when +query planning. + ## Table and column identifiers Snowflake supports both case-sensitive and case-insensitive identifiers. All unquoted and uppercase identifiers resolve case-insensitively in SQL statements. Case-insensitive [naming conventions](../../general-usage/naming-convention.md#case-sensitive-and-insensitive-destinations) like the default **snake_case** will generate case-insensitive identifiers. Case-sensitive (like **sql_cs_v1**) will generate @@ -308,6 +314,7 @@ pipeline = dlt.pipeline( ## Additional destination options You can define your own stage to PUT files and disable the removal of the staged files after loading. +You can also opt-in to [create indexes](#supported-column-hints). ```toml [destination.snowflake] @@ -315,6 +322,8 @@ You can define your own stage to PUT files and disable the removal of the staged stage_name="DLT_STAGE" # Whether to keep or delete the staged files after COPY INTO succeeds keep_staged_files=true +# Add UNIQUE and PRIMARY KEY hints to tables +create_indexes=true ``` ### Setting up CSV format diff --git a/tests/load/bigquery/test_bigquery_table_builder.py b/tests/load/bigquery/test_bigquery_table_builder.py index 56a674cfa3..b2857b7c08 100644 --- a/tests/load/bigquery/test_bigquery_table_builder.py +++ b/tests/load/bigquery/test_bigquery_table_builder.py @@ -107,25 +107,25 @@ def test_create_table(gcp_client: BigQueryClient) -> None: sqlfluff.parse(sql, dialect="bigquery") assert sql.startswith("CREATE TABLE") assert "event_test_table" in sql - assert "`col1` INT64 NOT NULL" in sql - assert "`col2` FLOAT64 NOT NULL" in sql - assert "`col3` BOOL NOT NULL" in sql - assert "`col4` TIMESTAMP NOT NULL" in sql + assert "`col1` INT64 NOT NULL" in sql + assert "`col2` FLOAT64 NOT NULL" in sql + assert "`col3` BOOL NOT NULL" in sql + assert "`col4` TIMESTAMP NOT NULL" in sql assert "`col5` STRING " in sql - assert "`col6` NUMERIC(38,9) NOT NULL" in sql + assert "`col6` NUMERIC(38,9) NOT NULL" in sql assert "`col7` BYTES" in sql assert "`col8` BIGNUMERIC" in sql - assert "`col9` JSON NOT NULL" in sql + assert "`col9` JSON NOT NULL" in sql assert "`col10` DATE" in sql assert "`col11` TIME" in sql - assert "`col1_precision` INT64 NOT NULL" in sql - assert "`col4_precision` TIMESTAMP NOT NULL" in sql + assert "`col1_precision` INT64 NOT NULL" in sql + assert "`col4_precision` TIMESTAMP NOT NULL" in sql assert "`col5_precision` STRING(25) " in sql - assert "`col6_precision` NUMERIC(6,2) NOT NULL" in sql + assert "`col6_precision` NUMERIC(6,2) NOT NULL" in sql assert "`col7_precision` BYTES(19)" in sql - assert "`col11_precision` TIME NOT NULL" in sql - assert "`col_high_p_decimal` BIGNUMERIC(76,0) NOT NULL" in sql - assert "`col_high_s_decimal` BIGNUMERIC(38,24) NOT NULL" in sql + assert "`col11_precision` TIME NOT NULL" in sql + assert "`col_high_p_decimal` BIGNUMERIC(76,0) NOT NULL" in sql + assert "`col_high_s_decimal` BIGNUMERIC(38,24) NOT NULL" in sql assert "CLUSTER BY" not in sql assert "PARTITION BY" not in sql @@ -137,29 +137,29 @@ def test_alter_table(gcp_client: BigQueryClient) -> None: assert sql.startswith("ALTER TABLE") assert sql.count("ALTER TABLE") == 1 assert "event_test_table" in sql - assert "ADD COLUMN `col1` INT64 NOT NULL" in sql - assert "ADD COLUMN `col2` FLOAT64 NOT NULL" in sql - assert "ADD COLUMN `col3` BOOL NOT NULL" in sql - assert "ADD COLUMN `col4` TIMESTAMP NOT NULL" in sql + assert "ADD COLUMN `col1` INT64 NOT NULL" in sql + assert "ADD COLUMN `col2` FLOAT64 NOT NULL" in sql + assert "ADD COLUMN `col3` BOOL NOT NULL" in sql + assert "ADD COLUMN `col4` TIMESTAMP NOT NULL" in sql assert "ADD COLUMN `col5` STRING" in sql - assert "ADD COLUMN `col6` NUMERIC(38,9) NOT NULL" in sql + assert "ADD COLUMN `col6` NUMERIC(38,9) NOT NULL" in sql assert "ADD COLUMN `col7` BYTES" in sql assert "ADD COLUMN `col8` BIGNUMERIC" in sql - assert "ADD COLUMN `col9` JSON NOT NULL" in sql + assert "ADD COLUMN `col9` JSON NOT NULL" in sql assert "ADD COLUMN `col10` DATE" in sql assert "ADD COLUMN `col11` TIME" in sql - assert "ADD COLUMN `col1_precision` INT64 NOT NULL" in sql - assert "ADD COLUMN `col4_precision` TIMESTAMP NOT NULL" in sql + assert "ADD COLUMN `col1_precision` INT64 NOT NULL" in sql + assert "ADD COLUMN `col4_precision` TIMESTAMP NOT NULL" in sql assert "ADD COLUMN `col5_precision` STRING(25)" in sql - assert "ADD COLUMN `col6_precision` NUMERIC(6,2) NOT NULL" in sql + assert "ADD COLUMN `col6_precision` NUMERIC(6,2) NOT NULL" in sql assert "ADD COLUMN `col7_precision` BYTES(19)" in sql - assert "ADD COLUMN `col11_precision` TIME NOT NULL" in sql + assert "ADD COLUMN `col11_precision` TIME NOT NULL" in sql # table has col1 already in storage mod_table = deepcopy(TABLE_UPDATE) mod_table.pop(0) sql = gcp_client._get_table_update_sql("event_test_table", mod_table, True)[0] - assert "ADD COLUMN `col1` INTEGER NOT NULL" not in sql - assert "ADD COLUMN `col2` FLOAT64 NOT NULL" in sql + assert "ADD COLUMN `col1` INTEGER NOT NULL" not in sql + assert "ADD COLUMN `col2` FLOAT64 NOT NULL" in sql def test_create_table_case_insensitive(ci_gcp_client: BigQueryClient) -> None: diff --git a/tests/load/dremio/test_dremio_client.py b/tests/load/dremio/test_dremio_client.py index efc72c0652..98212efb13 100644 --- a/tests/load/dremio/test_dremio_client.py +++ b/tests/load/dremio/test_dremio_client.py @@ -48,12 +48,12 @@ def test_dremio_factory() -> None: [ TColumnSchema(name="foo", data_type="text", partition=True), TColumnSchema(name="bar", data_type="bigint", sort=True), - TColumnSchema(name="baz", data_type="double"), + TColumnSchema(name="baz", data_type="double", nullable=False), ], False, [ 'CREATE TABLE "test_database"."test_dataset"."event_test_table"' - ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE )\nPARTITION BY' + ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE NOT NULL)\nPARTITION BY' ' ("foo")\nLOCALSORT BY ("bar")' ], ), @@ -66,7 +66,7 @@ def test_dremio_factory() -> None: False, [ 'CREATE TABLE "test_database"."test_dataset"."event_test_table"' - ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE )\nPARTITION BY' + ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE )\nPARTITION BY' ' ("foo","bar")' ], ), @@ -79,7 +79,7 @@ def test_dremio_factory() -> None: False, [ 'CREATE TABLE "test_database"."test_dataset"."event_test_table"' - ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE )' + ' (\n"foo" VARCHAR ,\n"bar" BIGINT ,\n"baz" DOUBLE )' ], ), ], diff --git a/tests/load/snowflake/test_snowflake_client.py b/tests/load/snowflake/test_snowflake_client.py index aebf514b56..674e01ba31 100644 --- a/tests/load/snowflake/test_snowflake_client.py +++ b/tests/load/snowflake/test_snowflake_client.py @@ -1,14 +1,17 @@ +from copy import deepcopy import os from typing import Iterator from pytest_mock import MockerFixture import pytest -from dlt.destinations.impl.snowflake.snowflake import SnowflakeClient +from dlt.common.schema.schema import Schema +from dlt.destinations.impl.snowflake.snowflake import SUPPORTED_HINTS, SnowflakeClient from dlt.destinations.job_client_impl import SqlJobClientBase from dlt.destinations.sql_client import TJobQueryTags -from tests.load.utils import yield_client_with_storage +from tests.cases import TABLE_UPDATE +from tests.load.utils import yield_client_with_storage, empty_schema # mark all tests as essential, do not remove pytestmark = pytest.mark.essential @@ -32,6 +35,39 @@ def client() -> Iterator[SqlJobClientBase]: yield from yield_client_with_storage("snowflake") +def test_create_table_with_hints(client: SnowflakeClient, empty_schema: Schema) -> None: + mod_update = deepcopy(TABLE_UPDATE[:11]) + # mock hints + client.config.create_indexes = True + client.active_hints = SUPPORTED_HINTS + client.schema = empty_schema + + mod_update[0]["primary_key"] = True + mod_update[5]["primary_key"] = True + + mod_update[0]["sort"] = True + mod_update[4]["parent_key"] = True + + # unique constraints are always single columns + mod_update[1]["unique"] = True + mod_update[7]["unique"] = True + + sql = ";".join(client._get_table_update_sql("event_test_table", mod_update, False)) + + print(sql) + client.sql_client.execute_sql(sql) + + # generate alter table + mod_update = deepcopy(TABLE_UPDATE[11:]) + mod_update[0]["primary_key"] = True + mod_update[1]["unique"] = True + + sql = ";".join(client._get_table_update_sql("event_test_table", mod_update, True)) + + print(sql) + client.sql_client.execute_sql(sql) + + def test_query_tag(client: SnowflakeClient, mocker: MockerFixture): assert client.config.query_tag == QUERY_TAG # make sure we generate proper query diff --git a/tests/load/snowflake/test_snowflake_table_builder.py b/tests/load/snowflake/test_snowflake_table_builder.py index 1fc0034f43..43d4395188 100644 --- a/tests/load/snowflake/test_snowflake_table_builder.py +++ b/tests/load/snowflake/test_snowflake_table_builder.py @@ -6,7 +6,7 @@ from dlt.common.utils import uniq_id from dlt.common.schema import Schema, utils from dlt.destinations import snowflake -from dlt.destinations.impl.snowflake.snowflake import SnowflakeClient +from dlt.destinations.impl.snowflake.snowflake import SnowflakeClient, SUPPORTED_HINTS from dlt.destinations.impl.snowflake.configuration import ( SnowflakeClientConfiguration, SnowflakeCredentials, @@ -66,16 +66,63 @@ def test_create_table(snowflake_client: SnowflakeClient) -> None: assert sql.strip().startswith("CREATE TABLE") assert "EVENT_TEST_TABLE" in sql - assert '"COL1" NUMBER(19,0) NOT NULL' in sql - assert '"COL2" FLOAT NOT NULL' in sql - assert '"COL3" BOOLEAN NOT NULL' in sql - assert '"COL4" TIMESTAMP_TZ NOT NULL' in sql + assert '"COL1" NUMBER(19,0) NOT NULL' in sql + assert '"COL2" FLOAT NOT NULL' in sql + assert '"COL3" BOOLEAN NOT NULL' in sql + assert '"COL4" TIMESTAMP_TZ NOT NULL' in sql assert '"COL5" VARCHAR' in sql - assert '"COL6" NUMBER(38,9) NOT NULL' in sql + assert '"COL6" NUMBER(38,9) NOT NULL' in sql assert '"COL7" BINARY' in sql assert '"COL8" NUMBER(38,0)' in sql - assert '"COL9" VARIANT NOT NULL' in sql - assert '"COL10" DATE NOT NULL' in sql + assert '"COL9" VARIANT NOT NULL' in sql + assert '"COL10" DATE NOT NULL' in sql + + +def test_create_table_with_hints(snowflake_client: SnowflakeClient) -> None: + mod_update = deepcopy(TABLE_UPDATE[:11]) + # mock hints + snowflake_client.config.create_indexes = True + snowflake_client.active_hints = SUPPORTED_HINTS + + mod_update[0]["primary_key"] = True + mod_update[5]["primary_key"] = True + + mod_update[0]["sort"] = True + + # unique constraints are always single columns + mod_update[1]["unique"] = True + mod_update[7]["unique"] = True + + mod_update[4]["parent_key"] = True + + sql = ";".join(snowflake_client._get_table_update_sql("event_test_table", mod_update, False)) + + assert sql.strip().startswith("CREATE TABLE") + assert "EVENT_TEST_TABLE" in sql + assert '"COL1" NUMBER(19,0) NOT NULL' in sql + assert '"COL2" FLOAT UNIQUE NOT NULL' in sql + assert '"COL3" BOOLEAN NOT NULL' in sql + assert '"COL4" TIMESTAMP_TZ NOT NULL' in sql + assert '"COL5" VARCHAR' in sql + assert '"COL6" NUMBER(38,9) NOT NULL' in sql + assert '"COL7" BINARY' in sql + assert '"COL8" NUMBER(38,0) UNIQUE' in sql + assert '"COL9" VARIANT NOT NULL' in sql + assert '"COL10" DATE NOT NULL' in sql + + # PRIMARY KEY constraint + assert 'CONSTRAINT "PK_EVENT_TEST_TABLE_' in sql + assert 'PRIMARY KEY ("COL1", "COL6")' in sql + + # generate alter + mod_update = deepcopy(TABLE_UPDATE[11:]) + mod_update[0]["primary_key"] = True + mod_update[1]["unique"] = True + + sql = ";".join(snowflake_client._get_table_update_sql("event_test_table", mod_update, True)) + # PK constraint ignored for alter + assert "PRIMARY KEY" not in sql + assert '"COL2_NULL" FLOAT UNIQUE' in sql def test_alter_table(snowflake_client: SnowflakeClient) -> None: @@ -90,15 +137,15 @@ def test_alter_table(snowflake_client: SnowflakeClient) -> None: assert sql.count("ALTER TABLE") == 1 assert sql.count("ADD COLUMN") == 1 assert '"EVENT_TEST_TABLE"' in sql - assert '"COL1" NUMBER(19,0) NOT NULL' in sql - assert '"COL2" FLOAT NOT NULL' in sql - assert '"COL3" BOOLEAN NOT NULL' in sql - assert '"COL4" TIMESTAMP_TZ NOT NULL' in sql + assert '"COL1" NUMBER(19,0) NOT NULL' in sql + assert '"COL2" FLOAT NOT NULL' in sql + assert '"COL3" BOOLEAN NOT NULL' in sql + assert '"COL4" TIMESTAMP_TZ NOT NULL' in sql assert '"COL5" VARCHAR' in sql - assert '"COL6" NUMBER(38,9) NOT NULL' in sql + assert '"COL6" NUMBER(38,9) NOT NULL' in sql assert '"COL7" BINARY' in sql assert '"COL8" NUMBER(38,0)' in sql - assert '"COL9" VARIANT NOT NULL' in sql + assert '"COL9" VARIANT NOT NULL' in sql assert '"COL10" DATE' in sql mod_table = deepcopy(TABLE_UPDATE) @@ -106,7 +153,7 @@ def test_alter_table(snowflake_client: SnowflakeClient) -> None: sql = snowflake_client._get_table_update_sql("event_test_table", mod_table, True)[0] assert '"COL1"' not in sql - assert '"COL2" FLOAT NOT NULL' in sql + assert '"COL2" FLOAT NOT NULL' in sql def test_create_table_case_sensitive(cs_client: SnowflakeClient) -> None: From 38d0dab53929af83a0ee51369a8acc0716986b8f Mon Sep 17 00:00:00 2001 From: Marcin Rudolf Date: Tue, 17 Dec 2024 16:06:01 +0100 Subject: [PATCH 23/23] bumps for version 1.5.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d12073601d..646ed215a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dlt" -version = "1.4.1" +version = "1.5.0" description = "dlt is an open-source python-first scalable data loading library that does not require any backend to run." authors = ["dltHub Inc. "] maintainers = [ "Marcin Rudolf ", "Adrian Brudaru ", "Anton Burnashev ", "David Scharf " ]