smart-data-engine-sdk 0.1.1__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.
Files changed (211) hide show
  1. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/PKG-INFO +1 -1
  2. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/__init__.py +8 -1
  3. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/cutover.py +2 -0
  4. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_index_build.py +58 -14
  5. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_operator.py +123 -10
  6. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_staging.py +5 -5
  7. smart_data_engine_sdk-0.2.0/src/sde/engines/orderbook.py +1207 -0
  8. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/postgres.py +74 -3
  9. smart_data_engine_sdk-0.2.0/src/sde/facts.py +94 -0
  10. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/generation.py +44 -3
  11. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/index_build.py +11 -1
  12. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/index_operator.py +13 -4
  13. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/inspection.py +3 -2
  14. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/layout.py +63 -12
  15. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/local_cutover.py +11 -3
  16. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/logging.py +6 -0
  17. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/physical.py +33 -3
  18. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/placement.py +40 -3
  19. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/provisioning.py +15 -3
  20. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/query.py +36 -3
  21. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/schema.py +41 -9
  22. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/session.py +92 -5
  23. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/staging.py +15 -1
  24. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/staging_operator.py +6 -2
  25. smart_data_engine_sdk-0.2.0/tests/_orderbook_live.py +62 -0
  26. smart_data_engine_sdk-0.2.0/tests/_unfenced_scenario.py +287 -0
  27. smart_data_engine_sdk-0.2.0/tests/orderbook_peer.py +116 -0
  28. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_slice.py +9 -2
  29. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_compatibility_views.py +1 -1
  30. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_conformance.py +57 -5
  31. smart_data_engine_sdk-0.2.0/tests/test_engine_facts.py +124 -0
  32. smart_data_engine_sdk-0.2.0/tests/test_engine_facts_live.py +64 -0
  33. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_implementer_documents.py +46 -0
  34. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_index_operator_live.py +95 -13
  35. smart_data_engine_sdk-0.2.0/tests/test_index_use_live.py +375 -0
  36. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_native_live.py +107 -0
  37. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_orderbook_adapter.py +3 -1
  38. smart_data_engine_sdk-0.2.0/tests/test_orderbook_session_slice.py +347 -0
  39. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_orderbook_shape.py +79 -9
  40. smart_data_engine_sdk-0.2.0/tests/test_orderbook_slice.py +334 -0
  41. smart_data_engine_sdk-0.2.0/tests/test_orderbook_tcp.py +800 -0
  42. smart_data_engine_sdk-0.2.0/tests/test_orderbook_three_engines.py +30 -0
  43. smart_data_engine_sdk-0.2.0/tests/test_orderbook_trading_example.py +182 -0
  44. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_physical.py +36 -1
  45. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_readme_claims.py +4 -1
  46. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_runtime_privileges_live.py +65 -1
  47. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_session_hashed.py +1 -1
  48. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_operator_live.py +60 -2
  49. smart_data_engine_sdk-0.2.0/tests/test_unfenced.py +152 -0
  50. smart_data_engine_sdk-0.2.0/tests/test_unfenced_operator_live.py +24 -0
  51. smart_data_engine_sdk-0.1.1/src/sde/engines/orderbook.py +0 -457
  52. smart_data_engine_sdk-0.1.1/tests/test_orderbook_slice.py +0 -272
  53. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/.gitignore +0 -0
  54. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/LICENSE +0 -0
  55. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/NOTICE +0 -0
  56. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/README.md +0 -0
  57. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/pyproject.toml +0 -0
  58. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/_cutover_project.py +0 -0
  59. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/_local_state.py +0 -0
  60. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/_operator_deadline.py +0 -0
  61. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/_usage.py +0 -0
  62. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/bulk.py +0 -0
  63. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/canonical.py +0 -0
  64. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/capabilities.py +0 -0
  65. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/__init__.py +0 -0
  66. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_clickhouse_connection.py +0 -0
  67. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_storage.py +0 -0
  68. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/_write_fences.py +0 -0
  69. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/engines/clickhouse.py +0 -0
  70. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/entity.py +0 -0
  71. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/errors.py +0 -0
  72. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/explain.py +0 -0
  73. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/frozen_verification.py +0 -0
  74. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/groups.py +0 -0
  75. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/hashing.py +0 -0
  76. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/infer.py +0 -0
  77. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/internal.py +0 -0
  78. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/migration.py +0 -0
  79. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/model.py +0 -0
  80. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/py.typed +0 -0
  81. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/routing.py +0 -0
  82. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/shapes.py +0 -0
  83. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/telemetry.py +0 -0
  84. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/__init__.py +0 -0
  85. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/loader.py +0 -0
  86. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/testing/memory.py +0 -0
  87. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/types.py +0 -0
  88. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/verification.py +0 -0
  89. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/watermark.py +0 -0
  90. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde/write_fence.py +0 -0
  91. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/__init__.py +0 -0
  92. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/__main__.py +0 -0
  93. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/diagnostics.py +0 -0
  94. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/model.py +0 -0
  95. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/project.py +0 -0
  96. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/py.typed +0 -0
  97. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/query_count.py +0 -0
  98. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/resources.py +0 -0
  99. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/runtime.py +0 -0
  100. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_demo/verification.py +0 -0
  101. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_operator/__init__.py +0 -0
  102. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/src/sde_operator/__main__.py +0 -0
  103. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_claims.py +0 -0
  104. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_cutover_driver.py +0 -0
  105. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_cutover_worker.py +0 -0
  106. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_generation.py +0 -0
  107. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_index_worker.py +0 -0
  108. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_staging_driver.py +0 -0
  109. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_staging_worker.py +0 -0
  110. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_weather_fixture.py +0 -0
  111. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/_write_fence.py +0 -0
  112. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/bulk_vectors.py +0 -0
  113. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/datetime_query_peer.py +0 -0
  114. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/fence_peer.py +0 -0
  115. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/__init__.py +0 -0
  116. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/metrics.py +0 -0
  117. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operation_metrics.py +0 -0
  118. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operation_worker.py +0 -0
  119. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operations.py +0 -0
  120. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/operator_worker.py +0 -0
  121. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/packets.py +0 -0
  122. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/project.py +0 -0
  123. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/run.py +0 -0
  124. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/transition.py +0 -0
  125. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/qualification/weather_worker.py +0 -0
  126. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_bulk.py +0 -0
  127. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_bulk_live.py +0 -0
  128. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_canonical.py +0 -0
  129. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_datetime_queries.py +0 -0
  130. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_tls.py +0 -0
  131. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_clickhouse_transport.py +0 -0
  132. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_copy_freshness.py +0 -0
  133. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_cutover_plan.py +0 -0
  134. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_cutover_project.py +0 -0
  135. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_query_count.py +0 -0
  136. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_resources.py +0 -0
  137. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_starter.py +0 -0
  138. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_starter_live.py +0 -0
  139. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_demo_verification.py +0 -0
  140. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_denormalized_layout.py +0 -0
  141. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_dual_write.py +0 -0
  142. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_engine_agreement.py +0 -0
  143. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_enrollment_durability.py +0 -0
  144. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_explain_live.py +0 -0
  145. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_failure_semantics.py +0 -0
  146. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_frozen_verification_live.py +0 -0
  147. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_map.py +0 -0
  148. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_migration_live.py +0 -0
  149. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_generation_session_live.py +0 -0
  150. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_groups_and_shapes.py +0 -0
  151. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_hashing.py +0 -0
  152. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_index_change_live.py +0 -0
  153. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_index_plan.py +0 -0
  154. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_infer.py +0 -0
  155. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_integration_guard.py +0 -0
  156. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_internal_failures.py +0 -0
  157. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_layout_dialects.py +0 -0
  158. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_local_cutover_live.py +0 -0
  159. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_map_unicode.py +0 -0
  160. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_map_unicode_live.py +0 -0
  161. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_measure_storage.py +0 -0
  162. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_measure_storage_live.py +0 -0
  163. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_migration.py +0 -0
  164. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_migration_live.py +0 -0
  165. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_model.py +0 -0
  166. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_neutral_declaration.py +0 -0
  167. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_no_account.py +0 -0
  168. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_no_account_live.py +0 -0
  169. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_no_expiry.py +0 -0
  170. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_no_retry.py +0 -0
  171. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_operation_metrics.py +0 -0
  172. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_operation_oracle.py +0 -0
  173. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_cli.py +0 -0
  174. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_operator_deadline.py +0 -0
  175. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_overhead.py +0 -0
  176. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_owned_session.py +0 -0
  177. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_owned_session_live.py +0 -0
  178. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_packaging.py +0 -0
  179. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_physical_live.py +0 -0
  180. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_placement_and_routing.py +0 -0
  181. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_platforms.py +0 -0
  182. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_postgres_slice.py +0 -0
  183. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_postgres_tls.py +0 -0
  184. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_cleanup.py +0 -0
  185. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_metrics.py +0 -0
  186. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_qualification_workers.py +0 -0
  187. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_query_live.py +0 -0
  188. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_query_plan.py +0 -0
  189. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_query_session.py +0 -0
  190. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_query_summary.py +0 -0
  191. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_query_vectors.py +0 -0
  192. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_release.py +0 -0
  193. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_rollback_protection.py +0 -0
  194. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_rollback_protection_live.py +0 -0
  195. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_schema_statements.py +0 -0
  196. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_schema_vectors_live.py +0 -0
  197. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_session.py +0 -0
  198. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_session_ownership_live.py +0 -0
  199. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_abandon_live.py +0 -0
  200. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_native_live.py +0 -0
  201. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_staging_plan.py +0 -0
  202. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_storage_sizes_live.py +0 -0
  203. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_telemetry.py +0 -0
  204. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_telemetry_storage_time.py +0 -0
  205. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_verification_request.py +0 -0
  206. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_write_fence.py +0 -0
  207. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/test_write_fence_live.py +0 -0
  208. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/timestamp_peer.py +0 -0
  209. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/tls_certificates.py +0 -0
  210. {smart_data_engine_sdk-0.1.1 → smart_data_engine_sdk-0.2.0}/tests/verification_peer.py +0 -0
  211. {smart_data_engine_sdk-0.1.1 → 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.1.1
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.1.1"
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]
@@ -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 as ``MATERIALIZE INDEX <name>`` with the name unquoted - measured, and relied on to find a
33
- materialization again instead of starting a second one."""
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
 
@@ -90,14 +91,21 @@ class NativeIndexBuild:
90
91
  ):
91
92
  time.sleep(POLL_SECONDS)
92
93
 
93
- def build(self, table: TableIdentity, index: Mapping[str, Any]) -> None:
94
- """Bring the index to ``ready`` without pausing the table's writers."""
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
+ """
95
103
  self.settle(table, index)
96
104
  status, reason = self.inspect(table, index)
97
105
  if status == "foreign":
98
106
  raise MigrationRefused(reason)
99
107
  if self.dialect == "postgres":
100
- self._pg_build(table, index, status)
108
+ self._pg_build(table, index, status, columns)
101
109
  else:
102
110
  self._ch_build(table, index, status)
103
111
  if self.status(table, index) != "ready":
@@ -223,14 +231,45 @@ class NativeIndexBuild:
223
231
  )
224
232
  return ("ready" if usable else "unfinished"), ""
225
233
 
226
- def _pg_build(self, table: TableIdentity, index: Mapping[str, Any], status: Status) -> None:
227
- if status == "ready":
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):
228
242
  return
229
- if status == "unfinished":
230
- # Our own leftover of an interrupted build: never valid again by itself, and kept by
231
- # IF NOT EXISTS. Removed without blocking writers, then built again.
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.
232
249
  self.native.command(f"DROP INDEX CONCURRENTLY {self.quote(str(index['name']))}")
233
- self.native.command(f"CREATE INDEX CONCURRENTLY {postgres_index_target(index, table.name)}")
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
+ )
234
273
 
235
274
  # --- ClickHouse --------------------------------------------------------------------------
236
275
 
@@ -248,12 +287,17 @@ class NativeIndexBuild:
248
287
 
249
288
  def _ch_mutations(self, table: TableIdentity, name: str) -> list[tuple[str, bool, str]]:
250
289
  # KILL MUTATION removes the mutation from this table - measured - so what is listed here
251
- # 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}"
252
295
  rows = self.native.rows(
253
296
  "SELECT mutation_id, is_done, latest_fail_reason FROM system.mutations "
254
297
  "WHERE database = currentDatabase() AND table = {table:String} "
255
- "AND command = {command:String} AND NOT is_killed ORDER BY create_time",
256
- {"table": table.name, "command": f"MATERIALIZE INDEX {name}"},
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})"},
257
301
  )
258
302
  return [(str(mutation_id), bool(done), str(failure)) for mutation_id, done, failure in rows]
259
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, tables: Sequence[str], allowed_tables: Sequence[str], *, required_access: bool = True
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
- self._qualify_readers(tables, principals)
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
- """A missing runtime principal would keep reading the retired authority after cutover."""
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
- if any(str(row[0]) not in allowed for row in grants):
304
- raise MigrationRefused("cutover table has an undeclared runtime grantee")
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
- if any(role is not None or user not in allowed for user, role in grants):
317
- raise MigrationRefused("cutover table has an undeclared runtime grantee")
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
- column_sql = ", ".join(self.quote(column) for column in columns)
192
- using = "" if declared_method == "btree" else f"USING {declared_method} "
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
- f"CREATE INDEX {self.quote(name)} ON {self.quote(table.name)} "
195
- f"{using}({column_sql})"
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: