mainsequence 4.1.19__tar.gz → 4.2.1__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 (127) hide show
  1. {mainsequence-4.1.19/mainsequence.egg-info → mainsequence-4.2.1}/PKG-INFO +2 -1
  2. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +15 -22
  3. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +34 -19
  4. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/migrations.py +109 -57
  5. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/metatables/core.py +3 -0
  6. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/metatables/migrations.py +2 -5
  7. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/__init__.py +4 -0
  8. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/build_operations.py +6 -5
  9. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/persist_managers.py +9 -7
  10. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/migrations.py +132 -77
  11. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/sqlalchemy_contracts.py +35 -0
  12. {mainsequence-4.1.19 → mainsequence-4.2.1/mainsequence.egg-info}/PKG-INFO +2 -1
  13. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence.egg-info/requires.txt +1 -0
  14. {mainsequence-4.1.19 → mainsequence-4.2.1}/pyproject.toml +2 -1
  15. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_build_operations_hashing.py +6 -23
  16. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_cli_migrations.py +244 -20
  17. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_filter_normalization.py +2 -0
  18. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_meta_table_migrations.py +192 -29
  19. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_meta_tables_client_models.py +37 -2
  20. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_meta_tables_sqlalchemy_contracts.py +39 -41
  21. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_run_configuration.py +27 -0
  22. {mainsequence-4.1.19 → mainsequence-4.2.1}/LICENSE +0 -0
  23. {mainsequence-4.1.19 → mainsequence-4.2.1}/README.md +0 -0
  24. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/AGENTS.md +0 -0
  25. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  26. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  27. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  28. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  29. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  30. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  31. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  32. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  33. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  34. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  35. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  36. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  37. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  38. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  39. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  40. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  41. {mainsequence-4.1.19 → mainsequence-4.2.1}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  42. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/__init__.py +0 -0
  43. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/__main__.py +0 -0
  44. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/bootstrap.py +0 -0
  45. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/__init__.py +0 -0
  46. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/api.py +0 -0
  47. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/browser_auth.py +0 -0
  48. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/cli.py +0 -0
  49. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/config.py +0 -0
  50. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/docker_utils.py +0 -0
  51. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/doctor.py +0 -0
  52. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/local_ops.py +0 -0
  53. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/model_filters.py +0 -0
  54. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/project_status.py +0 -0
  55. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/pydantic_cli.py +0 -0
  56. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/sdk_utils.py +0 -0
  57. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/ssh_utils.py +0 -0
  58. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/cli/ui.py +0 -0
  59. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/__init__.py +0 -0
  60. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/agent_runtime_models.py +0 -0
  61. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/base.py +0 -0
  62. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/client.py +0 -0
  63. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/__init__.py +0 -0
  64. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/app_component.py +0 -0
  65. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/connections.py +0 -0
  66. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/data_models.py +0 -0
  67. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/workspace.py +0 -0
  68. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  69. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/compute_validation.py +0 -0
  70. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  71. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  72. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  73. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  74. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/dtype_codec.py +0 -0
  75. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/exceptions.py +0 -0
  76. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/fastapi/__init__.py +0 -0
  77. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/fastapi/auth.py +0 -0
  78. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/metatables/__init__.py +0 -0
  79. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/models_foundry.py +0 -0
  80. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/models_helpers.py +0 -0
  81. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/models_user.py +0 -0
  82. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/client/utils.py +0 -0
  83. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/defaults.py +0 -0
  84. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/instrumentation/__init__.py +0 -0
  85. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/instrumentation/utils.py +0 -0
  86. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/logconf.py +0 -0
  87. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/__main__.py +0 -0
  88. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  89. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  90. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  91. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  92. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  93. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  94. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  95. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  96. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/future_registry.py +0 -0
  97. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/hashing.py +0 -0
  98. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  99. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence/runtime_flags.py +0 -0
  100. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence.egg-info/SOURCES.txt +0 -0
  101. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence.egg-info/dependency_links.txt +0 -0
  102. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence.egg-info/entry_points.txt +0 -0
  103. {mainsequence-4.1.19 → mainsequence-4.2.1}/mainsequence.egg-info/top_level.txt +0 -0
  104. {mainsequence-4.1.19 → mainsequence-4.2.1}/setup.cfg +0 -0
  105. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_auth_precedence.py +0 -0
  106. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_cli.py +0 -0
  107. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_cli_browser_auth.py +0 -0
  108. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_client.py +0 -0
  109. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_command_center_app_component_models.py +0 -0
  110. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_command_center_data_models.py +0 -0
  111. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_command_center_models.py +0 -0
  112. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  113. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_data_node_storage_dimension_queries.py +0 -0
  114. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_data_node_update_flow.py +0 -0
  115. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_dependency_extras.py +0 -0
  116. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_duckdb_interface_dimensions.py +0 -0
  117. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_logconf.py +0 -0
  118. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_secret_client_model.py +0 -0
  122. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_source_table_configuration.py +0 -0
  123. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_sqlite_interface_dimensions.py +0 -0
  124. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_update_runner_uid_runtime.py +0 -0
  125. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_update_statistics.py +0 -0
  126. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_update_uid_guards.py +0 -0
  127. {mainsequence-4.1.19 → mainsequence-4.2.1}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.1.19
