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.
- {supertable-2.2.4/supertable.egg-info → supertable-2.2.6}/PKG-INFO +1 -1
- {supertable-2.2.4 → supertable-2.2.6}/pyproject.toml +1 -1
- {supertable-2.2.4 → supertable-2.2.6}/setup.py +1 -1
- {supertable-2.2.4 → supertable-2.2.6}/supertable/__init__.py +1 -1
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/duckdb_lite.py +6 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/duckdb_pro.py +6 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/engine_common.py +58 -3
- supertable-2.2.6/supertable/engine/engine_config.py +228 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/executor.py +32 -18
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/spark_thrift.py +9 -3
- {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_catalog.py +54 -31
- {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_keys.py +21 -15
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_redis_key_prefix.py +3 -5
- {supertable-2.2.4 → supertable-2.2.6/supertable.egg-info}/PKG-INFO +1 -1
- {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/SOURCES.txt +1 -0
- {supertable-2.2.4 → supertable-2.2.6}/LICENSE +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/README.md +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/requirements.txt +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/setup.cfg +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/admin.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/chain.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/consumers.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/crypto.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/events.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/export.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/logger.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/middleware.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/reader.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/retention.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/tests/test_retention.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/audit/writer_redis.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/defaults.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/homedir.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/settings.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_defaults.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/data_classes.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/data_reader.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/data_writer.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/data_estimator.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/test_dedup_read.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/errors.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/file_lock.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/logging.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/meta_reader.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_formats.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring/partitions.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/monitoring_writer.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/plan_extender.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/processing.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/query_plan_manager.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/access_control.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/permissions.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_rbac.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_connector.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/redis_infra.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/simple_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/staging_area.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/local_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/storage/tests/test_storage.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/super_pipe.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/super_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_align_to_schema_fix.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_create_if_missing.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_reader.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_reader_preflight.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_compact.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_comprehensive.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_data_writer_tombstones.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_dedup_on_read_write.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_errors.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_meta_reader.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_monitoring_partitions.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_monitoring_sink_guard.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_process_delete_only.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_processing.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_processing_compact_resources.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_small_file_compaction.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/tests/test_supertable_all.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/__init__.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/helper.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/profiler.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/sql_parser.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable/utils/timer.py +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.2.4 → supertable-2.2.6}/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.2.
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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 (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|