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.
- {supertable-2.2.8/supertable.egg-info → supertable-2.3.0}/PKG-INFO +1 -1
- {supertable-2.2.8 → supertable-2.3.0}/pyproject.toml +1 -1
- {supertable-2.2.8 → supertable-2.3.0}/setup.py +1 -1
- {supertable-2.2.8 → supertable-2.3.0}/supertable/__init__.py +1 -1
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/defaults.py +2 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/settings.py +14 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_defaults.py +4 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/data_classes.py +28 -31
- {supertable-2.2.8 → supertable-2.3.0}/supertable/data_reader.py +135 -34
- {supertable-2.2.8 → supertable-2.3.0}/supertable/data_writer.py +295 -190
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/data_estimator.py +107 -7
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/duckdb_lite.py +45 -48
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/duckdb_pro.py +23 -49
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_common.py +71 -108
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/executor.py +4 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/spark_thrift.py +61 -120
- {supertable-2.2.8 → supertable-2.3.0}/supertable/meta_reader.py +6 -0
- supertable-2.3.0/supertable/processing.py +1805 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_catalog.py +14 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_keys.py +16 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/simple_table.py +20 -1
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/tests/test_storage.py +77 -4
- supertable-2.3.0/supertable/system_query.py +135 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_reader.py +343 -1
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer.py +113 -32
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer_compact.py +97 -63
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_writer_comprehensive.py +94 -178
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_meta_reader.py +44 -0
- supertable-2.3.0/supertable/tests/test_parquet_statistics.py +86 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_processing.py +168 -400
- supertable-2.3.0/supertable/tests/test_processing_stats.py +328 -0
- supertable-2.3.0/supertable/tests/test_read_pruning_differential.py +457 -0
- supertable-2.3.0/supertable/tests/test_read_pruning_integration.py +352 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_redis_key_prefix.py +19 -0
- supertable-2.3.0/supertable/tests/test_stats_cache.py +152 -0
- supertable-2.3.0/supertable/tests/test_stats_pruning.py +557 -0
- supertable-2.3.0/supertable/tests/test_stats_schema_snapshot.py +115 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_supertable_all.py +90 -372
- supertable-2.3.0/supertable/tests/test_system_query.py +216 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/sql_parser.py +291 -1
- {supertable-2.2.8 → supertable-2.3.0/supertable.egg-info}/PKG-INFO +1 -1
- {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/SOURCES.txt +9 -5
- supertable-2.2.8/supertable/engine/tests/test_dedup_read.py +0 -1172
- supertable-2.2.8/supertable/processing.py +0 -1480
- supertable-2.2.8/supertable/tests/test_data_writer_tombstones.py +0 -802
- supertable-2.2.8/supertable/tests/test_dedup_on_read_write.py +0 -1037
- supertable-2.2.8/supertable/tests/test_process_delete_only.py +0 -447
- supertable-2.2.8/supertable/tests/test_small_file_compaction.py +0 -959
- {supertable-2.2.8 → supertable-2.3.0}/LICENSE +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/README.md +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/requirements.txt +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/setup.cfg +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/admin.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/chain.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/consumers.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/crypto.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/events.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/export.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/logger.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/middleware.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/reader.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/retention.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/tests/test_retention.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/audit/writer_redis.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/homedir.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_config.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_config.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_routing.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/engine/tests/test_engine_spill.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/errors.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/file_lock.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/logging.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_formats.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring/partitions.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/monitoring_writer.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/plan_extender.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/query_plan_manager.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/access_control.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/permissions.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_rbac.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_connector.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/redis_infra.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/staging_area.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/local_storage.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/super_pipe.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/super_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_align_to_schema_fix.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_create_if_missing.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_data_reader_preflight.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_errors.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_monitoring_partitions.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_monitoring_sink_guard.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_processing_compact_resources.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/__init__.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/helper.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/profiler.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable/utils/timer.py +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.2.8 → supertable-2.3.0}/supertable.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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.
|
|
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 ``
|
|
60
|
-
consumed by executors to create a
|
|
61
|
-
|
|
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
|
-
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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=
|
|
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
|
-
# ---
|
|
301
|
+
# --- Tombstone (deletion-vector): look up snapshot metadata ------
|
|
198
302
|
try:
|
|
199
303
|
catalog = RedisCatalog()
|
|
200
304
|
for td in tables:
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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=
|
|
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
|
-
|
|
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,
|