3
+ Version: 4.2.1
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -63,6 +63,7 @@ Requires-Dist: opentelemetry-exporter-otlp
63
63
  Requires-Dist: opentelemetry-sdk
64
64
  Requires-Dist: pandas
65
65
  Requires-Dist: psutil
66
+ Requires-Dist: psycopg2>=2.9.12
66
67
  Requires-Dist: pydantic
67
68
  Requires-Dist: pytz
68
69
  Requires-Dist: pyyaml
@@ -182,20 +182,14 @@ class PricesTable(PlatformTimeIndexMetaData, Base):
182
182
  )
183
183
  ```
184
184
 
185
- Storage registration is inferred from the class metadata and active Main
186
- Sequence project/session. The DataNode constructor ensures the output
187
- `storage_table` is registered when needed. You may still call it explicitly
188
- during bootstrap when code needs the returned metadata object:
185
+ Storage registration is migration-first. Add the storage model to the
186
+ MetaTable migration provider and run `mainsequence migrations upgrade --provider
187
+ ... --to head`. Do not call `PricesTable.register()` directly and do not rely on
188
+ DataNode construction to register storage tables.
189
189
 
190
- ```python
191
- PricesTable.register()
192
- ```
193
-
194
- `PlatformTimeIndexMetaData.register()` is the storage lifecycle path.
195
- Treat them as idempotent get-or-create operations: the platform returns the
196
- registered metadata and UID, and the SDK records that metadata on the class. Do
197
- not manually attach an existing UID, reconstruct a generic `MetaTable`, or use
198
- manual bind helpers as an authoring step.
190
+ `PlatformTimeIndexMetaData.register()` remains SDK plumbing for the migration
191
+ workflow. Do not manually attach an existing UID, reconstruct a generic
192
+ `MetaTable`, or use manual bind helpers as an authoring step.
199
193
 
200
194
  ### 2. Keep DataNode As Update Logic
201
195
 
@@ -206,17 +200,16 @@ The DataNode constructor should accept:
206
200
  - optional `hash_namespace`
207
201
 
208
202
  The constructor `storage_table` is the output storage contract. Keep it out of
209
- `DataNodeConfiguration`. Do not pre-register this output storage class just to
210
- construct the node; `DataNode` and `PersistManager` call the SDK registration
211
- lifecycle automatically when the class is not yet bound.
203
+ `DataNodeConfiguration`. The storage class must already be registered by the
204
+ migration workflow before the node is constructed or run.
212
205
 
213
206
  If the DataNode needs to select another DataNode's storage table as a
214
207
  dependency, put that dependency storage reference in the config as
215
208
  `type[PlatformTimeIndexMetaData]`. Do not add an extra constructor argument for
216
209
  dependency storage tables. Config values of this type are hashed by the bound
217
210
  `TimeIndexMetaData.uid` from `StorageClass.__time_index_metadata__`. If the
218
- class is not yet bound, the config serializer calls `StorageClass.register()`
219
- before reading the UID.
211
+ class is not yet bound, config serialization must fail and tell the user to run
212
+ the migration workflow.
220
213
 
221
214
  Do not accept `test_node`. It has been removed. Use explicit
222
215
  `hash_namespace(...)` or `hash_namespace="..."`.
@@ -398,10 +391,10 @@ Do not ask users to name these foreign keys. `MetaTableForeignKey(...)` derives
398
391
  a stable contract name when `name` is omitted; `name=...` is only for deliberate
399
392
  overrides.
400
393
 
401
- Registration of the storage class follows the MetaTable lifecycle:
402
- `register()` recursively registers unresolved FK target model classes, uses the
403
- local process registry keyed by `storage_hash`, and writes the target
404
- `MetaTable.uid` into the FK contract.
394
+ Registration of the storage class follows the MetaTable migration lifecycle.
395
+ Migration tooling recursively resolves/registers unresolved FK target model
396
+ classes, uses the local process registry keyed by `storage_hash`, and writes the
397
+ target `MetaTable.uid` into the FK contract.
405
398
 
406
399
  Do not add DataNode configuration fields just to mutate storage metadata.
407
400
 
@@ -161,15 +161,18 @@ class Account(PlatformManagedMetaTable, Base):
161
161
  },
162
162
  )
163
163
 
164
-
165
- account_meta_table = Account.register()
166
164
  ```
