smart-data-engine-sdk 0.1.0rc1__tar.gz → 0.2.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.
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/PKG-INFO +1 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/__init__.py +8 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/cutover.py +2 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_index_build.py +88 -16
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_operator.py +123 -10
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_staging.py +5 -5
- smart_data_engine_sdk-0.2.0/src/sde/engines/orderbook.py +1207 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/postgres.py +74 -3
- smart_data_engine_sdk-0.2.0/src/sde/facts.py +94 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/generation.py +44 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/index_build.py +11 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/index_operator.py +13 -4
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/inspection.py +3 -2
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/layout.py +63 -12
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/local_cutover.py +11 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/logging.py +6 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/physical.py +33 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/placement.py +40 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/provisioning.py +15 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/query.py +36 -3
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/schema.py +41 -9
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/session.py +92 -5
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/staging.py +15 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/staging_operator.py +6 -2
- smart_data_engine_sdk-0.2.0/tests/_orderbook_live.py +62 -0
- smart_data_engine_sdk-0.2.0/tests/_unfenced_scenario.py +287 -0
- smart_data_engine_sdk-0.2.0/tests/orderbook_peer.py +116 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_slice.py +9 -2
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_compatibility_views.py +1 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_conformance.py +57 -5
- smart_data_engine_sdk-0.2.0/tests/test_engine_facts.py +124 -0
- smart_data_engine_sdk-0.2.0/tests/test_engine_facts_live.py +64 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_implementer_documents.py +46 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_index_operator_live.py +195 -16
- smart_data_engine_sdk-0.2.0/tests/test_index_use_live.py +375 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_native_live.py +107 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_orderbook_adapter.py +3 -1
- smart_data_engine_sdk-0.2.0/tests/test_orderbook_session_slice.py +347 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_orderbook_shape.py +79 -9
- smart_data_engine_sdk-0.2.0/tests/test_orderbook_slice.py +334 -0
- smart_data_engine_sdk-0.2.0/tests/test_orderbook_tcp.py +800 -0
- smart_data_engine_sdk-0.2.0/tests/test_orderbook_three_engines.py +30 -0
- smart_data_engine_sdk-0.2.0/tests/test_orderbook_trading_example.py +182 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_physical.py +36 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_readme_claims.py +4 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_release.py +43 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_runtime_privileges_live.py +65 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_session_hashed.py +1 -1
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_operator_live.py +60 -2
- smart_data_engine_sdk-0.2.0/tests/test_unfenced.py +152 -0
- smart_data_engine_sdk-0.2.0/tests/test_unfenced_operator_live.py +24 -0
- smart_data_engine_sdk-0.1.0rc1/src/sde/engines/orderbook.py +0 -457
- smart_data_engine_sdk-0.1.0rc1/tests/test_orderbook_slice.py +0 -272
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/.gitignore +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/LICENSE +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/NOTICE +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/README.md +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/pyproject.toml +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/_cutover_project.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/_local_state.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/_operator_deadline.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/_usage.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/bulk.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/canonical.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/capabilities.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/__init__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_clickhouse_connection.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_storage.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_write_fences.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/clickhouse.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/entity.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/errors.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/explain.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/frozen_verification.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/groups.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/hashing.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/infer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/internal.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/migration.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/model.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/py.typed +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/routing.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/shapes.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/telemetry.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/__init__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/loader.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/memory.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/types.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/verification.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/watermark.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/write_fence.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/__init__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/__main__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/diagnostics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/model.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/project.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/py.typed +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/query_count.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/resources.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/runtime.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/verification.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_operator/__init__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde_operator/__main__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_claims.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_cutover_driver.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_cutover_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_generation.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_index_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_staging_driver.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_staging_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_weather_fixture.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/_write_fence.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/bulk_vectors.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/datetime_query_peer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/fence_peer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/__init__.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/metrics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operation_metrics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operation_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operations.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operator_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/packets.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/project.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/run.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/transition.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/qualification/weather_worker.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_bulk.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_bulk_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_canonical.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_datetime_queries.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_tls.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_transport.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_copy_freshness.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_cutover_plan.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_cutover_project.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_query_count.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_resources.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_starter.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_starter_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_verification.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_denormalized_layout.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_dual_write.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_engine_agreement.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_enrollment_durability.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_explain_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_failure_semantics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_frozen_verification_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_map.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_migration_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_session_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_groups_and_shapes.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_hashing.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_index_change_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_index_plan.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_infer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_integration_guard.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_internal_failures.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_layout_dialects.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_local_cutover_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_map_unicode.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_map_unicode_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_measure_storage.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_measure_storage_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_migration.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_migration_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_model.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_neutral_declaration.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_no_account.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_no_account_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_no_expiry.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_no_retry.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_operation_metrics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_operation_oracle.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_cli.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_deadline.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_overhead.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_owned_session.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_owned_session_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_packaging.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_physical_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_placement_and_routing.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_platforms.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_postgres_slice.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_postgres_tls.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_cleanup.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_metrics.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_workers.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_query_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_query_plan.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_query_session.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_query_summary.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_query_vectors.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_rollback_protection.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_rollback_protection_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_schema_statements.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_schema_vectors_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_session.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_session_ownership_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_abandon_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_native_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_plan.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_storage_sizes_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_telemetry.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_telemetry_storage_time.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_verification_request.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_write_fence.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/test_write_fence_live.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/timestamp_peer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/tls_certificates.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/verification_peer.py +0 -0
- {smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/tests/weather_starter_peer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: smart-data-engine-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Smart Data Engine client library: declare a data model, we place it and move it
|
|
5
5
|
Project-URL: Homepage, https://github.com/Smart-Data-Engines/smart-data-engine-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/Smart-Data-Engines/smart-data-engine-sdk
|
|
@@ -51,6 +51,7 @@ from .errors import (
|
|
|
51
51
|
SdeError,
|
|
52
52
|
)
|
|
53
53
|
from .explain import Cost, Explains, PlanFinding, QueryPlan, QueryPlanRefused, explain
|
|
54
|
+
from .facts import FACTS_VERSION, engine_facts
|
|
54
55
|
from .frozen_verification import FrozenVerifyReport, verify_frozen
|
|
55
56
|
from .groups import Group, colocation_groups, group_of
|
|
56
57
|
from .hashing import NameMap, hash_identifiers, load_or_create_salt
|
|
@@ -69,6 +70,7 @@ from .layout import (
|
|
|
69
70
|
DIALECTS,
|
|
70
71
|
FIXED_SCHEMA,
|
|
71
72
|
ORDERBOOK_KEY,
|
|
73
|
+
ORDERBOOK_NULLABLE,
|
|
72
74
|
ORDERBOOK_SHAPE,
|
|
73
75
|
ORDERBOOK_TABLE,
|
|
74
76
|
DerivedLayout,
|
|
@@ -77,6 +79,7 @@ from .layout import (
|
|
|
77
79
|
denormalized_layout,
|
|
78
80
|
fixed_schema_mismatch,
|
|
79
81
|
group_columns,
|
|
82
|
+
group_nullable,
|
|
80
83
|
snake_case,
|
|
81
84
|
stored_types,
|
|
82
85
|
)
|
|
@@ -156,7 +159,7 @@ from .watermark import (
|
|
|
156
159
|
from .write_fence import EPOCH_COLUMN as WRITE_EPOCH_COLUMN
|
|
157
160
|
from .write_fence import FenceState, WriteFence
|
|
158
161
|
|
|
159
|
-
__version__ = "0.
|
|
162
|
+
__version__ = "0.2.0"
|
|
160
163
|
|
|
161
164
|
__all__ = [
|
|
162
165
|
"ALSO_WRITE_SINCE",
|
|
@@ -167,6 +170,7 @@ __all__ = [
|
|
|
167
170
|
"CUTOVER_RELAYOUT_PROTOCOL",
|
|
168
171
|
"DIALECTS",
|
|
169
172
|
"DIALECT_PRECISION",
|
|
173
|
+
"FACTS_VERSION",
|
|
170
174
|
"FIXED_SCHEMA",
|
|
171
175
|
"INDEX_CHANGE_PROTOCOL",
|
|
172
176
|
"INDEX_PROTOCOL",
|
|
@@ -177,6 +181,7 @@ __all__ = [
|
|
|
177
181
|
"MAX_PAGE_ROWS",
|
|
178
182
|
"MEASURED_FIELDS",
|
|
179
183
|
"ORDERBOOK_KEY",
|
|
184
|
+
"ORDERBOOK_NULLABLE",
|
|
180
185
|
"ORDERBOOK_SHAPE",
|
|
181
186
|
"ORDERBOOK_TABLE",
|
|
182
187
|
"PHYSICAL_DESIGN_SINCE",
|
|
@@ -277,11 +282,13 @@ __all__ = [
|
|
|
277
282
|
"denormalized_layout",
|
|
278
283
|
"digest16",
|
|
279
284
|
"enforce_forward_only",
|
|
285
|
+
"engine_facts",
|
|
280
286
|
"entity",
|
|
281
287
|
"enumerate_shapes",
|
|
282
288
|
"explain",
|
|
283
289
|
"fixed_schema_mismatch",
|
|
284
290
|
"group_columns",
|
|
291
|
+
"group_nullable",
|
|
285
292
|
"group_of",
|
|
286
293
|
"has_time_dimension",
|
|
287
294
|
"hash_identifiers",
|
|
@@ -191,6 +191,8 @@ def _load(
|
|
|
191
191
|
raise MigrationRefused("cutover verification must require the signed before map")
|
|
192
192
|
request.check_session(before, project_id=project_id, group=group)
|
|
193
193
|
spot = before.placement_of(group)
|
|
194
|
+
# A group without a write generation (contract 6) never gets this far: the map refuses a copy
|
|
195
|
+
# in such a group, and a cutover needs one.
|
|
194
196
|
if len(spot.derived) != 1 or spot.also_write != spot.derived:
|
|
195
197
|
raise MigrationRefused("cutover requires exactly one derived copy maintained by fan-out")
|
|
196
198
|
source, target = spot.source, spot.derived[0]
|
{smart_data_engine_sdk-0.1.0rc1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_index_build.py
RENAMED
|
@@ -21,6 +21,7 @@ from typing import Any, Literal
|
|
|
21
21
|
|
|
22
22
|
from .._operator_deadline import DeadlineInterrupt
|
|
23
23
|
from ..errors import MigrationRefused
|
|
24
|
+
from ..layout import POSTGRES_TYPES
|
|
24
25
|
from ..physical import CLICKHOUSE_METHODS, POSTGRES_METHODS, index_method
|
|
25
26
|
from ..schema import _skip_index_type, clickhouse_index_clause, postgres_index_target
|
|
26
27
|
from ._operator import NativeOperator, TableIdentity
|
|
@@ -29,8 +30,8 @@ Status = Literal["absent", "unfinished", "ready", "foreign"]
|
|
|
29
30
|
|
|
30
31
|
_NAME = re.compile(r"sde_i_[0-9a-f]{32}_[0-9]{6}")
|
|
31
32
|
"""The only names an in-place build creates. Plain identifiers, so ClickHouse records a mutation
|
|
32
|
-
over one
|
|
33
|
-
|
|
33
|
+
over one with the name unquoted - measured, and relied on to find a materialization again instead
|
|
34
|
+
of starting a second one (:meth:`NativeIndexBuild._ch_mutations`)."""
|
|
34
35
|
|
|
35
36
|
POLL_SECONDS = 0.2
|
|
36
37
|
|
|
@@ -66,13 +67,45 @@ class NativeIndexBuild:
|
|
|
66
67
|
def status(self, table: TableIdentity, index: Mapping[str, Any]) -> Status:
|
|
67
68
|
return self.inspect(table, index)[0]
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
70
|
+
def settle(self, table: TableIdentity, index: Mapping[str, Any]) -> None:
|
|
71
|
+
"""Wait until no statement still running in the server names this build's index.
|
|
72
|
+
|
|
73
|
+
On PostgreSQL a deadline or a killed operator ends the client, not its statement: the
|
|
74
|
+
server notices a vanished client only when it next talks to it, which for a concurrent
|
|
75
|
+
build is once the index is valid (measured, PostgreSQL 15.19). The build also releases the
|
|
76
|
+
table's session lock before its last transaction commits, so a drop waiting for that lock
|
|
77
|
+
started in the window and failed with "tuple concurrently updated". Read after the wait,
|
|
78
|
+
the catalogue says what that statement finished, which is kept, or left, which is dropped.
|
|
79
|
+
|
|
80
|
+
pg_stat_activity shows a statement's text to its own login, which is the one an earlier
|
|
81
|
+
run of this operator used. The wait is bounded by the operator's deadline. ClickHouse
|
|
82
|
+
needs none: its materialization is found again in ``system.mutations`` and waited for.
|
|
83
|
+
"""
|
|
84
|
+
name = self.quote(self._check(table, index))
|
|
85
|
+
if self.dialect != "postgres":
|
|
86
|
+
return
|
|
87
|
+
while self.native.rows(
|
|
88
|
+
"SELECT 1 FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND state = 'active' "
|
|
89
|
+
"AND strpos(query, %s) > 0",
|
|
90
|
+
[name],
|
|
91
|
+
):
|
|
92
|
+
time.sleep(POLL_SECONDS)
|
|
93
|
+
|
|
94
|
+
def build(
|
|
95
|
+
self, table: TableIdentity, index: Mapping[str, Any], columns: Mapping[str, str]
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Bring the index to ``ready`` without pausing the table's writers.
|
|
98
|
+
|
|
99
|
+
``columns`` is the entity's layout columns, from the map being built: PostgreSQL renders a
|
|
100
|
+
text column with the collation the library's reads use, and builds again an index of ours
|
|
101
|
+
that an earlier library left without it.
|
|
102
|
+
"""
|
|
103
|
+
self.settle(table, index)
|
|
71
104
|
status, reason = self.inspect(table, index)
|
|
72
105
|
if status == "foreign":
|
|
73
106
|
raise MigrationRefused(reason)
|
|
74
107
|
if self.dialect == "postgres":
|
|
75
|
-
self._pg_build(table, index, status)
|
|
108
|
+
self._pg_build(table, index, status, columns)
|
|
76
109
|
else:
|
|
77
110
|
self._ch_build(table, index, status)
|
|
78
111
|
if self.status(table, index) != "ready":
|
|
@@ -105,7 +138,9 @@ class NativeIndexBuild:
|
|
|
105
138
|
if self.dialect == "postgres":
|
|
106
139
|
# IF EXISTS only for an index that goes while this drop waits for its lock: an earlier
|
|
107
140
|
# drop whose client the budget closed runs on in the server until the transaction it
|
|
108
|
-
# waits for ends. What is left afterwards is read back below, as always.
|
|
141
|
+
# waits for ends. What is left afterwards is read back below, as always. No settle
|
|
142
|
+
# here, unlike a build: two drops of one index are serialized by the exclusive lock
|
|
143
|
+
# the first holds on it until it commits, so this one never meets a half-done drop.
|
|
109
144
|
self.native.command(f"DROP INDEX CONCURRENTLY IF EXISTS {self.quote(name)}")
|
|
110
145
|
else:
|
|
111
146
|
for mutation_id, is_done, _failure in self._ch_mutations(table, name):
|
|
@@ -127,7 +162,8 @@ class NativeIndexBuild:
|
|
|
127
162
|
|
|
128
163
|
A foreign object under the name means ours is not there: it is left as it is.
|
|
129
164
|
"""
|
|
130
|
-
|
|
165
|
+
self.settle(table, index) # checks the name and the table first
|
|
166
|
+
status, _ = self.inspect(table, index)
|
|
131
167
|
if status == "foreign":
|
|
132
168
|
return
|
|
133
169
|
name = str(index["name"])
|
|
@@ -195,14 +231,45 @@ class NativeIndexBuild:
|
|
|
195
231
|
)
|
|
196
232
|
return ("ready" if usable else "unfinished"), ""
|
|
197
233
|
|
|
198
|
-
def _pg_build(
|
|
199
|
-
|
|
234
|
+
def _pg_build(
|
|
235
|
+
self,
|
|
236
|
+
table: TableIdentity,
|
|
237
|
+
index: Mapping[str, Any],
|
|
238
|
+
status: Status,
|
|
239
|
+
columns: Mapping[str, str],
|
|
240
|
+
) -> None:
|
|
241
|
+
if status == "ready" and not self._pg_text_collation_stale(index, columns):
|
|
200
242
|
return
|
|
201
|
-
if status
|
|
202
|
-
# Our own
|
|
203
|
-
#
|
|
243
|
+
if status != "absent":
|
|
244
|
+
# Our own index, in one of two states that are no use. Unfinished: the leftover of an
|
|
245
|
+
# interrupted build, never valid again by itself and kept by IF NOT EXISTS. Ready with a
|
|
246
|
+
# text column outside the reads' collation: built for this map by a library from
|
|
247
|
+
# before 2 October 2026 and resumed by this one, valid and unusable by a single read.
|
|
248
|
+
# Either way removed without blocking writers, then built again.
|
|
204
249
|
self.native.command(f"DROP INDEX CONCURRENTLY {self.quote(str(index['name']))}")
|
|
205
|
-
self.native.command(
|
|
250
|
+
self.native.command(
|
|
251
|
+
f"CREATE INDEX CONCURRENTLY {postgres_index_target(index, table.name, columns)}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _pg_text_collation_stale(
|
|
255
|
+
self, index: Mapping[str, Any], columns: Mapping[str, str]
|
|
256
|
+
) -> bool:
|
|
257
|
+
"""Whether a text column of this ready index has a collation other than ``C``.
|
|
258
|
+
|
|
259
|
+
A ready index is plain and of exactly the declared columns (:meth:`_pg_status`), so its
|
|
260
|
+
collations line up with them one for one.
|
|
261
|
+
"""
|
|
262
|
+
rows = self.native.rows(
|
|
263
|
+
"SELECT ARRAY(SELECT coalesce(co.collname, '') FROM unnest(i.indcollation) "
|
|
264
|
+
"WITH ORDINALITY k(oid, pos) LEFT JOIN pg_collation co ON co.oid = k.oid "
|
|
265
|
+
"ORDER BY k.pos) FROM pg_index i WHERE i.indexrelid = to_regclass(%s)",
|
|
266
|
+
[self.quote(str(index["name"]))],
|
|
267
|
+
)
|
|
268
|
+
collations = [str(collation) for collation in rows[0][0]] if rows else []
|
|
269
|
+
return any(
|
|
270
|
+
columns.get(str(column)) == POSTGRES_TYPES["string"] and collation != "C"
|
|
271
|
+
for column, collation in zip(index["columns"], collations, strict=False)
|
|
272
|
+
)
|
|
206
273
|
|
|
207
274
|
# --- ClickHouse --------------------------------------------------------------------------
|
|
208
275
|
|
|
@@ -220,12 +287,17 @@ class NativeIndexBuild:
|
|
|
220
287
|
|
|
221
288
|
def _ch_mutations(self, table: TableIdentity, name: str) -> list[tuple[str, bool, str]]:
|
|
222
289
|
# KILL MUTATION removes the mutation from this table - measured - so what is listed here
|
|
223
|
-
# is either done or still to be done.
|
|
290
|
+
# is either done or still to be done. The command is recorded as the server formats it:
|
|
291
|
+
# `MATERIALIZE INDEX <name>` on 24.8, `(MATERIALIZE INDEX <name>)` on 26.5 (measured).
|
|
292
|
+
# Matching only the first, a build on 26.5 never saw its materialization: it waited out
|
|
293
|
+
# its budget, and every resume started another one.
|
|
294
|
+
command = f"MATERIALIZE INDEX {name}"
|
|
224
295
|
rows = self.native.rows(
|
|
225
296
|
"SELECT mutation_id, is_done, latest_fail_reason FROM system.mutations "
|
|
226
297
|
"WHERE database = currentDatabase() AND table = {table:String} "
|
|
227
|
-
"AND command
|
|
228
|
-
|
|
298
|
+
"AND command IN ({command:String}, {parenthesised:String}) AND NOT is_killed "
|
|
299
|
+
"ORDER BY create_time",
|
|
300
|
+
{"table": table.name, "command": command, "parenthesised": f"({command})"},
|
|
229
301
|
)
|
|
230
302
|
return [(str(mutation_id), bool(done), str(failure)) for mutation_id, done, failure in rows]
|
|
231
303
|
|
|
@@ -42,6 +42,14 @@ class NativeOperator:
|
|
|
42
42
|
from .clickhouse import ClickHouseEngine
|
|
43
43
|
from .postgres import PostgresEngine
|
|
44
44
|
|
|
45
|
+
if not callable(getattr(engine, "write_fence", None)):
|
|
46
|
+
# The orderbook engine. Its groups carry no write generation (map contract 6), so no
|
|
47
|
+
# staging, cutover or index build reaches them and there is nothing to bind it for.
|
|
48
|
+
raise MigrationRefused(
|
|
49
|
+
f"a local operator acts only on groups whose writes carry a generation, and "
|
|
50
|
+
f"{type(engine).__name__} cannot fence writes: leave its engine out of the "
|
|
51
|
+
f"operator bindings"
|
|
52
|
+
)
|
|
45
53
|
if not isinstance(engine, (PostgresEngine, ClickHouseEngine)) or not runtime:
|
|
46
54
|
raise MigrationRefused(
|
|
47
55
|
"local cutover requires stock operator adapters and runtime probes"
|
|
@@ -151,9 +159,20 @@ class NativeOperator:
|
|
|
151
159
|
return TableIdentity(self.dialect, server, database, str(oid), str(namespace), str(actual))
|
|
152
160
|
|
|
153
161
|
def qualify(
|
|
154
|
-
self,
|
|
162
|
+
self,
|
|
163
|
+
tables: Sequence[str],
|
|
164
|
+
allowed_tables: Sequence[str],
|
|
165
|
+
*,
|
|
166
|
+
required_access: bool = True,
|
|
167
|
+
readers: bool = True,
|
|
155
168
|
) -> tuple[str, ...]:
|
|
156
|
-
"""Require direct restricted logins whose table grants can be removed independently.
|
|
169
|
+
"""Require direct restricted logins whose table grants can be removed independently.
|
|
170
|
+
|
|
171
|
+
``readers`` also requires every other grantee of ``tables`` to be the operator or an
|
|
172
|
+
administrator (:meth:`_qualify_readers`), which is what a cutover stands on. An in-place
|
|
173
|
+
index build passes ``False``: it moves no authority, so a principal the operator does
|
|
174
|
+
not know reads and writes the same rows before the build and after it.
|
|
175
|
+
"""
|
|
157
176
|
endpoint = self.endpoint()
|
|
158
177
|
principals: list[str] = []
|
|
159
178
|
for probe in self.runtime:
|
|
@@ -281,12 +300,23 @@ class NativeOperator:
|
|
|
281
300
|
self.principal_ids[str(user)] = str(oid)
|
|
282
301
|
if len(set(principals)) != len(principals):
|
|
283
302
|
raise MigrationRefused("runtime probes repeat the same login")
|
|
284
|
-
|
|
303
|
+
if readers:
|
|
304
|
+
self._qualify_readers(tables, principals)
|
|
285
305
|
self.principals = tuple(sorted(principals))
|
|
286
306
|
return self.principals
|
|
287
307
|
|
|
288
308
|
def _qualify_readers(self, tables: Sequence[str], principals: Sequence[str]) -> None:
|
|
289
|
-
"""
|
|
309
|
+
"""Every grantee of a table must be the operator, a runtime login or an administrator.
|
|
310
|
+
|
|
311
|
+
A principal the operator does not know gets no barrier and no access transition, so after
|
|
312
|
+
a cutover it keeps reading the retired source, or writing to it, and those writes are lost
|
|
313
|
+
in silence. An administrator is trusted, because this protocol does not revoke
|
|
314
|
+
administrative powers (docs/local-cutover.md): refusing one protects nothing. On
|
|
315
|
+
PostgreSQL that is a superuser, who appears in no ACL. On ClickHouse it is a user with a
|
|
316
|
+
direct global ACCESS MANAGEMENT, who can grant itself anything (:meth:`_administrators`).
|
|
317
|
+
A role is never an administrator here, and any other grant covering the table refuses,
|
|
318
|
+
whatever its kind, global ones included.
|
|
319
|
+
"""
|
|
290
320
|
if self.dialect == "postgres":
|
|
291
321
|
operator = str(self.rows("SELECT current_user")[0][0])
|
|
292
322
|
allowed = {*principals, operator}
|
|
@@ -300,21 +330,104 @@ class NativeOperator:
|
|
|
300
330
|
"WHERE c.oid=to_regclass(%s) AND a.grantee <> c.relowner",
|
|
301
331
|
[self.quote(table), self.quote(table)],
|
|
302
332
|
)
|
|
303
|
-
|
|
304
|
-
|
|
333
|
+
outsiders = sorted(str(row[0]) for row in grants if str(row[0]) not in allowed)
|
|
334
|
+
if outsiders:
|
|
335
|
+
raise MigrationRefused(
|
|
336
|
+
f"cutover table {table} has an undeclared runtime grantee: "
|
|
337
|
+
f"{', '.join(outsiders)} holds a grant on it and is neither this "
|
|
338
|
+
f"operator, a runtime login nor a superuser; declare it as a runtime "
|
|
339
|
+
f"login or revoke its grant for the operation"
|
|
340
|
+
)
|
|
305
341
|
else:
|
|
306
342
|
operator = str(self.rows("SELECT currentUser()")[0][0])
|
|
307
343
|
database = self.endpoint()[2]
|
|
308
|
-
allowed = {*principals, operator}
|
|
344
|
+
allowed = {*principals, operator, *self._administrators()}
|
|
309
345
|
for table in tables:
|
|
310
346
|
grants = self.rows(
|
|
311
|
-
"SELECT user_name,role_name FROM system.grants "
|
|
347
|
+
"SELECT user_name,role_name,access_type,database,table FROM system.grants "
|
|
312
348
|
"WHERE (database IS NULL OR database='' OR database={database:String}) "
|
|
313
349
|
"AND (table IS NULL OR table='' OR table={table:String})",
|
|
314
350
|
{"database": database, "table": table},
|
|
315
351
|
)
|
|
316
|
-
|
|
317
|
-
|
|
352
|
+
held: dict[str, set[str]] = {}
|
|
353
|
+
for user, role, access, granted_db, granted_table in grants:
|
|
354
|
+
if role is not None or user not in allowed:
|
|
355
|
+
who = f"role {role}" if role is not None else f"user {user}"
|
|
356
|
+
level = f"{granted_db or '*'}.{granted_table or '*'}"
|
|
357
|
+
held.setdefault(who, set()).add(f"{access} ON {level}")
|
|
358
|
+
if held:
|
|
359
|
+
named = "; ".join(
|
|
360
|
+
who + " (" + ", ".join(sorted(rights)[:3])
|
|
361
|
+
+ (", ..." if len(rights) > 3 else "") + ")"
|
|
362
|
+
for who, rights in sorted(held.items())
|
|
363
|
+
)
|
|
364
|
+
raise MigrationRefused(
|
|
365
|
+
f"cutover table {table} has an undeclared runtime grantee: {named}. A "
|
|
366
|
+
f"grantee must be this operator, a runtime login or a user with a direct "
|
|
367
|
+
f"global ACCESS MANAGEMENT; declare it as a runtime login or narrow its "
|
|
368
|
+
f"grants away from this database for the operation"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _administrators(self) -> set[str]:
|
|
372
|
+
"""The ClickHouse users holding the whole of ACCESS MANAGEMENT, directly and globally.
|
|
373
|
+
|
|
374
|
+
The group is the server's own, read from ``system.privileges``, and so is the way a grant
|
|
375
|
+
of it is listed (measured): on 24.8 as one row, ``ACCESS MANAGEMENT`` with no database or
|
|
376
|
+
table; from 26.5 a row per member, because seven of them are now granted per user name
|
|
377
|
+
(``ON *`` rather than ``ON *.*``) and the group is never shown whole; 26.9 adds a member,
|
|
378
|
+
``CREATE TOKEN``. Read only as the row of 24.8, no administrator was found on a current
|
|
379
|
+
server. So a member counts when the user holds it, a group above it (``ALL`` too), or
|
|
380
|
+
every privilege below it; and a partial revoke of anything in the group, or above it, at
|
|
381
|
+
any level, leaves no administrator.
|
|
382
|
+
"""
|
|
383
|
+
parents: dict[str, str | None] = {
|
|
384
|
+
str(privilege): None if group is None else str(group)
|
|
385
|
+
for privilege, group in self.rows(
|
|
386
|
+
"SELECT privilege, parent_group FROM system.privileges"
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
children: dict[str, list[str]] = {}
|
|
390
|
+
for privilege, parent in parents.items():
|
|
391
|
+
if parent is not None:
|
|
392
|
+
children.setdefault(parent, []).append(privilege)
|
|
393
|
+
group = "ACCESS MANAGEMENT"
|
|
394
|
+
within = {group}
|
|
395
|
+
pending = [group]
|
|
396
|
+
while pending:
|
|
397
|
+
for child in children.get(pending.pop(), []):
|
|
398
|
+
within.add(child)
|
|
399
|
+
pending.append(child)
|
|
400
|
+
above = parents.get(group)
|
|
401
|
+
while above is not None:
|
|
402
|
+
within.add(above)
|
|
403
|
+
above = parents.get(above)
|
|
404
|
+
|
|
405
|
+
held: dict[str, set[str]] = {}
|
|
406
|
+
revoked: set[str] = set()
|
|
407
|
+
for user, access, partial, database, table, column in self.rows(
|
|
408
|
+
"SELECT user_name, access_type, is_partial_revoke, database, table, column "
|
|
409
|
+
"FROM system.grants WHERE user_name IS NOT NULL"
|
|
410
|
+
):
|
|
411
|
+
if partial:
|
|
412
|
+
if str(access) in within:
|
|
413
|
+
revoked.add(str(user))
|
|
414
|
+
elif database is None and table is None and column is None:
|
|
415
|
+
held.setdefault(str(user), set()).add(str(access))
|
|
416
|
+
|
|
417
|
+
def covered(rights: set[str], privilege: str) -> bool:
|
|
418
|
+
node: str | None = privilege
|
|
419
|
+
while node is not None:
|
|
420
|
+
if node in rights:
|
|
421
|
+
return True
|
|
422
|
+
node = parents.get(node)
|
|
423
|
+
below = children.get(privilege, [])
|
|
424
|
+
return bool(below) and all(covered(rights, child) for child in below)
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
user
|
|
428
|
+
for user, rights in held.items()
|
|
429
|
+
if user not in revoked and covered(rights, group)
|
|
430
|
+
}
|
|
318
431
|
|
|
319
432
|
def access(self, tables: Sequence[TableIdentity], *, enabled: bool) -> None:
|
|
320
433
|
if not self.principals:
|
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
from ..errors import EngineError, MigrationRefused
|
|
11
11
|
from ..physical import POSTGRES_METHODS, index_method
|
|
12
12
|
from ..placement import PhysicalLayout
|
|
13
|
-
from ..schema import schema_statements
|
|
13
|
+
from ..schema import postgres_index_target, schema_statements
|
|
14
14
|
from ._operator import NativeOperator, TableIdentity
|
|
15
15
|
|
|
16
16
|
|
|
@@ -188,11 +188,11 @@ class NativeStaging:
|
|
|
188
188
|
columns = [str(column) for column in index["columns"]]
|
|
189
189
|
rows = self._index(name)
|
|
190
190
|
if not rows:
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
# The same rendering as a new table's indexes and an in-place build's: one
|
|
192
|
+
# function, so a staged copy's index has the collation the reads use.
|
|
193
193
|
self.native.command(
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
"CREATE INDEX "
|
|
195
|
+
+ postgres_index_target(index, table.name, layout.columns.get(entity, {}))
|
|
196
196
|
)
|
|
197
197
|
rows = self._index(name)
|
|
198
198
|
if len(rows) != 1:
|