supertable 2.2.4__tar.gz → 2.2.6__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 (178) hide show
  1. {supertable-2.2.4/supertable.egg-info → supertable-2.2.6}/PKG-INFO +1 -1
  2. {supertable-2.2.4 → supertable-2.2.6}/pyproject.toml +1 -1
  3. {supertable-2.2.4 → supertable-2.2.6}/setup.py +1 -1
  4. {supertable-2.2.4 → supertable-2.2.6}/supertable/__init__.py +1 -1
  5. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/duckdb_lite.py +6 -0
  6. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/duckdb_pro.py +6 -0
  7. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/engine_common.py +58 -3
  8. supertable-2.2.6/supertable/engine/engine_config.py +228 -0
  9. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/executor.py +32 -18
  10. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/spark_thrift.py +9 -3
  11. {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_catalog.py +54 -31
  12. {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_keys.py +21 -15
  13. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_redis_key_prefix.py +3 -5
  14. {supertable-2.2.4 → supertable-2.2.6/supertable.egg-info}/PKG-INFO +1 -1
  15. {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/SOURCES.txt +1 -0
  16. {supertable-2.2.4 → supertable-2.2.6}/LICENSE +0 -0
  17. {supertable-2.2.4 → supertable-2.2.6}/README.md +0 -0
  18. {supertable-2.2.4 → supertable-2.2.6}/requirements.txt +0 -0
  19. {supertable-2.2.4 → supertable-2.2.6}/setup.cfg +0 -0
  20. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/__init__.py +0 -0
  21. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/admin.py +0 -0
  22. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/chain.py +0 -0
  23. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/consumers.py +0 -0
  24. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/crypto.py +0 -0
  25. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/events.py +0 -0
  26. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/export.py +0 -0
  27. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/logger.py +0 -0
  28. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/middleware.py +0 -0
  29. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/reader.py +0 -0
  30. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/retention.py +0 -0
  31. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/__init__.py +0 -0
  32. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_chain.py +0 -0
  33. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_crypto.py +0 -0
  34. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_emit.py +0 -0
  35. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_events.py +0 -0
  36. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_retention.py +0 -0
  37. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/writer_parquet.py +0 -0
  38. {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/writer_redis.py +0 -0
  39. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/__init__.py +0 -0
  40. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/defaults.py +0 -0
  41. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/homedir.py +0 -0
  42. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/settings.py +0 -0
  43. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/__init__.py +0 -0
  44. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_defaults.py +0 -0
  45. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_homedir.py +0 -0
  46. {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_settings.py +0 -0
  47. {supertable-2.2.4 → supertable-2.2.6}/supertable/data_classes.py +0 -0
  48. {supertable-2.2.4 → supertable-2.2.6}/supertable/data_reader.py +0 -0
  49. {supertable-2.2.4 → supertable-2.2.6}/supertable/data_writer.py +0 -0
  50. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/__init__.py +0 -0
  51. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/__init__.py +0 -0
  52. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/__main__.py +0 -0
  53. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/check_filter_builder.py +0 -0
  54. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/controller.py +0 -0
  55. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
  56. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/defaults.py +0 -0
  57. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/dummy_data.py +0 -0
  58. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/read_parquet_header.py +0 -0
  59. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
  60. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
  61. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
  62. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
  63. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
  64. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
  65. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
  66. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
  67. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
  68. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
  69. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
  70. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
  71. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
  72. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
  73. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
  74. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
  75. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
  76. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
  77. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
  78. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
  79. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
  80. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
  81. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
  82. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
  83. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/__init__.py +0 -0
  84. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/core.py +0 -0
  85. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/defaults.py +0 -0
  86. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/generate.py +0 -0
  87. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/load.py +0 -0
  88. {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/topup.py +0 -0
  89. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/__init__.py +0 -0
  90. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/data_estimator.py +0 -0
  91. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/engine_enum.py +0 -0
  92. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/plan_stats.py +0 -0
  93. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/__init__.py +0 -0
  94. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/conftest.py +0 -0
  95. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/test_dedup_read.py +0 -0
  96. {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/test_engine.py +0 -0
  97. {supertable-2.2.4 → supertable-2.2.6}/supertable/errors.py +0 -0
  98. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/__init__.py +0 -0
  99. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/__init__.py +0 -0
  100. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
  101. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
  102. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
  103. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/file_lock.py +0 -0
  104. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/redis_lock.py +0 -0
  105. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/__init__.py +0 -0
  106. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/test_file_lock.py +0 -0
  107. {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/test_redis_lock.py +0 -0
  108. {supertable-2.2.4 → supertable-2.2.6}/supertable/logging.py +0 -0
  109. {supertable-2.2.4 → supertable-2.2.6}/supertable/meta_reader.py +0 -0
  110. {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/__init__.py +0 -0
  111. {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_delta.py +0 -0
  112. {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_formats.py +0 -0
  113. {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_iceberg.py +0 -0
  114. {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_parquet.py +0 -0
  115. {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring/__init__.py +0 -0
  116. {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring/partitions.py +0 -0
  117. {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring_writer.py +0 -0
  118. {supertable-2.2.4 → supertable-2.2.6}/supertable/plan_extender.py +0 -0
  119. {supertable-2.2.4 → supertable-2.2.6}/supertable/processing.py +0 -0
  120. {supertable-2.2.4 → supertable-2.2.6}/supertable/query_plan_manager.py +0 -0
  121. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/__init__.py +0 -0
  122. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/access_control.py +0 -0
  123. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/filter_builder.py +0 -0
  124. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/permissions.py +0 -0
  125. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/role_manager.py +0 -0
  126. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/row_column_security.py +0 -0
  127. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_filter_builder.py +0 -0
  128. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_rbac.py +0 -0
  129. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
  130. {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/user_manager.py +0 -0
  131. {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_connector.py +0 -0
  132. {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_infra.py +0 -0
  133. {supertable-2.2.4 → supertable-2.2.6}/supertable/simple_table.py +0 -0
  134. {supertable-2.2.4 → supertable-2.2.6}/supertable/staging_area.py +0 -0
  135. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/__init__.py +0 -0
  136. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/azure_storage.py +0 -0
  137. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/gcp_storage.py +0 -0
  138. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/local_storage.py +0 -0
  139. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/minio_storage.py +0 -0
  140. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/s3_storage.py +0 -0
  141. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/storage_factory.py +0 -0
  142. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/storage_interface.py +0 -0
  143. {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/tests/test_storage.py +0 -0
  144. {supertable-2.2.4 → supertable-2.2.6}/supertable/super_pipe.py +0 -0
  145. {supertable-2.2.4 → supertable-2.2.6}/supertable/super_table.py +0 -0
  146. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/__init__.py +0 -0
  147. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_align_to_schema_fix.py +0 -0
  148. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_create_if_missing.py +0 -0
  149. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_reader.py +0 -0
  150. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_reader_preflight.py +0 -0
  151. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer.py +0 -0
  152. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_compact.py +0 -0
  153. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_comprehensive.py +0 -0
  154. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_tombstones.py +0 -0
  155. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_dedup_on_read_write.py +0 -0
  156. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_errors.py +0 -0
  157. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_meta_reader.py +0 -0
  158. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_monitoring_partitions.py +0 -0
  159. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_monitoring_sink_guard.py +0 -0
  160. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_newer_than.py +0 -0
  161. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_process_delete_only.py +0 -0
  162. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_processing.py +0 -0
  163. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_processing_compact_resources.py +0 -0
  164. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_query_sql.py +0 -0
  165. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_simple_table.py +0 -0
  166. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_small_file_compaction.py +0 -0
  167. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_super_table.py +0 -0
  168. {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_supertable_all.py +0 -0
  169. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/__init__.py +0 -0
  170. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/helper.py +0 -0
  171. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/profiler.py +0 -0
  172. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/sql_parser.py +0 -0
  173. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
  174. {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/timer.py +0 -0
  175. {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/dependency_links.txt +0 -0
  176. {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/entry_points.txt +0 -0
  177. {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/requires.txt +0 -0
  178. {supertable-2.2.4 → supertable-2.2.6}/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.4
3
+ Version: 2.2.6
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.4"
7
+ version = "2.2.6"
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.4",
22
+ version="2.2.6",
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.4"
28
+ __version__ = "2.2.6"
29
29
 
30
30
  # Re-export the core public surface so users can do ``from supertable import …``
31
31
  # instead of remembering submodule paths.
@@ -21,6 +21,7 @@ from supertable.engine.engine_common import (
21
21
  create_reflection_view_with_presign_retry,
22
22
  rewrite_query_with_hashed_tables,
23
23
  init_connection,
24
+ apply_runtime_pragmas,
24
25
  create_rbac_view,
25
26
  create_dedup_view,
26
27
  create_tombstone_view,
@@ -105,6 +106,7 @@ class DuckDBLite:
105
106
  query_manager: QueryPlanManager,
106
107
  timer_capture,
107
108
  log_prefix: str = "",
109
+ engine_config=None,
108
110
  ) -> pd.DataFrame:
109
111
  tried_presign = False
110
112
 
@@ -237,6 +239,10 @@ class DuckDBLite:
237
239
  except Exception:
238
240
  pass
239
241
 
242
+ # Re-apply live engine config (memory/threads/http/cache) so UI
243
+ # changes take effect on this persistent connection per query.
244
+ apply_runtime_pragmas(con, engine_config)
245
+
240
246
  logger.debug(f"{log_prefix}[duckdb.lite] executing: {executing_query}")
241
247
  result = con.execute(executing_query).fetchdf()
242
248
 
@@ -25,6 +25,7 @@ from supertable.engine.engine_common import (
25
25
  make_presigned_list,
26
26
  rewrite_query_with_hashed_tables,
27
27
  init_connection,
28
+ apply_runtime_pragmas,
28
29
  create_rbac_view,
29
30
  create_dedup_view,
30
31
  create_tombstone_view,
@@ -268,6 +269,7 @@ class DuckDBPro:
268
269
  query_manager: QueryPlanManager,
269
270
  timer_capture,
270
271
  log_prefix: str = "",
272
+ engine_config=None,
271
273
  ) -> pd.DataFrame:
272
274
  tables_used: Set[str] = set()
273
275
 
@@ -368,6 +370,10 @@ class DuckDBPro:
368
370
 
369
371
  logger.debug(f"{log_prefix}[duckdb.pro] executing: {executing_query}")
370
372
 
373
+ # Re-apply live engine config (memory/threads/http/cache) so UI
374
+ # changes take effect on this persistent connection per query.
375
+ apply_runtime_pragmas(con, engine_config)
376
+
371
377
  # Profiling PRAGMAs are connection-level state. Under concurrent
372
378
  # queries the last SET wins — one query's profile may land in the
373
379
  # wrong file. This is acceptable: profiling is best-effort
@@ -140,7 +140,11 @@ def _parse_memory_limit_mb(value: str) -> Optional[int]:
140
140
  return int(amount)
141
141
 
142
142
 
143
- def _derive_thread_count(memory_limit_str: str, fallback: int = 2) -> int:
143
+ def _derive_thread_count(
144
+ memory_limit_str: str,
145
+ fallback: int = 2,
146
+ multiplier: Optional[float] = None,
147
+ ) -> int:
144
148
  """Derive a safe DuckDB thread count for remote parquet reads.
145
149
 
146
150
  DuckDB uses synchronous IO: each thread can make at most one HTTP/S3
@@ -170,9 +174,10 @@ def _derive_thread_count(memory_limit_str: str, fallback: int = 2) -> int:
170
174
  return fallback
171
175
 
172
176
  cpu_count = _os.cpu_count() or fallback
173
- multiplier = settings.SUPERTABLE_DUCKDB_IO_MULTIPLIER
177
+ if multiplier is None:
178
+ multiplier = settings.SUPERTABLE_DUCKDB_IO_MULTIPLIER
174
179
 
175
- io_threads = cpu_count * multiplier
180
+ io_threads = int(cpu_count * multiplier)
176
181
  # Safety ceiling: never allocate fewer than ~400 MB per thread.
177
182
  memory_floor = max(1, mb // 400)
178
183
  result = min(io_threads, memory_floor)
@@ -625,6 +630,56 @@ def init_connection(
625
630
  pass
626
631
 
627
632
 
633
+ def apply_runtime_pragmas(con: duckdb.DuckDBPyConnection, cfg) -> None:
634
+ """Re-apply the session-settable DuckDB pragmas from a live engine config.
635
+
636
+ DuckDB Lite/Pro reuse a persistent connection, so settings applied once at
637
+ ``init_connection`` time would otherwise freeze for the connection's life.
638
+ Calling this immediately before each query makes the org's engine config
639
+ (memory limit, thread count, HTTP timeout, external file cache) take effect
640
+ live — the connection adopts the latest values without being torn down.
641
+
642
+ ``cfg`` is an ``EngineRuntimeConfig``. When None the connection keeps
643
+ whatever ``init_connection`` applied (settings-based), so callers without a
644
+ resolved config are unaffected. Every pragma is best-effort: httpfs-only
645
+ settings silently no-op on connections that have not loaded httpfs.
646
+ """
647
+ if cfg is None:
648
+ return
649
+
650
+ memory_limit = (cfg.duckdb_memory_limit or "").strip() or "1GB"
651
+ try:
652
+ con.execute(f"PRAGMA memory_limit='{memory_limit}';")
653
+ except Exception:
654
+ pass
655
+
656
+ # Explicit thread count wins; otherwise derive from the live memory limit
657
+ # and IO multiplier (same formula as init_connection).
658
+ if cfg.duckdb_threads is not None:
659
+ thread_count = cfg.duckdb_threads
660
+ else:
661
+ thread_count = _derive_thread_count(memory_limit, multiplier=cfg.duckdb_io_multiplier)
662
+ try:
663
+ con.execute(f"SET threads={thread_count};")
664
+ except Exception:
665
+ pass
666
+
667
+ # httpfs settings — only effective once httpfs is loaded (remote reads).
668
+ if cfg.duckdb_http_timeout is not None:
669
+ try:
670
+ con.execute(f"SET http_timeout={int(cfg.duckdb_http_timeout)};")
671
+ except Exception:
672
+ pass
673
+
674
+ cache_size = (cfg.duckdb_external_cache_size or "").strip()
675
+ if cache_size:
676
+ try:
677
+ con.execute("SET enable_external_file_cache=true;")
678
+ con.execute(f"SET external_file_cache_max_size='{sanitize_sql_string(cache_size)}';")
679
+ except Exception:
680
+ pass
681
+
682
+
628
683
  # =========================================================
629
684
  # RBAC view creation
630
685
  # =========================================================
@@ -0,0 +1,228 @@
1
+ # route: supertable.engine.engine_config
2
+ """
3
+ Engine runtime configuration — single source of truth for resolution.
4
+
5
+ Engine config is stored once per organization at **system scope**
6
+ (``supertable:{org}:system:engine:duckdb``). It is global to the org, not
7
+ per-supertable, and is resolved **live on every query** (no caching) so that
8
+ changes made in the UI take effect on the next query without a restart or a
9
+ connection reset.
10
+
11
+ Two kinds of settings live in that document:
12
+
13
+ * **Shared auto-pick thresholds** (one value, used by the router):
14
+ ``engine_lite_max_bytes``, ``engine_spark_min_bytes``,
15
+ ``engine_freshness_sec``. These describe the *boundaries* between engines
16
+ so they cannot be per-engine.
17
+ * **Per-engine DuckDB runtime pragmas** (separate values for Lite and Pro):
18
+ ``duckdb_memory_limit``, ``duckdb_io_multiplier``, ``duckdb_threads``,
19
+ ``duckdb_http_timeout``, ``duckdb_external_cache_size``. Each DuckDB engine
20
+ carries its own copy under a ``"lite"`` / ``"pro"`` section, so tuning Lite
21
+ never changes Pro and vice-versa.
22
+
23
+ Stored document shape::
24
+
25
+ {
26
+ "engine_lite_max_bytes": "104857600",
27
+ "engine_spark_min_bytes": "10737418240",
28
+ "engine_freshness_sec": "300",
29
+ "lite": {"duckdb_memory_limit": "1GB", ...},
30
+ "pro": {"duckdb_memory_limit": "8GB", ...},
31
+ "modified_ms": 1750000000000
32
+ }
33
+
34
+ Resolution precedence, per field::
35
+
36
+ Redis (org system config) → environment variable → built-in default
37
+
38
+ The environment variable is a single global fallback shared by both engines;
39
+ per-engine differentiation therefore only happens once a value is stored in
40
+ Redis (via the UI).
41
+
42
+ Entry points (all share that precedence so engine and UI never disagree):
43
+
44
+ * :func:`resolve_engine_configs` → ``{"lite": cfg, "pro": cfg}`` in a
45
+ single Redis read; used by the
46
+ executor.
47
+ * :func:`resolve_engine_config` → typed :class:`EngineRuntimeConfig`
48
+ for one engine (convenience).
49
+ * :func:`resolve_engine_config_provenance` → ``{shared, lite, pro,
50
+ modified_ms}`` per-field
51
+ ``{value, source, env_var,
52
+ default}`` for the API / UI.
53
+
54
+ Built-in defaults mirror the :mod:`supertable.config.settings` dataclass
55
+ defaults, so behaviour is identical whether a value flows through ``settings``
56
+ (no Redis override) or through this resolver.
57
+ """
58
+ from __future__ import annotations
59
+
60
+ import os
61
+ from dataclasses import dataclass
62
+ from typing import Any, Dict, Optional, Tuple
63
+
64
+
65
+ # Shared auto-pick thresholds: key → (env var, built-in default as string).
66
+ _SHARED_SPEC: Dict[str, Tuple[str, str]] = {
67
+ "engine_lite_max_bytes": ("SUPERTABLE_ENGINE_LITE_MAX_BYTES", str(100 * 1024 * 1024)),
68
+ "engine_spark_min_bytes": ("SUPERTABLE_ENGINE_SPARK_MIN_BYTES", str(10 * 1024 * 1024 * 1024)),
69
+ "engine_freshness_sec": ("SUPERTABLE_ENGINE_FRESHNESS_SEC", "300"),
70
+ }
71
+
72
+ # Per-engine DuckDB runtime pragmas: key → (env var, built-in default as string).
73
+ # Defaults mirror the settings dataclass defaults in supertable/config/settings.py.
74
+ _DUCKDB_SPEC: Dict[str, Tuple[str, str]] = {
75
+ "duckdb_memory_limit": ("SUPERTABLE_DUCKDB_MEMORY_LIMIT", "1GB"),
76
+ "duckdb_io_multiplier": ("SUPERTABLE_DUCKDB_IO_MULTIPLIER", "3"),
77
+ "duckdb_threads": ("SUPERTABLE_DUCKDB_THREADS", ""),
78
+ "duckdb_http_timeout": ("SUPERTABLE_DUCKDB_HTTP_TIMEOUT", ""),
79
+ "duckdb_external_cache_size": ("SUPERTABLE_DUCKDB_EXTERNAL_CACHE_SIZE", "5GB"),
80
+ }
81
+
82
+ # The two DuckDB engines that each carry their own runtime section.
83
+ DUCKDB_ENGINES: Tuple[str, ...] = ("lite", "pro")
84
+
85
+ # Public whitelists (for callers / catalog / tests).
86
+ SHARED_CONFIG_FIELDS: Tuple[str, ...] = tuple(_SHARED_SPEC.keys())
87
+ DUCKDB_CONFIG_FIELDS: Tuple[str, ...] = tuple(_DUCKDB_SPEC.keys())
88
+ ENGINE_CONFIG_FIELDS: Tuple[str, ...] = SHARED_CONFIG_FIELDS + DUCKDB_CONFIG_FIELDS
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class EngineRuntimeConfig:
93
+ """Effective, typed config for one DuckDB engine resolution.
94
+
95
+ Carries the shared auto-pick thresholds (identical across engines) plus the
96
+ DuckDB session pragmas for the engine it was resolved for.
97
+ """
98
+
99
+ # auto-pick thresholds (shared)
100
+ engine_lite_max_bytes: int
101
+ engine_spark_min_bytes: int
102
+ engine_freshness_sec: int
103
+
104
+ # duckdb session pragmas (per-engine, re-applied live per query)
105
+ duckdb_memory_limit: str # e.g. "1GB"
106
+ duckdb_io_multiplier: float
107
+ duckdb_threads: Optional[int] # None → auto-derive from memory limit
108
+ duckdb_http_timeout: Optional[int] # seconds; None → leave DuckDB default
109
+ duckdb_external_cache_size: str # "" → external file cache disabled
110
+
111
+
112
+ def _redis_cfg(org: str, catalog: Optional[Any]) -> Dict[str, Any]:
113
+ """Read the stored engine config for ``org`` (best effort).
114
+
115
+ Returns an empty dict when no catalog is supplied or Redis is unreachable,
116
+ so the resolver degrades cleanly to env vars + defaults.
117
+ """
118
+ if not (org and catalog is not None):
119
+ return {}
120
+ try:
121
+ return catalog.get_engine_config(org) or {}
122
+ except Exception:
123
+ return {}
124
+
125
+
126
+ def _effective(stored: Dict[str, Any], key: str, spec: Dict[str, Tuple[str, str]]) -> Tuple[str, str]:
127
+ """Return ``(value, source)`` for one field. source ∈ redis|env|default.
128
+
129
+ ``stored`` is the dict the value is looked up in directly: the top-level
130
+ document for shared fields, or the ``lite`` / ``pro`` sub-section for
131
+ per-engine fields.
132
+ """
133
+ env_var, default = spec[key]
134
+ redis_val = stored.get(key) if isinstance(stored, dict) else None
135
+ if redis_val is not None and str(redis_val).strip() != "":
136
+ return str(redis_val).strip(), "redis"
137
+ env_val = (os.getenv(env_var) or "").strip()
138
+ if env_val:
139
+ return env_val, "env"
140
+ return default, "default"
141
+
142
+
143
+ def _to_int(s: Any, fallback: int) -> int:
144
+ try:
145
+ return int(str(s).strip())
146
+ except (TypeError, ValueError):
147
+ return fallback
148
+
149
+
150
+ def _to_float(s: Any, fallback: float) -> float:
151
+ try:
152
+ return float(str(s).strip())
153
+ except (TypeError, ValueError):
154
+ return fallback
155
+
156
+
157
+ def _build_runtime(redis_cfg: Dict[str, Any], engine: str) -> EngineRuntimeConfig:
158
+ """Build the typed config for ``engine`` ("lite"/"pro") from a stored doc."""
159
+ section = redis_cfg.get(engine) if isinstance(redis_cfg.get(engine), dict) else {}
160
+
161
+ sv = {k: _effective(redis_cfg, k, _SHARED_SPEC)[0] for k in _SHARED_SPEC}
162
+ dv = {k: _effective(section, k, _DUCKDB_SPEC)[0] for k in _DUCKDB_SPEC}
163
+
164
+ threads = _to_int(dv["duckdb_threads"], 0) if dv["duckdb_threads"] else 0
165
+ http = _to_int(dv["duckdb_http_timeout"], 0) if dv["duckdb_http_timeout"] else 0
166
+
167
+ return EngineRuntimeConfig(
168
+ engine_lite_max_bytes=_to_int(sv["engine_lite_max_bytes"], 100 * 1024 * 1024),
169
+ engine_spark_min_bytes=_to_int(sv["engine_spark_min_bytes"], 10 * 1024 * 1024 * 1024),
170
+ engine_freshness_sec=_to_int(sv["engine_freshness_sec"], 300),
171
+ duckdb_memory_limit=dv["duckdb_memory_limit"].strip() or "1GB",
172
+ duckdb_io_multiplier=_to_float(dv["duckdb_io_multiplier"], 3.0),
173
+ duckdb_threads=(threads if threads > 0 else None),
174
+ duckdb_http_timeout=(http if http > 0 else None),
175
+ duckdb_external_cache_size=dv["duckdb_external_cache_size"].strip(),
176
+ )
177
+
178
+
179
+ def resolve_engine_configs(org: str, catalog: Optional[Any] = None) -> Dict[str, EngineRuntimeConfig]:
180
+ """Resolve both DuckDB engines in a single Redis read.
181
+
182
+ Returns ``{"lite": EngineRuntimeConfig, "pro": EngineRuntimeConfig}``. The
183
+ shared thresholds are identical in both; only the DuckDB pragmas differ.
184
+ """
185
+ cfg = _redis_cfg(org, catalog)
186
+ return {e: _build_runtime(cfg, e) for e in DUCKDB_ENGINES}
187
+
188
+
189
+ def resolve_engine_config(org: str, catalog: Optional[Any] = None, engine: str = "lite") -> EngineRuntimeConfig:
190
+ """Resolve effective config for a single ``engine`` (Redis → env → default).
191
+
192
+ ``catalog`` is an optional ``RedisCatalog``. When it is None or Redis is
193
+ unreachable the resolver falls back to environment variables and built-in
194
+ defaults, which keeps the engine usable without Redis (e.g. in unit tests).
195
+ """
196
+ engine = engine if engine in DUCKDB_ENGINES else "lite"
197
+ return _build_runtime(_redis_cfg(org, catalog), engine)
198
+
199
+
200
+ def resolve_engine_config_provenance(org: str, catalog: Optional[Any] = None) -> Dict[str, Any]:
201
+ """Per-field provenance for the UI.
202
+
203
+ Shape::
204
+
205
+ {
206
+ "shared": {field: {value, source, env_var, default}, ...},
207
+ "lite": {field: {value, source, env_var, default}, ...},
208
+ "pro": {field: {value, source, env_var, default}, ...},
209
+ "modified_ms": <int|None>,
210
+ }
211
+
212
+ ``source`` is one of ``redis`` / ``env`` / ``default``.
213
+ """
214
+ cfg = _redis_cfg(org, catalog)
215
+
216
+ def _block(stored: Dict[str, Any], spec: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
217
+ out: Dict[str, Any] = {}
218
+ for key, (env_var, default) in spec.items():
219
+ value, source = _effective(stored, key, spec)
220
+ out[key] = {"value": value, "source": source, "env_var": env_var, "default": default}
221
+ return out
222
+
223
+ result: Dict[str, Any] = {"shared": _block(cfg, _SHARED_SPEC)}
224
+ for engine in DUCKDB_ENGINES:
225
+ section = cfg.get(engine) if isinstance(cfg.get(engine), dict) else {}
226
+ result[engine] = _block(section, _DUCKDB_SPEC)
227
+ result["modified_ms"] = cfg.get("modified_ms")
228
+ return result
@@ -16,9 +16,9 @@ from supertable.utils.sql_parser import SQLParser
16
16
  from supertable.engine.engine_enum import Engine
17
17
  from supertable.engine.duckdb_lite import DuckDBLite
18
18
  from supertable.engine.duckdb_pro import DuckDBPro
19
+ from supertable.engine.engine_config import resolve_engine_configs, EngineRuntimeConfig
19
20
  from supertable.data_classes import Reflection
20
21
  from supertable.config.defaults import logger
21
- from supertable.config.settings import settings
22
22
 
23
23
 
24
24
  # Module-level singleton for the pro executor so the persistent
@@ -46,8 +46,23 @@ class Executor:
46
46
  self.organization = organization
47
47
  self.lite_exec = DuckDBLite(storage=storage)
48
48
  self.spark_exec = None
49
+ self._catalog = None # lazily created RedisCatalog for live config reads
49
50
 
50
- def _auto_pick(self, reflection: Reflection) -> Engine:
51
+ def _get_catalog(self):
52
+ """Lazily create a RedisCatalog for live engine-config reads.
53
+
54
+ Returns None when Redis is unreachable so config resolution degrades to
55
+ environment variables and built-in defaults instead of failing a query.
56
+ """
57
+ if self._catalog is None:
58
+ try:
59
+ from supertable.redis_catalog import RedisCatalog
60
+ self._catalog = RedisCatalog()
61
+ except Exception:
62
+ self._catalog = False # sentinel: construction failed, do not retry
63
+ return self._catalog or None
64
+
65
+ def _auto_pick(self, reflection: Reflection, cfg: EngineRuntimeConfig) -> Engine:
51
66
  """Select the best engine based on data size and freshness.
52
67
 
53
68
  Decision matrix (all thresholds configurable via env vars):
@@ -74,21 +89,10 @@ class Executor:
74
89
  """
75
90
  bytes_total = reflection.reflection_bytes
76
91
 
77
- # --- thresholds (env-configurable) ---
78
- try:
79
- lite_max = settings.SUPERTABLE_ENGINE_LITE_MAX_BYTES
80
- except ValueError:
81
- lite_max = 100 * 1024 * 1024 # 100 MB
82
-
83
- try:
84
- spark_min = settings.SUPERTABLE_ENGINE_SPARK_MIN_BYTES
85
- except ValueError:
86
- spark_min = 10 * 1024 * 1024 * 1024 # 10 GB
87
-
88
- try:
89
- freshness_threshold_s = settings.SUPERTABLE_ENGINE_FRESHNESS_SEC
90
- except ValueError:
91
- freshness_threshold_s = 300 # 5 min
92
+ # --- thresholds (resolved live from org system config) ---
93
+ lite_max = cfg.engine_lite_max_bytes
94
+ spark_min = cfg.engine_spark_min_bytes
95
+ freshness_threshold_s = cfg.engine_freshness_sec
92
96
 
93
97
  # --- freshness: how long ago was the most recent snapshot updated ---
94
98
  if reflection.freshness_ms > 0:
@@ -138,7 +142,15 @@ class Executor:
138
142
  plan_stats: PlanStats,
139
143
  log_prefix: str,
140
144
  ) -> Tuple[pd.DataFrame, str]:
141
- chosen = engine if engine != Engine.AUTO else self._auto_pick(reflection)
145
+ # Resolve engine config live (Redis env → default) for this query so
146
+ # UI changes take effect immediately without restart or cache. Lite and
147
+ # Pro carry independent DuckDB pragmas; the shared auto-pick thresholds
148
+ # are identical in both, so either may drive the routing decision.
149
+ cfgs = resolve_engine_configs(self.organization, self._get_catalog())
150
+ lite_cfg = cfgs["lite"]
151
+ pro_cfg = cfgs["pro"]
152
+
153
+ chosen = engine if engine != Engine.AUTO else self._auto_pick(reflection, lite_cfg)
142
154
 
143
155
  def timer_capture(evt: str):
144
156
  timer.capture_and_reset_timing(evt)
@@ -150,6 +162,7 @@ class Executor:
150
162
  query_manager=query_manager,
151
163
  timer_capture=timer_capture,
152
164
  log_prefix=log_prefix,
165
+ engine_config=lite_cfg,
153
166
  )
154
167
  used = "duckdb_lite"
155
168
 
@@ -161,6 +174,7 @@ class Executor:
161
174
  query_manager=query_manager,
162
175
  timer_capture=timer_capture,
163
176
  log_prefix=log_prefix,
177
+ engine_config=pro_cfg,
164
178
  )
165
179
  used = "duckdb_pro"
166
180
 
@@ -25,7 +25,7 @@ from supertable.engine.engine_common import (
25
25
  escape_parquet_path,
26
26
  )
27
27
 
28
- from urllib.parse import urlparse
28
+ from urllib.parse import urlparse, unquote
29
29
 
30
30
  # Suppress verbose INFO logging from PyHive and Thrift libraries.
31
31
  # PyHive logs every SQL statement (CREATE VIEW, DROP VIEW, SET, etc.)
@@ -77,8 +77,14 @@ def _to_s3a_path(file_path: str) -> str:
77
77
 
78
78
  if file_path.startswith("http://") or file_path.startswith("https://"):
79
79
  parsed = urlparse(file_path)
80
- # path is /bucket/key — strip the leading slash, split into bucket + key
81
- path = parsed.path.lstrip("/")
80
+ # path is /bucket/key — strip the leading slash, split into bucket + key.
81
+ # urlparse leaves percent-escapes intact, and boto3 presigning encodes
82
+ # the key (Hive partition dirs like ``year=2006`` become ``year%3D2006``).
83
+ # Spark's S3A client expects the *raw* object key and re-encodes itself,
84
+ # so we must decode here — otherwise Ceph RGW, which matches keys
85
+ # literally, 404s on the ``%3D`` form. The presign query string is
86
+ # dropped on purpose: Spark authenticates with its own fs.s3a.* creds.
87
+ path = unquote(parsed.path).lstrip("/")
82
88
  if "/" in path:
83
89
  return f"s3a://{path}"
84
90
  # Degenerate case: only bucket, no key — return as-is