ducklakepy 0.1.0.dev0__tar.gz → 0.1.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/PKG-INFO +7 -8
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/README.md +6 -7
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/pyproject.toml +1 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_arrow.py +34 -9
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_defaults.py +43 -2
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_hive.py +3 -4
- ducklakepy-0.1.0.dev1/src/ducklakepy/_inlined_lists.py +113 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_inlined_text.py +14 -18
- ducklakepy-0.1.0.dev1/src/ducklakepy/_interval.py +100 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_parquet_sink.py +11 -4
- ducklakepy-0.1.0.dev1/src/ducklakepy/_parquet_types.py +246 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_promotions.py +7 -8
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_version.py +1 -1
- ducklakepy-0.1.0.dev1/src/ducklakepy/_virtual_columns.py +159 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/add_files.py +141 -84
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/capabilities.py +40 -52
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/__init__.py +130 -130
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_dialect.py +40 -29
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_schema.py +10 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_statements.py +2 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/changes.py +5 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/constants.py +1 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/encryption.py +9 -14
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/exceptions.py +1 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/expressions.py +15 -28
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/__init__.py +2 -2
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_bounds.py +2 -2
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_common.py +3 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/compaction.py +4 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/expire.py +10 -9
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/options.py +94 -112
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/__init__.py +78 -64
- ducklakepy-0.1.0.dev1/src/ducklakepy/partitioning/_hash_text.py +268 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/_value_text.py +13 -3
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/plan.py +30 -67
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/scan.py +48 -11
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/schema.py +63 -77
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/schema_update.py +119 -174
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/snapshots.py +45 -23
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/sorting.py +63 -77
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_feed.py +5 -6
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_write.py +17 -9
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/__init__.py +79 -157
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_footer.py +51 -10
- ducklakepy-0.1.0.dev1/src/ducklakepy/stats/_geometry.py +216 -0
- ducklakepy-0.1.0.dev1/src/ducklakepy/stats/_table.py +192 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_temporal.py +49 -3
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/table.py +43 -34
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/tags.py +5 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/__init__.py +217 -271
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_commit_sql.py +10 -14
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_conflicts.py +5 -3
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_keys.py +3 -2
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_null_bounds.py +2 -6
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_operations.py +3 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_table_stats.py +2 -32
- ducklakepy-0.1.0.dev1/src/ducklakepy/transaction/_upsert.py +185 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/types.py +3 -4
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/version.py +6 -8
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/views.py +19 -17
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/write_options.py +39 -41
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/writer.py +27 -6
- ducklakepy-0.1.0.dev1/tests/test_add_files_interval_variant.py +145 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_capabilities.py +38 -41
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_ddl.py +1 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_lifecycle.py +52 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_cross_impl.py +78 -40
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_dialect.py +25 -1
- ducklakepy-0.1.0.dev1/tests/test_end_of_day.py +218 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_footer_stats.py +96 -6
- ducklakepy-0.1.0.dev1/tests/test_inlined_lists.py +202 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_nested.py +13 -9
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_variants.py +75 -24
- ducklakepy-0.1.0.dev1/tests/test_interval_variant_files.py +291 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_expire.py +11 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_module_size.py +1 -1
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_writes.py +2 -2
- ducklakepy-0.1.0.dev1/tests/test_partition_key_text.py +59 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partitioning.py +115 -8
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_add_files.py +33 -4
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_alter.py +4 -4
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_stats.py +66 -7
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_sorting.py +4 -5
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spark.py +23 -3
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_stats.py +3 -3
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_transaction_ops.py +77 -15
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_type_writes.py +66 -7
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_upsert.py +143 -3
- ducklakepy-0.1.0.dev1/tests/test_virtual_columns.py +118 -0
- ducklakepy-0.1.0.dev0/src/ducklakepy/_interval.py +0 -63
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/.gitignore +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/LICENSE +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/__init__.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_changes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_encodings.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_globs.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_interval_text.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_json_list.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_lists.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_literals.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_logical_values.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_names.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_packing.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_parallel.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_paths.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_sources.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_thrift.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_time_flags.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_variant.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_variant_shredding.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_wkb.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_engine.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_entries.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_legacy_inlined.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_migration.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_namespaces.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/config.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/inlined.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/io/__init__.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/io/pyarrow.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_versions.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/checkpoint.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/cleanup.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/orphans.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/_bounds.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/properties.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/puffin.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/py.typed +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/rows.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/__init__.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_options.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_read.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_schemas.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_source.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_blob_text.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_skipped.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_companions.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_delete_files.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_entries.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_float_bounds.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_namespaces.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_staged.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/__init__.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/conftest.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/fileio.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/lake.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_arrow_mapping.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_azure_fileio.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_read.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_change_feed.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_changes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_ci_shards.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_cloud_stores.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_concurrent.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_concurrent_read.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_config.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_conflict_detection.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_count.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_database_pool.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_delete.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_dictionary_encoding.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_drop.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_encryption.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_expressions.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_gcs_fileio.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_infinite_temporals.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_text.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_writes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_io.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_checkpoint.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_cleanup.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_compaction.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_orphans.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_multi_file_writes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_predicates.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_schemas.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_options.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partial_files.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partition_bounds.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_paths.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_puffin.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_deletes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_inlining.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_migration.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_partitioning.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_paths.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_rowid.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_snapshot_info.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_sorting.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_table_changes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_time_travel.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_transactions.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_update.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_views.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_row_group_pruning.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_rows.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_scan.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_scan_batches.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_schema.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_schema_evolution.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_sequences.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_shredded_variants.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_skip_stats.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spark_packing.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spec_version.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_table_append.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_update.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_variant_bytes.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_variant_shredding.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_views.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_write_measurement.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_write_validation.py +0 -0
- {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/writing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ducklakepy
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
4
|
Summary: A Python implementation of the DuckLake specification
|
|
5
5
|
Project-URL: Homepage, https://dejii.github.io/ducklake-python/
|
|
6
6
|
Project-URL: Documentation, https://dejii.github.io/ducklake-python/
|
|
@@ -34,11 +34,10 @@ lakehouse format published at [ducklake.select](https://ducklake.select).
|
|
|
34
34
|
|
|
35
35
|
DuckLake keeps a table's metadata in a SQL database and its rows in Parquet files.
|
|
36
36
|
ducklakepy speaks to both directly, so a Python program can create tables, append Arrow
|
|
37
|
-
data, read at any snapshot, update and delete rows and evolve schemas
|
|
38
|
-
process. A
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
role for DuckLake.
|
|
37
|
+
data, read at any snapshot, update and delete rows and evolve schemas with no query engine
|
|
38
|
+
in the process. A lake written by ducklakepy is readable by DuckDB's `ducklake` extension,
|
|
39
|
+
and the other way round, so the two can share one lake. If you know PyIceberg, ducklakepy
|
|
40
|
+
plays the same role for DuckLake ([the calls side by side](https://dejii.github.io/ducklake-python/docs/getting-started/from-pyiceberg/)).
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
43
|
pip install ducklakepy[postgres] # with the Postgres catalog driver
|
|
@@ -97,9 +96,9 @@ To run the site locally, `make docs` (Node 22.12 or newer; there is a `.nvmrc`)
|
|
|
97
96
|
|
|
98
97
|
| | |
|
|
99
98
|
|---|---|
|
|
100
|
-
| [Getting started](https://dejii.github.io/ducklake-python/docs/getting-started/) | Install,
|
|
99
|
+
| [Getting started](https://dejii.github.io/ducklake-python/docs/getting-started/) | Install, a quickstart on SQLite, moving to Postgres and S3, coming from PyIceberg, and troubleshooting |
|
|
101
100
|
| [Concepts](https://dejii.github.io/ducklake-python/docs/concepts/) | How DuckLake works: snapshots, tables and schemas, where rows live, the ducklakepy object model, a glossary |
|
|
102
|
-
| [Guides](https://dejii.github.io/ducklake-python/docs/guides/
|
|
101
|
+
| [Guides](https://dejii.github.io/ducklake-python/docs/guides/) | One task per page: connect to a catalog, create tables, write, read, time travel, change feeds, update, delete, evolve a schema, transactions, partition, sort, flush, maintain, register existing files, object storage, encryption, use with DuckDB, distributed reads, Spark |
|
|
103
102
|
| [Reference](https://dejii.github.io/ducklake-python/docs/reference/support/) | Feature support by table layout, types, the expression DSL, configuration, write options, errors, catalog backends |
|
|
104
103
|
| [API reference](https://dejii.github.io/ducklake-python/docs/api/) | Every public module, generated from the docstrings |
|
|
105
104
|
| [Internals](https://dejii.github.io/ducklake-python/docs/internals/) | How it works underneath the API, and why |
|
|
@@ -5,11 +5,10 @@ lakehouse format published at [ducklake.select](https://ducklake.select).
|
|
|
5
5
|
|
|
6
6
|
DuckLake keeps a table's metadata in a SQL database and its rows in Parquet files.
|
|
7
7
|
ducklakepy speaks to both directly, so a Python program can create tables, append Arrow
|
|
8
|
-
data, read at any snapshot, update and delete rows and evolve schemas
|
|
9
|
-
process. A
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
role for DuckLake.
|
|
8
|
+
data, read at any snapshot, update and delete rows and evolve schemas with no query engine
|
|
9
|
+
in the process. A lake written by ducklakepy is readable by DuckDB's `ducklake` extension,
|
|
10
|
+
and the other way round, so the two can share one lake. If you know PyIceberg, ducklakepy
|
|
11
|
+
plays the same role for DuckLake ([the calls side by side](https://dejii.github.io/ducklake-python/docs/getting-started/from-pyiceberg/)).
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
14
|
pip install ducklakepy[postgres] # with the Postgres catalog driver
|
|
@@ -68,9 +67,9 @@ To run the site locally, `make docs` (Node 22.12 or newer; there is a `.nvmrc`)
|
|
|
68
67
|
|
|
69
68
|
| | |
|
|
70
69
|
|---|---|
|
|
71
|
-
| [Getting started](https://dejii.github.io/ducklake-python/docs/getting-started/) | Install,
|
|
70
|
+
| [Getting started](https://dejii.github.io/ducklake-python/docs/getting-started/) | Install, a quickstart on SQLite, moving to Postgres and S3, coming from PyIceberg, and troubleshooting |
|
|
72
71
|
| [Concepts](https://dejii.github.io/ducklake-python/docs/concepts/) | How DuckLake works: snapshots, tables and schemas, where rows live, the ducklakepy object model, a glossary |
|
|
73
|
-
| [Guides](https://dejii.github.io/ducklake-python/docs/guides/
|
|
72
|
+
| [Guides](https://dejii.github.io/ducklake-python/docs/guides/) | One task per page: connect to a catalog, create tables, write, read, time travel, change feeds, update, delete, evolve a schema, transactions, partition, sort, flush, maintain, register existing files, object storage, encryption, use with DuckDB, distributed reads, Spark |
|
|
74
73
|
| [Reference](https://dejii.github.io/ducklake-python/docs/reference/support/) | Feature support by table layout, types, the expression DSL, configuration, write options, errors, catalog backends |
|
|
75
74
|
| [API reference](https://dejii.github.io/ducklake-python/docs/api/) | Every public module, generated from the docstrings |
|
|
76
75
|
| [Internals](https://dejii.github.io/ducklake-python/docs/internals/) | How it works underneath the API, and why |
|
|
@@ -19,13 +19,14 @@ own and each become a Parquet column of their own.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import uuid as _uuid
|
|
22
|
+
from datetime import UTC, datetime
|
|
22
23
|
from decimal import Decimal
|
|
23
24
|
from typing import TYPE_CHECKING, Any
|
|
24
25
|
|
|
25
26
|
import pyarrow as pa
|
|
26
27
|
import pyarrow.compute as pc
|
|
27
28
|
|
|
28
|
-
from ducklakepy._defaults import assign_default, string_literal_value
|
|
29
|
+
from ducklakepy._defaults import assign_default, expression_default_values, string_literal_value
|
|
29
30
|
from ducklakepy._interval import INTERVAL, decoded_interval
|
|
30
31
|
from ducklakepy._lists import as_offset_list, is_parquet_list
|
|
31
32
|
from ducklakepy._logical_values import (
|
|
@@ -42,7 +43,7 @@ from ducklakepy.constants import PARQUET_FIELD_ID_KEY
|
|
|
42
43
|
from ducklakepy.exceptions import DuckLakeError, SchemaMismatchError
|
|
43
44
|
from ducklakepy.schema import Schema, check_unique_column_names, fold_identifier
|
|
44
45
|
from ducklakepy.stats import as_infinite, infinite_text
|
|
45
|
-
from ducklakepy.stats._temporal import far_temporal
|
|
46
|
+
from ducklakepy.stats._temporal import END_OF_DAY, far_temporal
|
|
46
47
|
from ducklakepy.types import (
|
|
47
48
|
BlobType,
|
|
48
49
|
BooleanType,
|
|
@@ -239,9 +240,13 @@ def python_values(column: pa.Array | pa.ChunkedArray, field_type: DuckLakeType)
|
|
|
239
240
|
An infinite DATE or TIMESTAMP is :class:`~ducklakepy.stats.Infinite` and
|
|
240
241
|
one beyond ``date``'s years
|
|
241
242
|
:class:`~ducklakepy.stats._temporal.FarTemporal`, whose text is what the
|
|
242
|
-
row stores.
|
|
243
|
+
row stores. A TIME of ``24:00:00`` is
|
|
244
|
+
:data:`~ducklakepy.stats._temporal.END_OF_DAY`, which pyarrow would hand
|
|
245
|
+
back as ``00:00:00``."""
|
|
243
246
|
if isinstance(field_type, (TimestampNsType, TimestampTzNsType, TimeNsType)):
|
|
244
247
|
column = column.cast(pa.int64())
|
|
248
|
+
if isinstance(field_type, TimeType):
|
|
249
|
+
return _times(column)
|
|
245
250
|
try:
|
|
246
251
|
values: list[Any] = column.to_pylist()
|
|
247
252
|
except (OverflowError, ValueError):
|
|
@@ -252,6 +257,14 @@ def python_values(column: pa.Array | pa.ChunkedArray, field_type: DuckLakeType)
|
|
|
252
257
|
return values
|
|
253
258
|
|
|
254
259
|
|
|
260
|
+
def _times(column: pa.Array | pa.ChunkedArray) -> list[Any]:
|
|
261
|
+
values: list[Any] = column.to_pylist()
|
|
262
|
+
ends = pc.equal(column.cast(pa.time64("us")).cast(pa.int64()), END_OF_DAY)
|
|
263
|
+
for position in pc.indices_nonzero(pc.fill_null(ends, False)).to_pylist():
|
|
264
|
+
values[position] = END_OF_DAY
|
|
265
|
+
return values
|
|
266
|
+
|
|
267
|
+
|
|
255
268
|
def _temporal_python_value(field_type: DuckLakeType, arrow_type: pa.DataType, raw: int | None) -> Any:
|
|
256
269
|
if raw is None:
|
|
257
270
|
return None
|
|
@@ -262,12 +275,14 @@ def _temporal_python_value(field_type: DuckLakeType, arrow_type: pa.DataType, ra
|
|
|
262
275
|
return pa.scalar(raw, type=arrow_type).as_py()
|
|
263
276
|
|
|
264
277
|
|
|
265
|
-
def align_to_schema(arrow_table: pa.Table, schema: Schema) -> pa.Table:
|
|
278
|
+
def align_to_schema(arrow_table: pa.Table, schema: Schema, *, moment: datetime | None = None) -> pa.Table:
|
|
266
279
|
"""Reorder, fill and cast ``arrow_table`` so it matches ``schema`` exactly.
|
|
267
280
|
|
|
268
281
|
Unknown columns are an error; a missing column takes its ``default_value``
|
|
269
282
|
(or NULL when it has none and is nullable), unless that default is an
|
|
270
|
-
expression
|
|
283
|
+
expression only a SQL engine can evaluate (``now()`` and the few others
|
|
284
|
+
:func:`~ducklakepy._defaults.expression_default_values` names are taken
|
|
285
|
+
at ``moment``, the transaction's time, or now); values are cast to the
|
|
271
286
|
column's canonical Arrow type with pyarrow's safe cast, so an int64 that
|
|
272
287
|
doesn't fit an int32 column is an error rather than a silent wrap.
|
|
273
288
|
|
|
@@ -296,7 +311,7 @@ def align_to_schema(arrow_table: pa.Table, schema: Schema) -> pa.Table:
|
|
|
296
311
|
for column in schema.fields:
|
|
297
312
|
batch_name = batch_by_fold.get(fold_identifier(column.name))
|
|
298
313
|
if batch_name is None:
|
|
299
|
-
arrays.append(_filled(column, n, f"column {column.name!r}"))
|
|
314
|
+
arrays.append(_filled(column, n, f"column {column.name!r}", moment))
|
|
300
315
|
continue
|
|
301
316
|
source = _fitted(arrow_table.column(batch_name), column, column.name)
|
|
302
317
|
if column.required and source.null_count:
|
|
@@ -393,7 +408,7 @@ def _fitted_fields(array: pa.Array, field_type: StructType, path: str) -> list[p
|
|
|
393
408
|
if folded in positions:
|
|
394
409
|
raise SchemaMismatchError(
|
|
395
410
|
f"column {path!r}: the batch's fields {spelled[folded]!r} and {name!r} name the same struct "
|
|
396
|
-
"field (
|
|
411
|
+
"field (field names compare case-insensitively)"
|
|
397
412
|
)
|
|
398
413
|
positions[folded] = position
|
|
399
414
|
spelled[folded] = name
|
|
@@ -441,11 +456,12 @@ def _omitted(child: Column, n: int, path: str) -> pa.Array:
|
|
|
441
456
|
return arrow_array_for(child, [None] * n)
|
|
442
457
|
|
|
443
458
|
|
|
444
|
-
def _filled(column: Column, n: int, where: str) -> pa.Array:
|
|
459
|
+
def _filled(column: Column, n: int, where: str, moment: datetime | None) -> pa.Array:
|
|
445
460
|
"""The ``n`` values a column or struct field the batch does not carry
|
|
446
461
|
takes: its ``default_value``, or NULL when it has none and is nullable.
|
|
447
462
|
An expression default is evaluated only when it is one SQL string
|
|
448
|
-
literal, which is how the spec stores a literal whose text is ``NULL
|
|
463
|
+
literal, which is how the spec stores a literal whose text is ``NULL``,
|
|
464
|
+
or one of the zero-argument functions ducklakepy evaluates at ``moment``."""
|
|
449
465
|
default = column.default
|
|
450
466
|
if default is None:
|
|
451
467
|
if column.required:
|
|
@@ -453,6 +469,15 @@ def _filled(column: Column, n: int, where: str) -> pa.Array:
|
|
|
453
469
|
fill = None
|
|
454
470
|
else:
|
|
455
471
|
text = string_literal_value(default.value) if default.is_expression else default.value
|
|
472
|
+
evaluated = (
|
|
473
|
+
None
|
|
474
|
+
if text is not None
|
|
475
|
+
else expression_default_values(column.field_type, default.value, n, moment or datetime.now(UTC))
|
|
476
|
+
)
|
|
477
|
+
if evaluated is not None:
|
|
478
|
+
array = arrow_array_for(column, evaluated)
|
|
479
|
+
check_logical_values(column, array, where=where)
|
|
480
|
+
return array
|
|
456
481
|
if text is None:
|
|
457
482
|
# Only a SQL engine can evaluate one of these; filling the column
|
|
458
483
|
# with its text would write "CAST(uuid() AS VARCHAR)" as the value.
|
|
@@ -41,7 +41,9 @@ blob DuckDB's own blob text: an ASCII character is its
|
|
|
41
41
|
*Not* the spec's hex bounds encoding, which stays
|
|
42
42
|
with statistics.
|
|
43
43
|
uuid the standard UUID spelling
|
|
44
|
-
date
|
|
44
|
+
date ISO 8601
|
|
45
|
+
time ISO 8601, up to ``24:00:00``, which is
|
|
46
|
+
``END_OF_DAY``
|
|
45
47
|
time_ns ISO 8601, as nanoseconds since midnight up to
|
|
46
48
|
``24:00:00``
|
|
47
49
|
timestamp / _s / _ms ISO 8601, rounded to the type's own precision
|
|
@@ -72,7 +74,7 @@ from ducklakepy.stats import (
|
|
|
72
74
|
render_blob_text,
|
|
73
75
|
split_utc_offset,
|
|
74
76
|
)
|
|
75
|
-
from ducklakepy.stats._temporal import parse_time_ns_text
|
|
77
|
+
from ducklakepy.stats._temporal import SUBSECOND_DIGITS, parse_time_ns_text
|
|
76
78
|
from ducklakepy.types import (
|
|
77
79
|
BlobType,
|
|
78
80
|
BooleanType,
|
|
@@ -376,10 +378,49 @@ def _round_to_unit(value: int, unit: int) -> int:
|
|
|
376
378
|
return magnitude * unit * (-1 if value < 0 else 1)
|
|
377
379
|
|
|
378
380
|
|
|
381
|
+
# The zero-argument expression defaults ducklakepy evaluates, as the text
|
|
382
|
+
# DuckDB stores for each: a function's name lower case, a keyword in the case
|
|
383
|
+
# it was written (measured against DuckDB 2.0.0), so the text is compared
|
|
384
|
+
# case-insensitively and nothing else is parsed. Anything else needs a SQL
|
|
385
|
+
# engine.
|
|
386
|
+
_NOW = frozenset({"now()", "current_timestamp", "transaction_timestamp()", "get_current_timestamp()"})
|
|
387
|
+
_TODAY = frozenset({"current_date", "today()"})
|
|
388
|
+
_RANDOM_UUID = frozenset({"gen_random_uuid()", "uuid()", "uuidv4()"})
|
|
389
|
+
_ZONED = (TimestampTzType, TimestampTzNsType)
|
|
390
|
+
_NAIVE = (TimestampType, TimestampSType, TimestampMsType, TimestampNsType)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def expression_default_values(field_type: DuckLakeType, expression: str, n: int, moment: datetime) -> list[Any] | None:
|
|
394
|
+
"""The ``n`` values an insert that omits a column gets from its default
|
|
395
|
+
``expression``, or None for an expression ducklakepy does not evaluate.
|
|
396
|
+
|
|
397
|
+
``now()`` and ``current_timestamp`` are the transaction's ``moment``, one
|
|
398
|
+
value for every row, as DuckDB takes its transaction's start; a timestamp
|
|
399
|
+
without a zone takes its UTC wall time and ``current_date`` its UTC date,
|
|
400
|
+
which is what a DuckDB session set to UTC writes. ``gen_random_uuid()``
|
|
401
|
+
is a fresh UUID for each row. Each fills only the type it yields."""
|
|
402
|
+
text = expression.strip().lower()
|
|
403
|
+
utc = moment.astimezone(UTC)
|
|
404
|
+
if text in _NOW and isinstance(field_type, _ZONED):
|
|
405
|
+
return [utc] * n
|
|
406
|
+
if text in _NOW and isinstance(field_type, _NAIVE):
|
|
407
|
+
digits = SUBSECOND_DIGITS[type(field_type)]
|
|
408
|
+
wall = utc.replace(tzinfo=None)
|
|
409
|
+
if digits < 6:
|
|
410
|
+
wall = wall.replace(microsecond=wall.microsecond - wall.microsecond % 10 ** (6 - digits))
|
|
411
|
+
return [wall] * n
|
|
412
|
+
if text in _TODAY and isinstance(field_type, DateType):
|
|
413
|
+
return [utc.date()] * n
|
|
414
|
+
if text in _RANDOM_UUID and isinstance(field_type, UuidType):
|
|
415
|
+
return [_uuid.uuid4().bytes for _ in range(n)]
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
379
419
|
__all__ = [
|
|
380
420
|
"INT_RANGES",
|
|
381
421
|
"assign_default",
|
|
382
422
|
"encode_default",
|
|
423
|
+
"expression_default_values",
|
|
383
424
|
"integer_digits",
|
|
384
425
|
"literal_default",
|
|
385
426
|
"render_default",
|
|
@@ -164,8 +164,7 @@ def hive_column_value(column_type: DuckLakeType, text: str | None, *, where: str
|
|
|
164
164
|
"""One path segment as a value of ``column_type``, or None for
|
|
165
165
|
:data:`HIVE_NULL`. The conversion is DuckDB's VARCHAR cast
|
|
166
166
|
(:func:`~ducklakepy._defaults.assign_default`), and a text the type
|
|
167
|
-
cannot take raises:
|
|
168
|
-
'PART_KEY') to: 'INTEGER'``.
|
|
167
|
+
cannot take raises :class:`~ducklakepy.exceptions.SchemaMismatchError`.
|
|
169
168
|
|
|
170
169
|
``infinity`` and ``-infinity`` are read first, because they are a
|
|
171
170
|
spelling a DATE or TIMESTAMP path really carries -- DuckDB writes such a
|
|
@@ -188,8 +187,8 @@ def hive_key_value(key: PathPartitionKey, text: str | None, *, where: str) -> An
|
|
|
188
187
|
None for :data:`HIVE_NULL`.
|
|
189
188
|
|
|
190
189
|
Identity keys take the column's own type; every other transform's key is
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
an integer, so ``year=abc`` raises
|
|
191
|
+
:class:`~ducklakepy.exceptions.SchemaMismatchError`.
|
|
193
192
|
Whether a ``bucket`` key's integer is a bucket the specification has is
|
|
194
193
|
:func:`is_valid_key_value`'s question, asked later and answered
|
|
195
194
|
differently.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""A column-wide read of DuckDB's inlined text for a list of numbers, booleans
|
|
2
|
+
or plain strings.
|
|
3
|
+
|
|
4
|
+
:mod:`ducklakepy._inlined_text` reads DuckDB's VARCHAR rendering of a nested
|
|
5
|
+
value one character at a time, which is exact and slow: a list of 100,000
|
|
6
|
+
integers takes about a second. Most lists DuckDB inlines are flat lists of
|
|
7
|
+
such leaves, and their text is simple enough for Arrow's string kernels to
|
|
8
|
+
split. DuckDB quotes a leaf whose text is empty, has a space at either end,
|
|
9
|
+
reads ``null`` in any case or holds one of ``" ' ( ) , : = [ ] { }``, so in a
|
|
10
|
+
column whose texts hold none of those characters (nor a backslash), every
|
|
11
|
+
comma separates two items and every item is a bare leaf.
|
|
12
|
+
|
|
13
|
+
:func:`fast_list_column` reads such a column, and answers None for anything
|
|
14
|
+
else: another element type, a text with one of those characters, or a leaf
|
|
15
|
+
not spelled the way DuckDB spells that type. The caller then reads the column
|
|
16
|
+
with the exact parser, so the values never depend on which path read them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
import pyarrow as pa
|
|
24
|
+
import pyarrow.compute as pc
|
|
25
|
+
|
|
26
|
+
from ducklakepy._arrow import arrow_type_for
|
|
27
|
+
from ducklakepy.types import (
|
|
28
|
+
BooleanType,
|
|
29
|
+
DecimalType,
|
|
30
|
+
Float32Type,
|
|
31
|
+
Float64Type,
|
|
32
|
+
Int8Type,
|
|
33
|
+
Int16Type,
|
|
34
|
+
Int32Type,
|
|
35
|
+
Int64Type,
|
|
36
|
+
ListType,
|
|
37
|
+
UInt8Type,
|
|
38
|
+
UInt16Type,
|
|
39
|
+
UInt32Type,
|
|
40
|
+
UInt64Type,
|
|
41
|
+
VarcharType,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from collections.abc import Sequence
|
|
46
|
+
|
|
47
|
+
from ducklakepy.types import DuckLakeType
|
|
48
|
+
|
|
49
|
+
# What DuckDB's ``StringUtil::CharacterIsSpace`` takes for a space.
|
|
50
|
+
_SPACES = " \t\n\v\f\r"
|
|
51
|
+
|
|
52
|
+
# A character that makes a text more than bare leaves and commas.
|
|
53
|
+
_STRUCTURE = r"""['"\\()\[\]{}:=]"""
|
|
54
|
+
|
|
55
|
+
_INTEGER = r"^-?[0-9]+$"
|
|
56
|
+
# Each leaf as DuckDB's VARCHAR cast spells the type, which Arrow's cast
|
|
57
|
+
# reads as the exact parser does. Arrow reads more than this (``0x10`` as 16,
|
|
58
|
+
# for one), so anything else goes to the exact parser.
|
|
59
|
+
_LEAF_SPELLINGS: dict[type[DuckLakeType], str] = {
|
|
60
|
+
Int8Type: _INTEGER,
|
|
61
|
+
Int16Type: _INTEGER,
|
|
62
|
+
Int32Type: _INTEGER,
|
|
63
|
+
Int64Type: _INTEGER,
|
|
64
|
+
UInt8Type: _INTEGER,
|
|
65
|
+
UInt16Type: _INTEGER,
|
|
66
|
+
UInt32Type: _INTEGER,
|
|
67
|
+
UInt64Type: _INTEGER,
|
|
68
|
+
Float32Type: r"^-?([0-9]+(\.[0-9]*)?([eE][-+]?[0-9]+)?|inf|nan)$",
|
|
69
|
+
Float64Type: r"^-?([0-9]+(\.[0-9]*)?([eE][-+]?[0-9]+)?|inf|nan)$",
|
|
70
|
+
DecimalType: r"^-?[0-9]+(\.[0-9]+)?$",
|
|
71
|
+
BooleanType: r"^(true|false)$",
|
|
72
|
+
VarcharType: r"",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fast_list_column(field_type: DuckLakeType, raw: Sequence[Any]) -> pa.Array | None:
|
|
77
|
+
"""``raw``, the inlined texts of a ``field_type`` column, as an array of its
|
|
78
|
+
Arrow type, or None when the column is not one this reads."""
|
|
79
|
+
if not isinstance(field_type, ListType) or any(text is not None and not isinstance(text, str) for text in raw):
|
|
80
|
+
return None
|
|
81
|
+
element = field_type.element.field_type
|
|
82
|
+
spelling = _LEAF_SPELLINGS.get(type(element))
|
|
83
|
+
if spelling is None:
|
|
84
|
+
return None
|
|
85
|
+
texts = pc.utf8_trim(pa.array(raw, pa.string()), _SPACES)
|
|
86
|
+
present = texts.drop_null()
|
|
87
|
+
if not pc.all(pc.and_(pc.starts_with(present, "["), pc.ends_with(present, "]")), min_count=0).as_py():
|
|
88
|
+
return None
|
|
89
|
+
inner = pc.fill_null(pc.utf8_slice_codeunits(texts, 1, -1), "")
|
|
90
|
+
if pc.any(pc.match_substring_regex(inner, _STRUCTURE)).as_py():
|
|
91
|
+
return None
|
|
92
|
+
split = pc.split_pattern(inner, ",")
|
|
93
|
+
# ``[]`` and ``[ ]`` are empty lists, where splitting finds one empty item.
|
|
94
|
+
empty = pc.equal(pc.utf8_trim(inner, _SPACES), "")
|
|
95
|
+
items = pc.utf8_trim(split.flatten(), _SPACES)
|
|
96
|
+
items = items.filter(pc.invert(pc.take(empty, pc.list_parent_indices(split))))
|
|
97
|
+
items = pc.if_else(pc.equal(pc.ascii_lower(items), "null"), pa.scalar(None, pa.string()), items)
|
|
98
|
+
leaves = items.drop_null()
|
|
99
|
+
if spelling and not pc.all(pc.match_substring_regex(leaves, spelling), min_count=0).as_py():
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
values = items.cast(arrow_type_for(element))
|
|
103
|
+
except (pa.ArrowInvalid, pa.ArrowNotImplementedError):
|
|
104
|
+
return None # past the type's range or scale, which the exact parser reports or rounds
|
|
105
|
+
offsets = [0]
|
|
106
|
+
for count, is_empty in zip(pc.list_value_length(split).to_pylist(), empty.to_pylist(), strict=True):
|
|
107
|
+
offsets.append(offsets[-1] + (0 if is_empty else count))
|
|
108
|
+
return pa.ListArray.from_arrays(
|
|
109
|
+
pa.array(offsets, pa.int32()), values, type=arrow_type_for(field_type), mask=texts.is_null()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["fast_list_column"]
|
|
@@ -38,7 +38,7 @@ from it.
|
|
|
38
38
|
from __future__ import annotations
|
|
39
39
|
|
|
40
40
|
import uuid as _uuid
|
|
41
|
-
from datetime import
|
|
41
|
+
from datetime import time
|
|
42
42
|
from decimal import Decimal
|
|
43
43
|
from typing import TYPE_CHECKING, Any
|
|
44
44
|
|
|
@@ -46,6 +46,7 @@ import pyarrow as pa
|
|
|
46
46
|
|
|
47
47
|
from ducklakepy._arrow import arrow_type_for, project, widen, with_field_ids
|
|
48
48
|
from ducklakepy._defaults import assign_default
|
|
49
|
+
from ducklakepy._inlined_lists import fast_list_column
|
|
49
50
|
from ducklakepy._interval import INTERVAL
|
|
50
51
|
from ducklakepy._interval_text import IntervalTextError, rendered_interval_parts
|
|
51
52
|
from ducklakepy._json_list import json_list
|
|
@@ -53,7 +54,7 @@ from ducklakepy._variant import inlined_value
|
|
|
53
54
|
from ducklakepy.exceptions import DuckLakeError, SchemaMismatchError
|
|
54
55
|
from ducklakepy.schema import NESTED_TYPES, Schema
|
|
55
56
|
from ducklakepy.stats import infinite_text, parse_blob_text
|
|
56
|
-
from ducklakepy.stats._temporal import far_temporal_text
|
|
57
|
+
from ducklakepy.stats._temporal import far_temporal_text, utc_time_of_day
|
|
57
58
|
from ducklakepy.types import (
|
|
58
59
|
BlobType,
|
|
59
60
|
DuckLakeType,
|
|
@@ -112,15 +113,17 @@ def rendered_column(
|
|
|
112
113
|
the column.
|
|
113
114
|
"""
|
|
114
115
|
field_type = source.field_type
|
|
115
|
-
values: list[Any]
|
|
116
116
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
stored = fast_list_column(field_type, raw)
|
|
118
|
+
if stored is None:
|
|
119
|
+
values: list[Any]
|
|
120
|
+
if isinstance(field_type, VariantType):
|
|
121
|
+
values = [inlined_value(_stored_bytes(decode(field_type, value))) for value in raw]
|
|
122
|
+
elif isinstance(field_type, IntervalType):
|
|
123
|
+
values = [_interval(decode(field_type, value)) for value in raw]
|
|
124
|
+
else:
|
|
125
|
+
values = [None if text is None else _cast(_stored_text(text), field_type) for text in raw]
|
|
126
|
+
stored = pa.array(values, type=arrow_type_for(field_type))
|
|
124
127
|
except (DuckLakeError, ValueError, ArithmeticError, pa.ArrowException) as error:
|
|
125
128
|
raise DuckLakeError(f"cannot read column {source.name!r} of inlined table {table_name!r}: {error}") from None
|
|
126
129
|
if not isinstance(field_type, NESTED_TYPES):
|
|
@@ -447,7 +450,7 @@ def _leaf(field_type: DuckLakeType, text: str) -> Any:
|
|
|
447
450
|
if isinstance(field_type, (Int128Type, UInt128Type)):
|
|
448
451
|
return Decimal(int(text.strip()))
|
|
449
452
|
if isinstance(field_type, TimeTzType):
|
|
450
|
-
return
|
|
453
|
+
return utc_time_of_day(time.fromisoformat(text.strip()))
|
|
451
454
|
if isinstance(field_type, NullType):
|
|
452
455
|
raise ValueError(f"a null column holds {text!r}")
|
|
453
456
|
if (infinite := infinite_text(field_type, text)) is not None:
|
|
@@ -460,11 +463,4 @@ def _leaf(field_type: DuckLakeType, text: str) -> Any:
|
|
|
460
463
|
raise ValueError(str(error)) from None
|
|
461
464
|
|
|
462
465
|
|
|
463
|
-
def _utc_time_of_day(value: time) -> time:
|
|
464
|
-
"""The UTC time of day a ``timetz`` names, as a file holds it."""
|
|
465
|
-
if value.tzinfo is None:
|
|
466
|
-
return value
|
|
467
|
-
return datetime.combine(date(2000, 1, 1), value).astimezone(UTC).time()
|
|
468
|
-
|
|
469
|
-
|
|
470
466
|
__all__ = ["is_rendered", "rendered_column"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The Arrow shape a DuckLake interval column is stored in and read as.
|
|
2
|
+
|
|
3
|
+
Parquet holds an interval in the twelve bytes of its INTERVAL converted type:
|
|
4
|
+
months, days and milliseconds, each a little-endian uint32, which is what a
|
|
5
|
+
file's column reads back as (:data:`STORED_INTERVAL`). Arrow has a type for
|
|
6
|
+
the same three parts, :data:`INTERVAL`, and that is what a reader of the
|
|
7
|
+
column gets: milliseconds scale to nanoseconds exactly, so nothing is lost,
|
|
8
|
+
and it is the type DuckDB's own Arrow output uses for an interval, so the
|
|
9
|
+
values of both implementations compare equal.
|
|
10
|
+
|
|
11
|
+
A file written here holds the same twelve bytes (:func:`stored_interval`),
|
|
12
|
+
and :mod:`ducklakepy._parquet_types` marks the column with the INTERVAL
|
|
13
|
+
converted type, which pyarrow does not write.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import struct
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pyarrow as pa
|
|
22
|
+
|
|
23
|
+
from ducklakepy.exceptions import DuckLakeError, SchemaMismatchError
|
|
24
|
+
|
|
25
|
+
STORED_INTERVAL = pa.binary(12)
|
|
26
|
+
"""What a Parquet INTERVAL column reads back as."""
|
|
27
|
+
|
|
28
|
+
INTERVAL = pa.month_day_nano_interval()
|
|
29
|
+
"""The type the column is handed back as: months, days and nanoseconds."""
|
|
30
|
+
|
|
31
|
+
_PARTS = struct.Struct("<III")
|
|
32
|
+
"""The twelve bytes, as Parquet's INTERVAL spells them."""
|
|
33
|
+
|
|
34
|
+
_MILLISECOND_NANOS = 1_000_000
|
|
35
|
+
_DAY_MILLISECONDS = 86_400_000
|
|
36
|
+
_PARTS_DTYPE = np.dtype([("months", "<i4"), ("days", "<i4"), ("nanos", "<i8")])
|
|
37
|
+
"""How Arrow lays out one month_day_nano value."""
|
|
38
|
+
_STORED_DTYPE = np.dtype([("months", "<u4"), ("days", "<u4"), ("milliseconds", "<u4")])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decoded_interval(column: pa.Array | pa.ChunkedArray, where: str) -> pa.Array:
|
|
42
|
+
"""``column``, twelve bytes a row, as an :data:`INTERVAL` array.
|
|
43
|
+
|
|
44
|
+
``where`` names the column in a refusal. Parquet counts all three parts
|
|
45
|
+
unsigned where Arrow counts months and days signed, so a months or days
|
|
46
|
+
past Arrow's range is refused rather than handed back as a negative:
|
|
47
|
+
DuckDB writes no such value (its own months and days are signed 32-bit,
|
|
48
|
+
and it refuses a negative interval into Parquet outright), so a value
|
|
49
|
+
that wide comes from a writer whose intent is not knowable here.
|
|
50
|
+
"""
|
|
51
|
+
if column.type != STORED_INTERVAL:
|
|
52
|
+
raise SchemaMismatchError(
|
|
53
|
+
f"{where} is an interval stored as {column.type}, and Parquet holds an interval in the "
|
|
54
|
+
f"{STORED_INTERVAL} of its INTERVAL converted type"
|
|
55
|
+
)
|
|
56
|
+
parts = [None if raw is None else _months_days_nanos(raw) for raw in column.to_pylist()]
|
|
57
|
+
try:
|
|
58
|
+
return pa.array(parts, type=INTERVAL)
|
|
59
|
+
except (pa.ArrowInvalid, OverflowError) as error:
|
|
60
|
+
raise DuckLakeError(f"{where} holds an interval Arrow's {INTERVAL} cannot express: {error}") from error
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stored_interval(column: pa.Array | pa.ChunkedArray, where: str) -> pa.Array:
|
|
64
|
+
"""``column``, an :data:`INTERVAL` array, as the twelve bytes a Parquet
|
|
65
|
+
INTERVAL holds, the way DuckDB 2.0.0 writes its own (measured): the time
|
|
66
|
+
is cut to whole milliseconds, its whole days are carried into the days,
|
|
67
|
+
and the months are kept apart. ``25 hours 1500 microseconds`` is stored as
|
|
68
|
+
1 day and 3,600,001 milliseconds.
|
|
69
|
+
|
|
70
|
+
``where`` names the column in a refusal. A negative part is refused, as
|
|
71
|
+
DuckDB refuses it ("Parquet files do not support negative intervals"), and
|
|
72
|
+
so are days that pass a signed 32-bit count once the time's days are
|
|
73
|
+
carried in, which DuckDB writes but reads back negative.
|
|
74
|
+
"""
|
|
75
|
+
array = column.combine_chunks() if isinstance(column, pa.ChunkedArray) else column
|
|
76
|
+
if len(array) == 0:
|
|
77
|
+
return pa.array([], STORED_INTERVAL)
|
|
78
|
+
parts = np.frombuffer(array.buffers()[1], _PARTS_DTYPE, count=array.offset + len(array))[array.offset :]
|
|
79
|
+
valid = np.ones(len(array), bool) if array.null_count == 0 else array.is_valid().to_numpy(zero_copy_only=False)
|
|
80
|
+
months, days, nanos = (parts[name][valid].astype(np.int64) for name in ("months", "days", "nanos"))
|
|
81
|
+
if (months < 0).any() or (days < 0).any() or (nanos < 0).any():
|
|
82
|
+
raise SchemaMismatchError(f"{where} holds a negative interval, which a Parquet INTERVAL cannot store")
|
|
83
|
+
carried, milliseconds = np.divmod(nanos // _MILLISECOND_NANOS, _DAY_MILLISECONDS)
|
|
84
|
+
days = days + carried
|
|
85
|
+
if (days > 2**31 - 1).any():
|
|
86
|
+
raise SchemaMismatchError(
|
|
87
|
+
f"{where} holds an interval whose days pass 2147483647 once its time's whole days are carried in"
|
|
88
|
+
)
|
|
89
|
+
stored = np.zeros(len(array), _STORED_DTYPE)
|
|
90
|
+
stored["months"][valid], stored["days"][valid], stored["milliseconds"][valid] = months, days, milliseconds
|
|
91
|
+
validity = None if array.null_count == 0 else array.is_valid().buffers()[1]
|
|
92
|
+
return pa.Array.from_buffers(STORED_INTERVAL, len(array), [validity, pa.py_buffer(stored.tobytes())])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _months_days_nanos(raw: bytes) -> tuple[int, int, int]:
|
|
96
|
+
months, days, milliseconds = _PARTS.unpack(raw)
|
|
97
|
+
return months, days, milliseconds * _MILLISECOND_NANOS
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["INTERVAL", "STORED_INTERVAL", "decoded_interval", "stored_interval"]
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
measured without reading it back.
|
|
3
3
|
|
|
4
4
|
A plaintext file streams to storage. One whose bytes have to change once
|
|
5
|
-
pyarrow has written them, by encryption (:mod:`ducklakepy.encryption`)
|
|
5
|
+
pyarrow has written them, by encryption (:mod:`ducklakepy.encryption`), by
|
|
6
6
|
the ``TIME`` flags pyarrow cannot set per column
|
|
7
|
-
(:mod:`ducklakepy._time_flags`)
|
|
7
|
+
(:mod:`ducklakepy._time_flags`) or by the annotations it does not write
|
|
8
|
+
(:mod:`ducklakepy._parquet_types`), is built in memory and only its finished
|
|
8
9
|
bytes reach storage.
|
|
9
10
|
"""
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ from typing import TYPE_CHECKING
|
|
|
15
16
|
|
|
16
17
|
import pyarrow as pa
|
|
17
18
|
|
|
19
|
+
from ducklakepy._parquet_types import Annotations, annotated, footer_length
|
|
18
20
|
from ducklakepy._time_flags import flagged
|
|
19
21
|
from ducklakepy.encryption import encrypt_parquet, key_bytes
|
|
20
22
|
|
|
@@ -77,6 +79,7 @@ def _close_and_measure(
|
|
|
77
79
|
to: _Destination,
|
|
78
80
|
encryption_key: str | None = None,
|
|
79
81
|
time_patch: Mapping[int, bool] | None = None,
|
|
82
|
+
annotations: Annotations | None = None,
|
|
80
83
|
) -> _Measured:
|
|
81
84
|
"""Close a file and measure it from what the writer already knows, so
|
|
82
85
|
nothing is read back: ``ducklake_data_file`` wants the file's size, its
|
|
@@ -93,7 +96,9 @@ def _close_and_measure(
|
|
|
93
96
|
|
|
94
97
|
A buffered file is finished here instead: the ``TIME`` flags
|
|
95
98
|
``time_patch`` names are set (:func:`~ducklakepy._time_flags.flagged`),
|
|
96
|
-
which moves no byte's offset,
|
|
99
|
+
which moves no byte's offset, and the ``annotations`` added
|
|
100
|
+
(:func:`~ducklakepy._parquet_types.annotated`), which lengthen the footer
|
|
101
|
+
and so have its size measured from the patched file. Then the Parquet is encrypted
|
|
97
102
|
(:func:`~ducklakepy.encryption.encrypt_parquet`) when ``encryption_key``
|
|
98
103
|
is given, and the result written to ``to``. An encrypted file's size and
|
|
99
104
|
footer size are the ciphertext's, and DuckDB refuses a file whose stored
|
|
@@ -116,11 +121,13 @@ def _close_and_measure(
|
|
|
116
121
|
data = buffered.to_pybytes()
|
|
117
122
|
if time_patch:
|
|
118
123
|
data = flagged(data, time_patch)
|
|
124
|
+
if annotations:
|
|
125
|
+
data = annotated(data, annotations)
|
|
119
126
|
if encryption_key is not None:
|
|
120
127
|
encrypted = encrypt_parquet(data, key_bytes(encryption_key))
|
|
121
128
|
data, footer_size = encrypted.data, encrypted.footer_size
|
|
122
129
|
else:
|
|
123
|
-
footer_size = _footer_size(metadata)
|
|
130
|
+
footer_size = footer_length(data) if annotations else _footer_size(metadata)
|
|
124
131
|
written = _open_output(to.fs, to.fs_path)
|
|
125
132
|
try:
|
|
126
133
|
written.write(data)
|