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.
Files changed (213) hide show
  1. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/PKG-INFO +7 -8
  2. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/README.md +6 -7
  3. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/pyproject.toml +1 -1
  4. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_arrow.py +34 -9
  5. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_defaults.py +43 -2
  6. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_hive.py +3 -4
  7. ducklakepy-0.1.0.dev1/src/ducklakepy/_inlined_lists.py +113 -0
  8. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_inlined_text.py +14 -18
  9. ducklakepy-0.1.0.dev1/src/ducklakepy/_interval.py +100 -0
  10. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_parquet_sink.py +11 -4
  11. ducklakepy-0.1.0.dev1/src/ducklakepy/_parquet_types.py +246 -0
  12. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_promotions.py +7 -8
  13. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_version.py +1 -1
  14. ducklakepy-0.1.0.dev1/src/ducklakepy/_virtual_columns.py +159 -0
  15. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/add_files.py +141 -84
  16. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/capabilities.py +40 -52
  17. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/__init__.py +130 -130
  18. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_dialect.py +40 -29
  19. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_schema.py +10 -1
  20. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_statements.py +2 -1
  21. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/changes.py +5 -5
  22. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/constants.py +1 -1
  23. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/encryption.py +9 -14
  24. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/exceptions.py +1 -1
  25. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/expressions.py +15 -28
  26. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/__init__.py +2 -2
  27. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_bounds.py +2 -2
  28. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_common.py +3 -5
  29. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/compaction.py +4 -5
  30. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/expire.py +10 -9
  31. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/options.py +94 -112
  32. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/__init__.py +78 -64
  33. ducklakepy-0.1.0.dev1/src/ducklakepy/partitioning/_hash_text.py +268 -0
  34. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/_value_text.py +13 -3
  35. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/plan.py +30 -67
  36. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/scan.py +48 -11
  37. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/schema.py +63 -77
  38. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/schema_update.py +119 -174
  39. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/snapshots.py +45 -23
  40. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/sorting.py +63 -77
  41. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_feed.py +5 -6
  42. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_write.py +17 -9
  43. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/__init__.py +79 -157
  44. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_footer.py +51 -10
  45. ducklakepy-0.1.0.dev1/src/ducklakepy/stats/_geometry.py +216 -0
  46. ducklakepy-0.1.0.dev1/src/ducklakepy/stats/_table.py +192 -0
  47. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_temporal.py +49 -3
  48. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/table.py +43 -34
  49. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/tags.py +5 -5
  50. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/__init__.py +217 -271
  51. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_commit_sql.py +10 -14
  52. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_conflicts.py +5 -3
  53. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_keys.py +3 -2
  54. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_null_bounds.py +2 -6
  55. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_operations.py +3 -5
  56. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_table_stats.py +2 -32
  57. ducklakepy-0.1.0.dev1/src/ducklakepy/transaction/_upsert.py +185 -0
  58. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/types.py +3 -4
  59. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/version.py +6 -8
  60. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/views.py +19 -17
  61. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/write_options.py +39 -41
  62. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/writer.py +27 -6
  63. ducklakepy-0.1.0.dev1/tests/test_add_files_interval_variant.py +145 -0
  64. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_capabilities.py +38 -41
  65. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_ddl.py +1 -1
  66. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_lifecycle.py +52 -0
  67. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_cross_impl.py +78 -40
  68. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_dialect.py +25 -1
  69. ducklakepy-0.1.0.dev1/tests/test_end_of_day.py +218 -0
  70. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_footer_stats.py +96 -6
  71. ducklakepy-0.1.0.dev1/tests/test_inlined_lists.py +202 -0
  72. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_nested.py +13 -9
  73. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_variants.py +75 -24
  74. ducklakepy-0.1.0.dev1/tests/test_interval_variant_files.py +291 -0
  75. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_expire.py +11 -0
  76. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_module_size.py +1 -1
  77. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_writes.py +2 -2
  78. ducklakepy-0.1.0.dev1/tests/test_partition_key_text.py +59 -0
  79. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partitioning.py +115 -8
  80. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_add_files.py +33 -4
  81. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_alter.py +4 -4
  82. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_stats.py +66 -7
  83. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_sorting.py +4 -5
  84. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spark.py +23 -3
  85. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_stats.py +3 -3
  86. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_transaction_ops.py +77 -15
  87. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_type_writes.py +66 -7
  88. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_upsert.py +143 -3
  89. ducklakepy-0.1.0.dev1/tests/test_virtual_columns.py +118 -0
  90. ducklakepy-0.1.0.dev0/src/ducklakepy/_interval.py +0 -63
  91. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/.gitignore +0 -0
  92. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/LICENSE +0 -0
  93. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/__init__.py +0 -0
  94. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_changes.py +0 -0
  95. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_encodings.py +0 -0
  96. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_globs.py +0 -0
  97. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_interval_text.py +0 -0
  98. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_json_list.py +0 -0
  99. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_lists.py +0 -0
  100. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_literals.py +0 -0
  101. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_logical_values.py +0 -0
  102. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_names.py +0 -0
  103. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_packing.py +0 -0
  104. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_parallel.py +0 -0
  105. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_paths.py +0 -0
  106. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_sources.py +0 -0
  107. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_thrift.py +0 -0
  108. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_time_flags.py +0 -0
  109. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_variant.py +0 -0
  110. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_variant_shredding.py +0 -0
  111. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/_wkb.py +0 -0
  112. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_engine.py +0 -0
  113. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_entries.py +0 -0
  114. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_legacy_inlined.py +0 -0
  115. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_migration.py +0 -0
  116. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/catalog/_namespaces.py +0 -0
  117. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/config.py +0 -0
  118. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/inlined.py +0 -0
  119. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/io/__init__.py +0 -0
  120. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/io/pyarrow.py +0 -0
  121. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/_versions.py +0 -0
  122. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/checkpoint.py +0 -0
  123. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/cleanup.py +0 -0
  124. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/maintenance/orphans.py +0 -0
  125. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/partitioning/_bounds.py +0 -0
  126. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/properties.py +0 -0
  127. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/puffin.py +0 -0
  128. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/py.typed +0 -0
  129. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/rows.py +0 -0
  130. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/__init__.py +0 -0
  131. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_options.py +0 -0
  132. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_read.py +0 -0
  133. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_schemas.py +0 -0
  134. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/spark/_source.py +0 -0
  135. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_blob_text.py +0 -0
  136. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/stats/_skipped.py +0 -0
  137. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_companions.py +0 -0
  138. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_delete_files.py +0 -0
  139. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_entries.py +0 -0
  140. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_float_bounds.py +0 -0
  141. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_namespaces.py +0 -0
  142. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/src/ducklakepy/transaction/_staged.py +0 -0
  143. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/__init__.py +0 -0
  144. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/conftest.py +0 -0
  145. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/fileio.py +0 -0
  146. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/lake.py +0 -0
  147. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_arrow_mapping.py +0 -0
  148. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_azure_fileio.py +0 -0
  149. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_catalog_read.py +0 -0
  150. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_change_feed.py +0 -0
  151. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_changes.py +0 -0
  152. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_ci_shards.py +0 -0
  153. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_cloud_stores.py +0 -0
  154. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_concurrent.py +0 -0
  155. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_concurrent_read.py +0 -0
  156. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_config.py +0 -0
  157. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_conflict_detection.py +0 -0
  158. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_count.py +0 -0
  159. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_database_pool.py +0 -0
  160. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_delete.py +0 -0
  161. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_dictionary_encoding.py +0 -0
  162. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_drop.py +0 -0
  163. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_encryption.py +0 -0
  164. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_expressions.py +0 -0
  165. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_gcs_fileio.py +0 -0
  166. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_infinite_temporals.py +0 -0
  167. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_text.py +0 -0
  168. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_inlined_writes.py +0 -0
  169. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_io.py +0 -0
  170. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_checkpoint.py +0 -0
  171. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_cleanup.py +0 -0
  172. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_compaction.py +0 -0
  173. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_maintenance_orphans.py +0 -0
  174. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_multi_file_writes.py +0 -0
  175. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_predicates.py +0 -0
  176. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_nested_schemas.py +0 -0
  177. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_options.py +0 -0
  178. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partial_files.py +0 -0
  179. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_partition_bounds.py +0 -0
  180. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_paths.py +0 -0
  181. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_puffin.py +0 -0
  182. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_deletes.py +0 -0
  183. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_inlining.py +0 -0
  184. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_migration.py +0 -0
  185. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_partitioning.py +0 -0
  186. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_paths.py +0 -0
  187. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_rowid.py +0 -0
  188. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_snapshot_info.py +0 -0
  189. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_sorting.py +0 -0
  190. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_table_changes.py +0 -0
  191. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_time_travel.py +0 -0
  192. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_transactions.py +0 -0
  193. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_update.py +0 -0
  194. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_reference_views.py +0 -0
  195. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_row_group_pruning.py +0 -0
  196. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_rows.py +0 -0
  197. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_scan.py +0 -0
  198. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_scan_batches.py +0 -0
  199. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_schema.py +0 -0
  200. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_schema_evolution.py +0 -0
  201. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_sequences.py +0 -0
  202. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_shredded_variants.py +0 -0
  203. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_skip_stats.py +0 -0
  204. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spark_packing.py +0 -0
  205. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_spec_version.py +0 -0
  206. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_table_append.py +0 -0
  207. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_update.py +0 -0
  208. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_variant_bytes.py +0 -0
  209. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_variant_shredding.py +0 -0
  210. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_views.py +0 -0
  211. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_write_measurement.py +0 -0
  212. {ducklakepy-0.1.0.dev0 → ducklakepy-0.1.0.dev1}/tests/test_write_validation.py +0 -0
  213. {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.dev0
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 without a DuckDB
38
- process. A
39
- lake written by ducklakepy is readable by DuckDB's `ducklake` extension, and the other way
40
- round, so the two can share one lake. If you know PyIceberg, ducklakepy plays the same
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, then a quickstart: a lake on SQLite, written and read back from Python and from DuckDB |
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/catalogs/) | 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, distributed reads, Spark, use with DuckDB |
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 without a DuckDB
9
- process. A
10
- lake written by ducklakepy is readable by DuckDB's `ducklake` extension, and the other way
11
- round, so the two can share one lake. If you know PyIceberg, ducklakepy plays the same
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, then a quickstart: a lake on SQLite, written and read back from Python and from DuckDB |
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/catalogs/) | 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, distributed reads, Spark, use with DuckDB |
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 |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ducklakepy"
7
- version = "0.1.0.dev0"
7
+ version = "0.1.0.dev1"
8
8
  description = "A Python implementation of the DuckLake specification"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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, which only a SQL engine can evaluate; values are cast to the
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 (identifiers fold ASCII case, as DuckDB's do)"
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 / time ISO 8601
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: ``Unable to cast 'p1' (from hive partition column
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
- the integer DuckDB reads it as, so ``year=abc`` is refused with ``Unable
192
- to cast 'abc' (from hive partition column 'YEAR') to: 'BIGINT'``.
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 UTC, date, datetime, time
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
- if isinstance(field_type, VariantType):
118
- values = [inlined_value(_stored_bytes(decode(field_type, value))) for value in raw]
119
- elif isinstance(field_type, IntervalType):
120
- values = [_interval(decode(field_type, value)) for value in raw]
121
- else:
122
- values = [None if text is None else _cast(_stored_text(text), field_type) for text in raw]
123
- stored = pa.array(values, type=arrow_type_for(field_type))
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 _utc_time_of_day(time.fromisoformat(text.strip()))
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`) or by
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`), is built in memory and only its finished
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, then the Parquet is encrypted
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)