167
165
 
168
- Registration metadata belongs on the class. Do not pass description, labels,
169
- provisioning, data-source UID, hash namespace, time-index fields, or storage
170
- layout into `register()`.
166
+ Registration metadata belongs on the class. Do not call `Account.register()`
167
+ directly for platform-managed models. Add platform-managed models to the
168
+ selected `AlembicMetaTableMigration.metatable_models` list and let
169
+ `mainsequence migrations upgrade --provider ... --to head` resolve/register and
170
+ bind them.
171
171
 
172
- For platform-managed registration, the data source is resolved from the active Main Sequence project/session, the same way DataNode does. Do not require or thread a `data_source_uid` through normal platform-managed example code.
172
+ For platform-managed migration registration, the data source is resolved from
173
+ the active Main Sequence project/session, the same way DataNode does. Do not
174
+ require or thread a `data_source_uid` through normal platform-managed example
175
+ code.
173
176
 
174
177
  Only call `build_registration_request()` when the task explicitly needs to inspect or validate the payload before registration.
175
178
 
@@ -183,7 +186,8 @@ Do not add an environment variable for namespace in examples.
183
186
 
184
187
  Do not add generic labels such as `"meta-table"` or `"platform-managed"` to examples. Keep labels specific to the example or domain.
185
188
 
186
- Do not add a `MAINSEQUENCE_META_TABLE_REGISTER` toggle in registration examples. Registration examples should register directly.
189
+ Do not add a `MAINSEQUENCE_META_TABLE_REGISTER` toggle in platform-managed
190
+ examples. Platform-managed examples should be migration-first.
187
191
 
188
192
  ### 3. Register parent tables before child tables
189
193
 
@@ -192,8 +196,8 @@ Foreign-key contracts reference the target `MetaTable` UID.
192
196
  For `PlatformManagedMetaTable`, define foreign keys with
193
197
  `MetaTableForeignKey(TargetModel, column=...)`. Do not write raw SQLAlchemy
194
198
  table fullnames, `Parent.__table__.c.<column>` targets, or explicit target UID
195
- maps in the platform-managed path. Registration is the lifecycle path:
196
- `register()` recursively registers unresolved target model classes, stores each
199
+ maps in the platform-managed path. Migration is the lifecycle path. Migration
200
+ tooling resolves/registers unresolved target model classes, stores each
197
201
  returned `MetaTable` in a local process registry keyed by `storage_hash`, and
198
202
  uses the target `MetaTable.uid` in the child FK contract.
199
203
 
