supertable 2.2.8__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. {supertable-2.2.8/supertable.egg-info → supertable-2.3.0}/PKG-INFO +1 -1
  2. {supertable-2.2.8 → supertable-2.3.0}/pyproject.toml +1 -1
  3. {supertable-2.2.8 → supertable-2.3.0}/setup.py +1 -1
  4. {supertable-2.2.8 → supertable-2.3.0}/supertable/__init__.py +1 -1
  5. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/defaults.py +2 -0
  6. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/settings.py +14 -0
  7. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_defaults.py +4 -0
  8. {supertable-2.2.8 → supertable-2.3.0}/supertable/data_classes.py +28 -31
  9. {supertable-2.2.8 → supertable-2.3.0}/supertable/data_reader.py +135 -34
  10. {supertable-2.2.8 → supertable-2.3.0}/supertable/data_writer.py +295 -190
  11. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/data_estimator.py +107 -7
  12. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/duckdb_lite.py +45 -48
  13. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/duckdb_pro.py +23 -49
  14. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_common.py +71 -108
  15. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/executor.py +4 -0
  16. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/spark_thrift.py +61 -120
  17. {supertable-2.2.8 → supertable-2.3.0}/supertable/meta_reader.py +6 -0
  18. supertable-2.3.0/supertable/processing.py +1805 -0
  19. {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_catalog.py +14 -0
  20. {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_keys.py +16 -0
  21. {supertable-2.2.8 → supertable-2.3.0}/supertable/simple_table.py +20 -1
  22. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/tests/test_storage.py +77 -4
  23. supertable-2.3.0/supertable/system_query.py +135 -0
  24. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_reader.py +343 -1
  25. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer.py +113 -32
  26. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer_compact.py +97 -63
  27. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer_comprehensive.py +94 -178
  28. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_meta_reader.py +44 -0
  29. supertable-2.3.0/supertable/tests/test_parquet_statistics.py +86 -0
  30. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_processing.py +168 -400
  31. supertable-2.3.0/supertable/tests/test_processing_stats.py +328 -0
  32. supertable-2.3.0/supertable/tests/test_read_pruning_differential.py +457 -0
  33. supertable-2.3.0/supertable/tests/test_read_pruning_integration.py +352 -0
  34. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_redis_key_prefix.py +19 -0
  35. supertable-2.3.0/supertable/tests/test_stats_cache.py +152 -0
  36. supertable-2.3.0/supertable/tests/test_stats_pruning.py +557 -0
  37. supertable-2.3.0/supertable/tests/test_stats_schema_snapshot.py +115 -0
  38. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_supertable_all.py +90 -372
  39. supertable-2.3.0/supertable/tests/test_system_query.py +216 -0
  40. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/sql_parser.py +291 -1
  41. {supertable-2.2.8 → supertable-2.3.0/supertable.egg-info}/PKG-INFO +1 -1
  42. {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/SOURCES.txt +9 -5
  43. supertable-2.2.8/supertable/engine/tests/test_dedup_read.py +0 -1172
  44. supertable-2.2.8/supertable/processing.py +0 -1480
  45. supertable-2.2.8/supertable/tests/test_data_writer_tombstones.py +0 -802
  46. supertable-2.2.8/supertable/tests/test_dedup_on_read_write.py +0 -1037
  47. supertable-2.2.8/supertable/tests/test_process_delete_only.py +0 -447
  48. supertable-2.2.8/supertable/tests/test_small_file_compaction.py +0 -959
  49. {supertable-2.2.8 → supertable-2.3.0}/LICENSE +0 -0
  50. {supertable-2.2.8 → supertable-2.3.0}/README.md +0 -0
  51. {supertable-2.2.8 → supertable-2.3.0}/requirements.txt +0 -0
  52. {supertable-2.2.8 → supertable-2.3.0}/setup.cfg +0 -0
  53. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/__init__.py +0 -0
  54. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/admin.py +0 -0
  55. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/chain.py +0 -0
  56. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/consumers.py +0 -0
  57. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/crypto.py +0 -0
  58. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/events.py +0 -0
  59. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/export.py +0 -0
  60. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/logger.py +0 -0
  61. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/middleware.py +0 -0
  62. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/reader.py +0 -0
  63. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/retention.py +0 -0
  64. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/__init__.py +0 -0
  65. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_chain.py +0 -0
  66. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_crypto.py +0 -0
  67. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_emit.py +0 -0
  68. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_events.py +0 -0
  69. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_retention.py +0 -0
  70. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/writer_parquet.py +0 -0
  71. {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/writer_redis.py +0 -0
  72. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/__init__.py +0 -0
  73. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/homedir.py +0 -0
  74. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/__init__.py +0 -0
  75. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_homedir.py +0 -0
  76. {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_settings.py +0 -0
  77. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/__init__.py +0 -0
  78. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/__init__.py +0 -0
  79. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/__main__.py +0 -0
  80. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/check_filter_builder.py +0 -0
  81. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/controller.py +0 -0
  82. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
  83. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/defaults.py +0 -0
  84. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/dummy_data.py +0 -0
  85. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/read_parquet_header.py +0 -0
  86. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
  87. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
  88. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
  89. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
  90. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
  91. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
  92. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
  93. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
  94. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
  95. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
  96. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
  97. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
  98. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
  99. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
  100. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
  101. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
  102. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
  103. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
  104. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
  105. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
  106. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
  107. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
  108. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
  109. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
  110. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/__init__.py +0 -0
  111. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/core.py +0 -0
  112. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/defaults.py +0 -0
  113. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/generate.py +0 -0
  114. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/load.py +0 -0
  115. {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/topup.py +0 -0
  116. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/__init__.py +0 -0
  117. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_config.py +0 -0
  118. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_enum.py +0 -0
  119. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/plan_stats.py +0 -0
  120. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/__init__.py +0 -0
  121. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/conftest.py +0 -0
  122. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine.py +0 -0
  123. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_config.py +0 -0
  124. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_routing.py +0 -0
  125. {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_spill.py +0 -0
  126. {supertable-2.2.8 → supertable-2.3.0}/supertable/errors.py +0 -0
  127. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/__init__.py +0 -0
  128. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/__init__.py +0 -0
  129. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
  130. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
  131. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
  132. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/file_lock.py +0 -0
  133. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/redis_lock.py +0 -0
  134. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/__init__.py +0 -0
  135. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/test_file_lock.py +0 -0
  136. {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/test_redis_lock.py +0 -0
  137. {supertable-2.2.8 → supertable-2.3.0}/supertable/logging.py +0 -0
  138. {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/__init__.py +0 -0
  139. {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_delta.py +0 -0
  140. {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_formats.py +0 -0
  141. {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_iceberg.py +0 -0
  142. {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_parquet.py +0 -0
  143. {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring/__init__.py +0 -0
  144. {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring/partitions.py +0 -0
  145. {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring_writer.py +0 -0
  146. {supertable-2.2.8 → supertable-2.3.0}/supertable/plan_extender.py +0 -0
  147. {supertable-2.2.8 → supertable-2.3.0}/supertable/query_plan_manager.py +0 -0
  148. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/__init__.py +0 -0
  149. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/access_control.py +0 -0
  150. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/filter_builder.py +0 -0
  151. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/permissions.py +0 -0
  152. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/role_manager.py +0 -0
  153. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/row_column_security.py +0 -0
  154. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_filter_builder.py +0 -0
  155. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_rbac.py +0 -0
  156. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
  157. {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/user_manager.py +0 -0
  158. {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_connector.py +0 -0
  159. {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_infra.py +0 -0
  160. {supertable-2.2.8 → supertable-2.3.0}/supertable/staging_area.py +0 -0
  161. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/__init__.py +0 -0
  162. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/azure_storage.py +0 -0
  163. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/gcp_storage.py +0 -0
  164. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/local_storage.py +0 -0
  165. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/minio_storage.py +0 -0
  166. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/s3_storage.py +0 -0
  167. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/storage_factory.py +0 -0
  168. {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/storage_interface.py +0 -0
  169. {supertable-2.2.8 → supertable-2.3.0}/supertable/super_pipe.py +0 -0
  170. {supertable-2.2.8 → supertable-2.3.0}/supertable/super_table.py +0 -0
  171. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/__init__.py +0 -0
  172. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_align_to_schema_fix.py +0 -0
  173. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_create_if_missing.py +0 -0
  174. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_reader_preflight.py +0 -0
  175. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_errors.py +0 -0
  176. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_monitoring_partitions.py +0 -0
  177. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_monitoring_sink_guard.py +0 -0
  178. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_newer_than.py +0 -0
  179. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_processing_compact_resources.py +0 -0
  180. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_query_sql.py +0 -0
  181. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_simple_table.py +0 -0
  182. {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_super_table.py +0 -0
  183. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/__init__.py +0 -0
  184. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/helper.py +0 -0
  185. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/profiler.py +0 -0
  186. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
  187. {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/timer.py +0 -0
  188. {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/dependency_links.txt +0 -0
  189. {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/entry_points.txt +0 -0
  190. {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/requires.txt +0 -0
  191. {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supertable
3
- Version: 2.2.8
3
+ Version: 2.3.0
4
4
  Summary: SuperTable — versioned data lake library for SQL analytics on Parquet + Redis.
5
5
  Author: Levente Kupas
6
6
  Author-email: Levente Kupas <lkupas@kladnasoft.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "supertable"
7
- version = "2.2.8"
7
+ version = "2.3.0"
8
8
  description = "SuperTable — versioned data lake library for SQL analytics on Parquet + Redis."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,7 +19,7 @@ long_description = readme.read_text(encoding="utf-8") if readme.exists() else ""
19
19
 
20
20
  setup(
21
21
  name="supertable",
22
- version="2.2.8",
22
+ version="2.3.0",
23
23
  description="SuperTable — versioned data lake library for SQL analytics on Parquet + Redis.",
24
24
  long_description=long_description,
25
25
  long_description_content_type="text/markdown",
@@ -25,7 +25,7 @@ See the ``supertable.demo`` package for runnable end-to-end demos and the
25
25
  project documentation for the full API surface.
26
26
  """
27
27
 
28
- __version__ = "2.2.8"
28
+ __version__ = "2.3.0"
29
29
 
30
30
  # Re-export the core public surface so users can do ``from supertable import …``
31
31
  # instead of remembering submodule paths.
@@ -23,6 +23,7 @@ _VALID_LOG_LEVELS = frozenset({"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"})
23
23
  class Default:
24
24
  MAX_MEMORY_CHUNK_SIZE: int = 16 * 1024 * 1024
25
25
  MAX_OVERLAPPING_FILES: int = 100
26
+ MAX_TOMBSTONE_ROWS: int = 1_000_000
26
27
  DEFAULT_TIMEOUT_SEC: int = 60
27
28
  DEFAULT_LOCK_DURATION_SEC: int = 30
28
29
  LOG_LEVEL: str = "INFO"
@@ -51,6 +52,7 @@ def load_defaults_from_env(env_file: str | None = None, prefer_system: bool = Tr
51
52
  return Default(
52
53
  MAX_MEMORY_CHUNK_SIZE=settings.MAX_MEMORY_CHUNK_SIZE,
53
54
  MAX_OVERLAPPING_FILES=settings.MAX_OVERLAPPING_FILES,
55
+ MAX_TOMBSTONE_ROWS=settings.MAX_TOMBSTONE_ROWS,
54
56
  DEFAULT_TIMEOUT_SEC=settings.DEFAULT_TIMEOUT_SEC,
55
57
  DEFAULT_LOCK_DURATION_SEC=settings.DEFAULT_LOCK_DURATION_SEC,
56
58
  LOG_LEVEL=log_level,
@@ -116,6 +116,7 @@ class Settings:
116
116
  # ── Defaults (legacy config/defaults.py) ─────────────────────────
117
117
  MAX_MEMORY_CHUNK_SIZE: int = 16 * 1024 * 1024 # MAX_MEMORY_CHUNK_SIZE
118
118
  MAX_OVERLAPPING_FILES: int = 100 # MAX_OVERLAPPING_FILES
119
+ MAX_TOMBSTONE_ROWS: int = 1_000_000 # MAX_TOMBSTONE_ROWS
119
120
  DEFAULT_TIMEOUT_SEC: int = 60 # DEFAULT_TIMEOUT_SEC
120
121
  DEFAULT_LOCK_DURATION_SEC: int = 30 # DEFAULT_LOCK_DURATION_SEC
121
122
  IS_SHOW_TIMING: bool = False # IS_SHOW_TIMING
@@ -259,6 +260,16 @@ class Settings:
259
260
  # ── Meta Reader / Caching ────────────────────────────────────────
260
261
  SUPERTABLE_SUPER_META_CACHE_TTL_S: Optional[float] = None # SUPERTABLE_SUPER_META_CACHE_TTL_S
261
262
 
263
+ # Max number of tables whose *latest* stats artifact is held in the
264
+ # in-process stats cache (one DataFrame per table; older versions are
265
+ # always read fresh and never cached). 0 disables caching.
266
+ SUPERTABLE_STATS_CACHE_MAX_TABLES: int = 64 # SUPERTABLE_STATS_CACHE_MAX_TABLES
267
+
268
+ # Read-path file pruning: when True the estimator uses the stats artifact to
269
+ # drop parquet files that provably can't satisfy a query's WHERE predicates.
270
+ # Conservative (never drops a file that could match); set False to disable.
271
+ SUPERTABLE_READ_PRUNING_ENABLED: bool = True # SUPERTABLE_READ_PRUNING_ENABLED
272
+
262
273
  # ── Audit ────────────────────────────────────────────────────────
263
274
  # Audit is OFF by default. Enable per-organization in the WebUI
264
275
  # /ui/audit → Compliance tab (persisted at supertable:{org}:system:audit:config),
@@ -366,6 +377,7 @@ def _build_settings() -> Settings:
366
377
  # ── Defaults ─────────────────────────────────────────────────
367
378
  MAX_MEMORY_CHUNK_SIZE=_env_int("MAX_MEMORY_CHUNK_SIZE", 16 * 1024 * 1024),
368
379
  MAX_OVERLAPPING_FILES=_env_int("MAX_OVERLAPPING_FILES", 100),
380
+ MAX_TOMBSTONE_ROWS=_env_int("MAX_TOMBSTONE_ROWS", 1_000_000),
369
381
  DEFAULT_TIMEOUT_SEC=_env_int("DEFAULT_TIMEOUT_SEC", 60),
370
382
  DEFAULT_LOCK_DURATION_SEC=_env_int("DEFAULT_LOCK_DURATION_SEC", 30),
371
383
  IS_SHOW_TIMING=_env_bool("IS_SHOW_TIMING", True),
@@ -509,6 +521,8 @@ def _build_settings() -> Settings:
509
521
 
510
522
  # ── Meta Reader / Caching ────────────────────────────────────
511
523
  SUPERTABLE_SUPER_META_CACHE_TTL_S=meta_ttl,
524
+ SUPERTABLE_STATS_CACHE_MAX_TABLES=_env_int("SUPERTABLE_STATS_CACHE_MAX_TABLES", 64),
525
+ SUPERTABLE_READ_PRUNING_ENABLED=_env_bool("SUPERTABLE_READ_PRUNING_ENABLED", True),
512
526
 
513
527
  # ── Audit ────────────────────────────────────────────────────
514
528
  SUPERTABLE_AUDIT_ENABLED=_env_bool("SUPERTABLE_AUDIT_ENABLED", False),
@@ -78,6 +78,7 @@ class TestLoadDefaultsFromEnv:
78
78
  SUPERTABLE_LOG_LEVEL="WARNING",
79
79
  MAX_MEMORY_CHUNK_SIZE=12345,
80
80
  MAX_OVERLAPPING_FILES=7,
81
+ MAX_TOMBSTONE_ROWS=999,
81
82
  DEFAULT_TIMEOUT_SEC=11,
82
83
  DEFAULT_LOCK_DURATION_SEC=22,
83
84
  IS_SHOW_TIMING=False,
@@ -89,6 +90,7 @@ class TestLoadDefaultsFromEnv:
89
90
  assert d.LOG_LEVEL == "WARNING"
90
91
  assert d.MAX_MEMORY_CHUNK_SIZE == 12345
91
92
  assert d.MAX_OVERLAPPING_FILES == 7
93
+ assert d.MAX_TOMBSTONE_ROWS == 999
92
94
  assert d.DEFAULT_TIMEOUT_SEC == 11
93
95
  assert d.DEFAULT_LOCK_DURATION_SEC == 22
94
96
  assert d.IS_SHOW_TIMING is False
@@ -101,6 +103,7 @@ class TestLoadDefaultsFromEnv:
101
103
  SUPERTABLE_LOG_LEVEL="LOUDEST",
102
104
  MAX_MEMORY_CHUNK_SIZE=1,
103
105
  MAX_OVERLAPPING_FILES=1,
106
+ MAX_TOMBSTONE_ROWS=1,
104
107
  DEFAULT_TIMEOUT_SEC=1,
105
108
  DEFAULT_LOCK_DURATION_SEC=1,
106
109
  IS_SHOW_TIMING=False,
@@ -121,6 +124,7 @@ class TestLoadDefaultsFromEnv:
121
124
  SUPERTABLE_LOG_LEVEL=level,
122
125
  MAX_MEMORY_CHUNK_SIZE=1,
123
126
  MAX_OVERLAPPING_FILES=1,
127
+ MAX_TOMBSTONE_ROWS=1,
124
128
  DEFAULT_TIMEOUT_SEC=1,
125
129
  DEFAULT_LOCK_DURATION_SEC=1,
126
130
  IS_SHOW_TIMING=False,
@@ -1,5 +1,23 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Dict, List, Optional, Set
2
+ from typing import Dict, List, NamedTuple, Optional, Set
3
+
4
+
5
+ class PredInterval(NamedTuple):
6
+ """A closed/open range of values a column is *allowed* to take, derived from
7
+ a SQL WHERE predicate, used for read-path file pruning.
8
+
9
+ ``lane`` is one of ``"numeric"`` / ``"string"`` / ``"timestamp"`` and selects
10
+ which stored stats lane the interval is compared against (numeric unifies the
11
+ stored ``bigint``/``double`` lanes). ``lo``/``hi`` are the bounds with
12
+ ``None`` meaning unbounded (-inf / +inf); ``lo_incl``/``hi_incl`` mark
13
+ whether each bound is inclusive. Examples: ``col = 5`` →
14
+ ``(lane, 5, True, 5, True)``; ``col > 5`` → ``(lane, 5, False, None, True)``.
15
+ """
16
+ lane: str
17
+ lo: object
18
+ lo_incl: bool
19
+ hi: object
20
+ hi_incl: bool
3
21
 
4
22
 
5
23
  @dataclass
@@ -32,39 +50,20 @@ class RbacViewDef:
32
50
  where_clause: str = ""
33
51
 
34
52
 
35
- @dataclass
36
- class DedupViewDef:
37
- """Dedup-on-read view definition for a single table alias.
38
-
39
- Produced by DataReader from table config in Redis, consumed by executors
40
- to create a ROW_NUMBER window view that keeps only the latest row per
41
- primary key combination.
42
-
43
- - primary_keys: columns that form the composite key for dedup.
44
- - order_column: column to ORDER BY DESC (default ``__timestamp__``).
45
- - visible_columns: columns to expose in the dedup view. When empty or
46
- ["*"], all columns except ``__rn__`` are exposed. When set, only
47
- these columns are projected (hiding PK / order cols that the user
48
- did not request).
49
- """
50
- primary_keys: List[str] = field(default_factory=list)
51
- order_column: str = "__timestamp__"
52
- visible_columns: List[str] = field(default_factory=list)
53
-
54
-
55
53
  @dataclass
56
54
  class TombstoneDef:
57
- """Tombstone definition for a single table alias.
55
+ """Tombstone (deletion-vector) definition for a single table alias.
58
56
 
59
- Produced by DataReader from the snapshot's ``tombstones`` block,
60
- consumed by executors to create a filtering view that excludes
61
- soft-deleted keys.
57
+ Produced by DataReader from the snapshot's ``tombstone`` pointer,
58
+ consumed by executors to create a view that anti-joins the data on
59
+ ``__rowid__`` against the deletion-vector parquet, hiding rows that
60
+ have been logically deleted but not yet physically compacted.
62
61
 
63
- - primary_keys: columns that form the composite key.
64
- - deleted_keys: list of key tuples (each a list of scalars).
62
+ - tombstone_path: storage path of the deletion-vector parquet
63
+ (columns ``__file__`` + ``__rowid__``). ``None`` means no
64
+ tombstone exists, so no anti-join is applied.
65
65
  """
66
- primary_keys: List[str] = field(default_factory=list)
67
- deleted_keys: List = field(default_factory=list)
66
+ tombstone_path: Optional[str] = None
68
67
 
69
68
 
70
69
  @dataclass
@@ -79,7 +78,5 @@ class Reflection:
79
78
  freshness_ms: int = 0
80
79
  # alias -> RbacViewDef. Empty dict means no RBAC filtering.
81
80
  rbac_views: Dict[str, RbacViewDef] = field(default_factory=dict)
82
- # alias -> DedupViewDef. Empty dict means no dedup-on-read.
83
- dedup_views: Dict[str, DedupViewDef] = field(default_factory=dict)
84
81
  # alias -> TombstoneDef. Empty dict means no tombstone filtering.
85
82
  tombstone_views: Dict[str, TombstoneDef] = field(default_factory=dict)
@@ -23,8 +23,9 @@ from supertable.rbac.access_control import restrict_read_access # noqa: F401
23
23
  from supertable.engine.data_estimator import DataEstimator
24
24
  from supertable.engine.executor import Executor
25
25
  from supertable.engine.engine_enum import Engine as engine
26
- from supertable.data_classes import DedupViewDef, TombstoneDef, RbacViewDef
26
+ from supertable.data_classes import TombstoneDef, RbacViewDef
27
27
  from supertable.redis_catalog import RedisCatalog
28
+ from supertable.system_query import classify_query, CommandKind
28
29
 
29
30
 
30
31
  class Status(Enum):
@@ -109,6 +110,82 @@ class DataReader:
109
110
  self.organization, super_name, simple_name
110
111
  )
111
112
 
113
+ def _resolve_latest_stats_file(
114
+ self, super_name: str, simple_name: str,
115
+ ) -> Optional[str]:
116
+ """Return the ``stats_file`` pointer of the table's latest snapshot.
117
+
118
+ Prefers the leaf payload (already in Redis); falls back to reading the
119
+ snapshot JSON from storage. ``None`` when the table has no stats artifact
120
+ yet (never written, or written before stats existed).
121
+ """
122
+ catalog = RedisCatalog()
123
+ leaf = catalog.get_leaf(self.organization, super_name, simple_name)
124
+ if not isinstance(leaf, dict):
125
+ return None
126
+ payload = leaf.get("payload")
127
+ if isinstance(payload, dict) and payload.get("stats_file"):
128
+ return payload["stats_file"]
129
+ path = leaf.get("path")
130
+ if not path:
131
+ return None
132
+ from supertable.super_table import SuperTable
133
+ snapshot = SuperTable(
134
+ super_name, self.organization, create_if_missing=False,
135
+ ).read_simple_table_snapshot(path)
136
+ return snapshot.get("stats_file") if isinstance(snapshot, dict) else None
137
+
138
+ def _execute_show_stats(
139
+ self, command, role_name: str,
140
+ ) -> Tuple[pd.DataFrame, Status, Optional[str]]:
141
+ """Return the raw contents of a table's latest statistics parquet.
142
+
143
+ Reads-never-create and table-level RBAC are enforced (the same gates a
144
+ SELECT hits); the statistics rows/columns themselves are returned
145
+ unfiltered. When the table exists but has no stats artifact yet, an empty
146
+ frame with the stats schema columns is returned (success, not error).
147
+ """
148
+ from supertable.data_classes import TableDefinition
149
+ from supertable.processing import load_stats, STATS_SCHEMA
150
+
151
+ super_name = command.super_name
152
+ simple_name = command.simple_name
153
+ td = TableDefinition(
154
+ super_name=super_name,
155
+ simple_name=simple_name,
156
+ alias=simple_name,
157
+ columns=[],
158
+ )
159
+
160
+ # Reads never create catalog entries.
161
+ try:
162
+ self._assert_targets_exist([td])
163
+ except (SuperTableNotFoundError, TableNotFoundError) as e:
164
+ logger.warning(self._lp(f"[show-stats] target missing: {e}"))
165
+ return pd.DataFrame(), Status.ERROR, str(e)
166
+
167
+ # Table-level RBAC: raises PermissionError if the role cannot read the
168
+ # table at all. columns=[] means "all columns", which skips column-level
169
+ # denial — we don't filter the stats output, only gate table access.
170
+ restrict_read_access(
171
+ super_name=super_name,
172
+ organization=self.organization,
173
+ role_name=role_name,
174
+ tables=[td],
175
+ physical_tables=[td],
176
+ )
177
+
178
+ try:
179
+ stats_file = self._resolve_latest_stats_file(super_name, simple_name)
180
+ stats_df = load_stats(stats_file, allow_cache=True) if stats_file else None
181
+ except Exception as e:
182
+ logger.error(self._lp(f"[show-stats] failed to load stats: {e}"))
183
+ return pd.DataFrame(), Status.ERROR, str(e)
184
+
185
+ if stats_df is None:
186
+ return pd.DataFrame(columns=list(STATS_SCHEMA.keys())), Status.OK, None
187
+ return stats_df.to_pandas(), Status.OK, None
188
+
112
189
  def execute(
113
190
  self,
114
191
  role_name: str,
@@ -120,10 +197,27 @@ class DataReader:
120
197
  self.timer = Timer()
121
198
  self.plan_stats = PlanStats()
122
199
 
123
- # Build parser with the correct dialect for the chosen engine.
200
+ # Classify into an allowed read-path command. Ordinary SELECTs fall
201
+ # through unchanged; EXPLAIN/SHOW STATS are the two diagnostic
202
+ # extensions. A recognised-but-malformed command (e.g. SHOW STATS with
203
+ # no table) returns a clean error rather than raising.
204
+ try:
205
+ command = classify_query(self.query, self.super_name)
206
+ except ValueError as e:
207
+ logger.warning(self._lp(f"rejected query: {e}"))
208
+ return pd.DataFrame(), Status.ERROR, str(e)
209
+
210
+ # SHOW STATS short-circuits the engine entirely — it returns the raw
211
+ # statistics artifact, no reflection/estimation/execution.
212
+ if command.kind is CommandKind.SHOW_STATS:
213
+ return self._execute_show_stats(command, role_name)
214
+
215
+ # Build parser with the correct dialect for the chosen engine. For
216
+ # EXPLAIN, parse only the inner SELECT so estimation/RBAC/reflection
217
+ # behave exactly as for the equivalent plain SELECT.
124
218
  parser = SQLParser(
125
219
  super_name=self.super_name,
126
- query=self.query,
220
+ query=command.sql,
127
221
  dialect=engine.dialect,
128
222
  )
129
223
  tables = parser.get_table_tuples()
@@ -180,11 +274,21 @@ class DataReader:
180
274
  self._log_ctx = f"[qid={self.query_plan_manager.query_id} qh={self.query_plan_manager.query_hash}] "
181
275
  self.query_plan_manager.original_table = ", ".join(t.simple_name for t in physical_tables) if physical_tables else ""
182
276
 
277
+ # Derive per-table WHERE constraints so the estimator can prune
278
+ # files via the stats artifact. Never let this break a read.
279
+ try:
280
+ predicate_constraints = parser.get_predicate_constraints()
281
+ except Exception as pc_err:
282
+ logger.debug(self._lp(f"[prune] predicate extraction failed: {pc_err}"))
283
+ predicate_constraints = {}
284
+
183
285
  # 1) ESTIMATE — use physical_tables so CTE aliases are excluded
184
286
  estimator = DataEstimator(
185
287
  organization=self.organization,
186
288
  storage=self.storage,
187
- tables=physical_tables
289
+ tables=physical_tables,
290
+ predicate_constraints=predicate_constraints,
291
+ plan_stats=self.plan_stats,
188
292
  )
189
293
  reflection = estimator.estimate()
190
294
 
@@ -194,26 +298,13 @@ class DataReader:
194
298
  # executors create filtered views for restricted roles.
195
299
  reflection.rbac_views = rbac_views
196
300
 
197
- # --- Dedup-on-read & tombstones: look up table configs and snapshot metadata ---
301
+ # --- Tombstone (deletion-vector): look up snapshot metadata ------
198
302
  try:
199
303
  catalog = RedisCatalog()
200
304
  for td in tables:
201
- tbl_cfg = catalog.get_table_config(
202
- self.organization, td.super_name, td.simple_name,
203
- )
204
- if tbl_cfg and tbl_cfg.get("dedup_on_read"):
205
- pk = tbl_cfg.get("primary_keys", [])
206
- if pk:
207
- # visible_columns: what the user actually asked for.
208
- # Empty list (SELECT *) means "all user columns" — the
209
- # executor will expose everything except __rn__.
210
- reflection.dedup_views[td.alias] = DedupViewDef(
211
- primary_keys=pk,
212
- order_column="__timestamp__",
213
- visible_columns=list(td.columns or []),
214
- )
215
-
216
- # Tombstone filtering: read from snapshot payload in Redis leaf
305
+ # Tombstone filtering: read the deletion-vector pointer from
306
+ # the snapshot payload in the Redis leaf. When present, the
307
+ # executor anti-joins the data on __rowid__ against it.
217
308
  payload = None
218
309
  try:
219
310
  leaf = catalog.get_leaf(
@@ -221,15 +312,11 @@ class DataReader:
221
312
  )
222
313
  payload = (leaf or {}).get("payload") if isinstance(leaf, dict) else None
223
314
  if isinstance(payload, dict):
224
- tomb_block = payload.get("tombstones")
225
- if isinstance(tomb_block, dict):
226
- tomb_keys = tomb_block.get("deleted_keys") or []
227
- tomb_pk = tomb_block.get("primary_keys") or []
228
- if tomb_keys and tomb_pk:
229
- reflection.tombstone_views[td.alias] = TombstoneDef(
230
- primary_keys=tomb_pk,
231
- deleted_keys=tomb_keys,
232
- )
315
+ tomb_path = payload.get("tombstone")
316
+ if tomb_path:
317
+ reflection.tombstone_views[td.alias] = TombstoneDef(
318
+ tombstone_path=tomb_path,
319
+ )
233
320
  except Exception as te:
234
321
  logger.debug(self._lp(f"[tombstone] leaf lookup failed for {td.alias}: {te}"))
235
322
 
@@ -262,15 +349,23 @@ class DataReader:
262
349
  message = "No parquet files found"
263
350
  return pd.DataFrame(), status, message
264
351
 
265
- # 2) EXECUTE
352
+ # 2) EXECUTE. EXPLAIN is pinned to DuckDB-lite so the plan is
353
+ # produced cheaply and uniformly (no Pro materialisation / Spark
354
+ # round trip) and prefixed onto the final rewritten query.
355
+ exec_engine = engine
356
+ if command.explain:
357
+ from supertable.engine.engine_enum import Engine as _EngineEnum
358
+ exec_engine = _EngineEnum.DUCKDB_LITE
266
359
  result_df, engine_used = executor.execute(
267
- engine=engine,
360
+ engine=exec_engine,
268
361
  reflection=reflection,
269
362
  parser=parser,
270
363
  query_manager=self.query_plan_manager,
271
364
  timer=self.timer,
272
365
  plan_stats=self.plan_stats,
273
366
  log_prefix=self._lp(""),
367
+ explain=command.explain,
368
+ explain_options=command.explain_options,
274
369
  )
275
370
  status = Status.OK
276
371
  except Exception as e:
@@ -337,8 +432,14 @@ def query_sql(
337
432
  the caller can correlate its own audit log to this read record.
338
433
  """
339
434
  # Safety guard: ensure a LIMIT is present so unbounded queries don't
340
- # overwhelm the MCP response payload.
341
- sql = _ensure_sql_limit(sql, default_limit=limit)
435
+ # overwhelm the MCP response payload. Only plain SELECTs take an appended
436
+ # LIMIT EXPLAIN output is tiny and SHOW STATS does not accept a LIMIT.
437
+ try:
438
+ is_select = classify_query(sql, super_name).kind is CommandKind.SELECT
439
+ except ValueError:
440
+ is_select = True
441
+ if is_select:
442
+ sql = _ensure_sql_limit(sql, default_limit=limit)
342
443
 
343
444
  reader = DataReader(
344
445
  organization=organization, super_name=super_name, query=sql, source=source,