@@ -215,16 +219,19 @@ account_uid: Mapped[uuid.UUID] = mapped_column(
215
219
  Every participating table must include `__metatable_description__` describing
216
220
  both the schema and the table's intention.
217
221
 
218
- Example registration order:
222
+ Provider scope:
219
223
 
220
224
  ```python
221
- asset_meta_table = Asset.register()
225
+ migration = AlembicMetaTableMigration(
226
+ ...,
227
+ metatable_models=[Account, Asset],
228
+ )
222
229
  ```
223
230
 
224
- The child registration registers `Account` first if it has not already been
225
- registered in the current process. The local registry prevents duplicate backend
226
- registration attempts for the same `storage_hash` and raises a clear error for
227
- recursive registration cycles.
231
+ Migration tooling registers `Account` first if `Asset` depends on it and it has
232
+ not already been bound in the current process. The local registry prevents
233
+ duplicate backend registration attempts for the same `storage_hash` and raises
234
+ a clear error for recursive registration cycles.
228
235
 
229
236
  For `external_registered`, there is no platform-managed parent lookup. Register
230
237
  the parent first, then build the child registration request with
@@ -278,17 +285,25 @@ migration request models, or use SDK helper functions directly. The backend
278
285
  request shape is reference material in the tutorial; the user-facing path is:
279
286
 
280
287
  ```bash
281
- mainsequence migrations register-version-table --provider mainsequence_migrations:migration
282
- mainsequence migrations revision --provider mainsequence_migrations:migration --autogenerate -m "change"
288
+ mainsequence migrations current --provider mainsequence_migrations:migration
289
+ mainsequence migrations revision --provider mainsequence_migrations:migration
283
290
  mainsequence migrations render --provider mainsequence_migrations:migration --to head
284
291
  mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head --dry-run
285
292
  mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head
286
293
  ```
287
294
 
295
+ `current` and `upgrade` automatically register the provider's
296
+ `AlembicVersionMetaTable` binding when backend migration state is needed.
297
+ `revision` accepts optional `-m/--message`; if omitted, the CLI uses
298
+ `migration`. `revision --autogenerate` is optional and requires an explicit
299
+ `--sqlalchemy-url` for the baseline database.
300
+
288
301
  The SQL must be Alembic-rendered from the selected provider. After SQL apply
289
302
  succeeds, register or refresh only the application MetaTable catalog bindings
290
- listed in `migration.metatable_models`. A migration is not complete until both
291
- backend SQL execution and catalog sync succeed.
303
+ listed in `migration.metatable_models`. Do not pass the Alembic version-table
304
+ data source into those application model registrations; each model uses its
305
+ own normal MetaTable data-source binding. A migration is not complete until
306
+ both backend SQL execution and catalog sync succeed.
292
307
 
293
308
  Do not use SDK-managed migration artifact tables, artifact sync helpers, or custom
294
309
  `operations()` migration modules.
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
4
  import json
5
+ import re
6
+ from collections.abc import Callable, Mapping
5
7
  from typing import Any
6
8
 
7
9
  import click
@@ -66,26 +68,67 @@ def _registry_uid(migration: AlembicMetaTableMigration) -> str:
66
68
  uid = migration.alembic_registry.get_meta_table_uid()
67
69
  if uid in (None, ""):
68
70
  raise typer.BadParameter(
69
- "Alembic registry MetaTable UID is not bound. Run register-version-table "
70
- "or set __metatable_uid__ on the registry class.",
71
+ "Alembic registry MetaTable UID is not bound. Run current or upgrade "
72
+ "to auto-register the registry, or set __metatable_uid__ on the registry class.",
71
73
  param_hint="--provider",
72
74
  )
73
75
  return str(uid)
74
76
 
75
77
 
78
+ def _model_reference(model: type[Any]) -> str:
79
+ module = getattr(model, "__module__", None)
80
+ qualname = getattr(model, "__qualname__", None)
81
+ if module and qualname:
82
+ return f"{module}.{qualname}"
83
+ return repr(model)
84
+
85
+
86
+ def _meta_table_value(meta_table: Any, *names: str) -> Any:
87
+ if isinstance(meta_table, Mapping):
88
+ for name in names:
89
+ value = meta_table.get(name)
90
+ if value not in (None, ""):
91
+ return value
92
+ return None
93
+ for name in names:
94
+ value = getattr(meta_table, name, None)
95
+ if value not in (None, ""):
96
+ return value
97
+ return None
98
+
99
+
100
+ def _print_metatable_resolution_callback(
101
+ migration: AlembicMetaTableMigration,
102
+ ) -> Callable[[type[Any], str, str, Any | None], None]:
103
+ def _print(model: type[Any], identifier: str, status: str, meta_table: Any | None) -> None:
104
+ fields = [
105
+ f"identifier={identifier}",
106
+ f"model={_model_reference(model)}",
107
+ f"package={migration.package}",
108
+ f"migration_namespace={migration.migration_namespace}",
109
+ ]
110
+ uid = _meta_table_value(meta_table, "uid", "meta_table_uid")
111
+ if uid is not None:
112
+ fields.append(f"uid={uid}")
113
+ physical_table_name = _meta_table_value(meta_table, "physical_table_name")
114
+ if physical_table_name is not None:
115
+ fields.append(f"physical_table_name={physical_table_name}")
116
+ typer.echo(f"migration MetaTable {status}: " + " ".join(fields), err=True)
117
+
118
+ return _print
119
+
120
+
76
121
  def _ensure_registry(
77
122
  migration: AlembicMetaTableMigration,
78
123
  *,
79
- data_source_uid: str | None,
80
124
  timeout: float | None,
81
125
  ) -> None:
82
- migration.ensure_alembic_registry(data_source_uid=data_source_uid, timeout=timeout)
126
+ migration.ensure_alembic_registry(timeout=timeout)
83
127
 
84
128
 
85
129
  def _status_request(migration: AlembicMetaTableMigration) -> AlembicMigrationStatusRequest:
86
130
  return AlembicMigrationStatusRequest(
87
131
  alembic_version_meta_table_uid=_registry_uid(migration),
88
- data_source_uid=migration.resolve_data_source_uid(),
89
132
  package=migration.package,
90
133
  migration_namespace=migration.migration_namespace,
91
134
  )
@@ -117,7 +160,6 @@ def _operation(
117
160
  ) -> AlembicMigrationOperation:
118
161
  return AlembicMigrationOperation(
119
162
  alembic_version_meta_table_uid=_registry_uid(migration),
120
- data_source_uid=migration.resolve_data_source_uid(),
121
163
  package=migration.package,
122
164
  migration_namespace=migration.migration_namespace,
123
165
  revision=str(artifact.manifest["revision"]),
@@ -152,39 +194,35 @@ def _alembic_config(
152
194
  return config
153
195
 
154
196
 
155
- @migrations.command("register-version-table")
156
- def register_version_table(
157
- provider: str | None = typer.Option(
158
- None,
159
- "--provider",
160
- help="Migration provider reference, for example msm.migrations:migration.",
161
- ),
162
- data_source_uid: str | None = typer.Option(
163
- None,
164
- "--data-source-uid",
165
- help="Explicit override for cross-data-source workflows.",
166
- ),
167
- timeout: float | None = typer.Option(None, "--timeout"),
168
- json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
169
- ) -> None:
170
- """Register the provider's Alembic version table as an external MetaTable."""
197
+ def _next_sequential_revision_id(migration: AlembicMetaTableMigration) -> str:
198
+ try:
199
+ from alembic.script import ScriptDirectory
200
+ except ImportError as exc:
201
+ raise RuntimeError("Alembic is required for revision generation.") from exc
171
202
 
172
- migration = _load_migration(provider)
173
- meta_table = migration.register_alembic_registry(
174
- data_source_uid=data_source_uid,
175
- timeout=timeout,
176
- )
177
- _emit(
178
- {
179
- "uid": migration.alembic_registry.get_meta_table_uid()
180
- or getattr(meta_table, "uid", None),
181
- "data_source_uid": migration.alembic_registry.get_data_source_uid(),
182
- "alembic_version_table": migration.alembic_version_table,
183
- "package": migration.package,
184
- "migration_namespace": migration.migration_namespace,
185
- },
186
- json_output=json_output,
203
+ script = ScriptDirectory.from_config(
204
+ _alembic_config(migration, sqlalchemy_url="postgresql://")
187
205
  )
206
+ heads = list(script.get_heads())
207
+ if len(heads) > 1:
208
+ raise typer.BadParameter(
209
+ "Sequential revision IDs require a single Alembic head. Pass --rev-id "
210
+ "explicitly for branched histories.",
211
+ param_hint="--rev-id",
212
+ )
213
+ if heads and not re.fullmatch(r"\d{4,}", str(heads[0])):
214
+ raise typer.BadParameter(
215
+ "Sequential revision IDs require the current Alembic head to be numeric. "
216
+ "Pass --rev-id explicitly for non-numeric histories.",
217
+ param_hint="--rev-id",
218
+ )
219
+
220
+ numeric_revisions: list[int] = []
221
+ for revision in script.walk_revisions():
222
+ revision_id = str(revision.revision)
223
+ if re.fullmatch(r"\d{4,}", revision_id):
224
+ numeric_revisions.append(int(revision_id))
225
+ return f"{max(numeric_revisions, default=0) + 1:04d}"
188
226
 
189
227
 
190
228
  @migrations.command("current")
@@ -194,26 +232,30 @@ def current(
194
232
  "--provider",
195
233
  help="Migration provider reference, for example msm.migrations:migration.",
196
234
  ),
197
- data_source_uid: str | None = typer.Option(
198
- None,
199
- "--data-source-uid",
200
- help="Explicit override for cross-data-source workflows.",
201
- ),
202
235
  timeout: float | None = typer.Option(None, "--timeout"),
203
236
  json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
204
237
  ) -> None:
205
238
  """Read current Alembic revision through the provider's registry MetaTable."""
206
239
 
207
240
  migration = _load_migration(provider)
208
- _ensure_registry(migration, data_source_uid=data_source_uid, timeout=timeout)
241
+ _ensure_registry(migration, timeout=timeout)
209
242
  status = MetaTable.get_migration_status(_status_request(migration), timeout=timeout)
210
243
  _emit(status, json_output=json_output)
211
244
 
212
245
 
213
246
  @migrations.command("revision")
214
247
  def revision(
215
- message: str = typer.Option(..., "--message", "-m", help="Alembic revision message."),
216
- autogenerate: bool = typer.Option(False, "--autogenerate", help="Use Alembic autogenerate."),
248
+ message: str | None = typer.Option(
249
+ None,
250
+ "--message",
251
+ "-m",
252
+ help="Alembic revision message. Defaults to 'migration'.",
253
+ ),
254
+ autogenerate: bool = typer.Option(
255
+ False,
256
+ "--autogenerate",
257
+ help="Use Alembic autogenerate. Requires --sqlalchemy-url.",
258
+ ),
217
259
  provider: str | None = typer.Option(
218
260
  None,
219
261
  "--provider",
@@ -221,10 +263,10 @@ def revision(
221
263
  ),
222
264
  rev_id: str | None = typer.Option(None, "--rev-id", help="Explicit Alembic revision id."),
223
265
  head: str = typer.Option("head", "--head", help="Alembic head to base the revision on."),
224
- sqlalchemy_url: str = typer.Option(
225
- "postgresql://",
266
+ sqlalchemy_url: str | None = typer.Option(
267
+ None,
226
268
  "--sqlalchemy-url",
227
- help="SQLAlchemy URL passed to the Alembic environment.",
269
+ help="SQLAlchemy URL passed to the Alembic environment. Required for --autogenerate.",
228
270
  ),
229
271
  json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
230
272
  ) -> None:
@@ -236,11 +278,19 @@ def revision(
236
278
  raise typer.BadParameter("Alembic is required for revision generation.") from exc
237
279
 
238
280
  migration = _load_migration(provider)
281
+ resolved_message = (message or "").strip() or "migration"
282
+ if autogenerate and not sqlalchemy_url:
283
+ raise typer.BadParameter(
284
+ "--sqlalchemy-url is required with --autogenerate because Alembic "
285
+ "must connect to a baseline database to compute the diff.",
286
+ param_hint="--sqlalchemy-url",
287
+ )
288
+ resolved_rev_id = rev_id or _next_sequential_revision_id(migration)
239
289
  script = command.revision(
240
- _alembic_config(migration, sqlalchemy_url=sqlalchemy_url),
241
- message=message,
290
+ _alembic_config(migration, sqlalchemy_url=sqlalchemy_url or "postgresql://"),
291
+ message=resolved_message,
242
292
  autogenerate=autogenerate,
243
- rev_id=rev_id,
293
+ rev_id=resolved_rev_id,
244
294
  head=head,
245
295
  )
246
296
  _emit(
@@ -284,6 +334,9 @@ def render(
284
334
  migration = _load_migration(provider)
285
335
  if direction not in {"upgrade", "downgrade"}:
286
336
  raise typer.BadParameter("direction must be 'upgrade' or 'downgrade'.")
337
+ migration.resolve_or_register_metatable_models(
338
+ on_metatable_resolution=_print_metatable_resolution_callback(migration),
339
+ )
287
340
  artifact = _render_artifact(
288
341
  migration,
289
342
  target_revision=target_revision,
@@ -305,11 +358,6 @@ def upgrade(
305
358
  "--provider",
306
359
  help="Migration provider reference, for example msm.migrations:migration.",
307
360
  ),
308
- data_source_uid: str | None = typer.Option(
309
- None,
310
- "--data-source-uid",
311
- help="Explicit override for cross-data-source workflows.",
312
- ),
313
361
  dry_run: bool = typer.Option(False, "--dry-run", help="Validate without executing SQL."),
314
362
  sqlalchemy_url: str = typer.Option(
315
363
  "postgresql://",
@@ -322,7 +370,11 @@ def upgrade(
322
370
  """Dry-run or apply an Alembic-rendered SQL artifact through the backend."""
323
371
 
324
372
  migration = _load_migration(provider)
325
- _ensure_registry(migration, data_source_uid=data_source_uid, timeout=timeout)
373
+ _ensure_registry(migration, timeout=timeout)
374
+ migration.resolve_or_register_metatable_models(
375
+ timeout=timeout,
376
+ on_metatable_resolution=_print_metatable_resolution_callback(migration),
377
+ )
326
378
  status = MetaTable.get_migration_status(_status_request(migration), timeout=timeout)
327
379
  current_revision = status.current_revision
328
380
  artifact = _render_artifact(
@@ -1653,12 +1653,15 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
1653
1653
  FILTERSET_FIELDS: ClassVar[dict[str, list[str]]] = {
1654
1654
  "uid": ["in", "exact"],
1655
1655
  "update_hash": ["exact"],
1656
+ "remote_table__uid": ["exact", "in"],
1656
1657
  "remote_table__data_source__uid": ["exact", "in"],
1657
1658
  "related_table__namespace": ["contains", "in", "isnull"],
1658
1659
  }
1659
1660
  FILTER_VALUE_NORMALIZERS: ClassVar[dict[str, str]] = {
1660
1661
  "uid": "uid",
1661
1662
  "uid__in": "uid",
1663
+ "remote_table__uid": "uid",
1664
+ "remote_table__uid__in": "uid",
1662
1665
  "remote_table__data_source__uid": "uid",
1663
1666
  "remote_table__data_source__uid__in": "uid",
1664
1667
  }
@@ -25,7 +25,6 @@ class AlembicMigrationError(BasePydanticModel):
25
25
  class AlembicMigrationOperation(BasePydanticModel):
26
26
  version: AlembicMigrationVersion = ALEMBIC_MIGRATION_V1
27
27
  alembic_version_meta_table_uid: str
28
- data_source_uid: str
29
28
  package: str = ""
30
29
  migration_namespace: str = ""
31
30
  revision: str
@@ -42,7 +41,6 @@ class AlembicMigrationOperation(BasePydanticModel):
42
41
 
43
42
  class AlembicMigrationStatusRequest(BasePydanticModel):
44
43
  alembic_version_meta_table_uid: str
45
- data_source_uid: str
46
44
  package: str = ""
47
45
  migration_namespace: str = ""
48
46
 
@@ -56,7 +54,7 @@ class AlembicMigrationApplyResponse(BasePydanticModel):
56
54
  dry_run: bool = False
57
55
  alembic_version_meta_table_uid: str
58
56
  alembic_version_table: str
59
- data_source_uid: str
57
+ data_source_uid: str | None = None
60
58
  package: str = ""
61
59
  migration_namespace: str = ""
62
60
  revision: str
@@ -76,7 +74,7 @@ class AlembicMigrationStatusResponse(BasePydanticModel):
76
74
  version: AlembicMigrationVersion = ALEMBIC_MIGRATION_V1
77
75
  alembic_version_meta_table_uid: str
78
76
  alembic_version_table: str
79
- data_source_uid: str
77
+ data_source_uid: str | None = None
80
78
  package: str = ""
81
79
  migration_namespace: str = ""
82
80
  current_revision: str | None = None
@@ -127,7 +125,6 @@ def _get_migration_status(
127
125
  "alembic_version_meta_table_uid",
128
126
  payload.alembic_version_meta_table_uid,
129
127
  )
130
- response_payload.setdefault("data_source_uid", payload.data_source_uid)
131
128
  response_payload.setdefault("package", payload.package)
132
129
  response_payload.setdefault("migration_namespace", payload.migration_namespace)
133
130
  return AlembicMigrationStatusResponse(**response_payload)
@@ -65,6 +65,10 @@ _LAZY_IMPORTS = {
65
65
  ".sqlalchemy_contracts",
66
66
  "platform_managed_registration_request_from_sqlalchemy_model",
67
67
  ),
68
+ "platform_managed_migration_registration_context": (
69
+ ".sqlalchemy_contracts",
70
+ "platform_managed_migration_registration_context",
71
+ ),
68
72
  "register_external_sqlalchemy_model": (
69
73
  ".sqlalchemy_contracts",
70
74
  "register_external_sqlalchemy_model",
@@ -95,14 +95,15 @@ def _(value: type[Any]) -> Any:
95
95
  time_index_metadata = value.get_time_index_metadata()
96
96
  uid = getattr(time_index_metadata, "uid", None)
97
97
  if uid in (None, ""):
98
- value.register()
99
- time_index_metadata = value.get_time_index_metadata()
100
- uid = getattr(time_index_metadata, "uid", None)
98
+ raise ValueError(
99
+ "PlatformTimeIndexMetaData config value is not registered. Run "
100
+ "`mainsequence migrations upgrade --provider <provider> --to head` "
101
+ "before using it in DataNode configuration."
102
+ )
101
103
 
102
104
  if uid in (None, ""):
103
105
  raise ValueError(
104
- "PlatformTimeIndexMetaData config value register() did not bind "
105
- "TimeIndexMetaData metadata before hashing."
106
+ "PlatformTimeIndexMetaData config value is missing TimeIndexMetaData metadata."
106
107
  )
107
108
  return {
108
109
  "__type__": "platform_time_index_metadata",
@@ -97,14 +97,16 @@ def ensure_registered_storage_table(
97
97
  )
98
98
 
99
99
  if storage_table.get_time_index_metadata() is None:
100
- if getattr(storage_table, "__table__", None) is None:
101
- raise ValueError(f"{context} storage_table must be registered before use.")
102
- storage_table.register()
100
+ raise ValueError(
101
+ f"{context} storage_table is not registered. Run "
102
+ "`mainsequence migrations upgrade --provider <provider> --to head` "
103
+ "before using this DataNode storage table."
104
+ )
103
105
 
104
106
  storage_metadata = storage_table.get_time_index_metadata()
105
107
  if storage_metadata is None:
106
108
  raise ValueError(
107
- f"{context} storage_table.register() did not bind TimeIndexMetaData metadata."
109
+ f"{context} storage_table is missing TimeIndexMetaData metadata."
108
110
  )
109
111
  if storage_table.get_meta_table_uid() in (None, ""):
110
112
  raise ValueError(f"{context} storage_table must provide a MetaTable UID.")
@@ -117,7 +119,7 @@ class BasePersistManager:
117
119
  UPDATE_CLASS: ClassVar[type[Any] | None] = None
118
120
  UPDATE_DETAILS_CLASS: ClassVar[type[Any] | None] = None
119
121
 
120
- UPDATE_GET_OR_NONE_DATASOURCE_LOOKUP: ClassVar[str] = "remote_table__data_source__uid"
122
+ UPDATE_GET_OR_NONE_STORAGE_LOOKUP: ClassVar[str] = "remote_table__uid"
121
123
  UPDATE_CREATE_STORAGE_LOOKUP: ClassVar[str] = "meta_table_uid"
122
124
  TIME_INDEXED_PROFILE_ATTR: ClassVar[str] = "time_indexed_profile"
123
125
 
@@ -180,7 +182,7 @@ class BasePersistManager:
180
182
  "update_hash": self.update_hash,
181
183
  "include_relations_detail": include_relations_detail,
182
184
  }
183
- kwargs[self.UPDATE_GET_OR_NONE_DATASOURCE_LOOKUP] = self.storage_table.get_data_source_uid()
185
+ kwargs[self.UPDATE_GET_OR_NONE_STORAGE_LOOKUP] = self.storage_table.get_meta_table_uid()
184
186
  return kwargs
185
187
 
186
188
  def _build_update_get_or_create_kwargs(
@@ -573,7 +575,7 @@ class APIPersistManager:
573
575
  class PersistManager(BasePersistManager):
574
576
  UPDATE_CLASS = DataNodeUpdate
575
577
  UPDATE_DETAILS_CLASS = DataNodeUpdateDetails
576
- UPDATE_GET_OR_NONE_DATASOURCE_LOOKUP = "remote_table__data_source__uid"
578
+ UPDATE_GET_OR_NONE_STORAGE_LOOKUP = "remote_table__uid"
577
579
  UPDATE_CREATE_STORAGE_LOOKUP = "meta_table_uid"
578
580
 
579
581
  @classmethod