mainsequence 4.1.18__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 (128) hide show
  1. {mainsequence-4.1.18/mainsequence.egg-info → mainsequence-4.2.1}/PKG-INFO +2 -1
  2. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +15 -22
  3. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +44 -20
  4. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/migrations.py +122 -71
  5. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/metatables/core.py +31 -4
  6. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/metatables/migrations.py +2 -5
  7. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/__init__.py +8 -0
  8. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/build_operations.py +6 -5
  9. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/persist_managers.py +9 -7
  10. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/migrations.py +149 -61
  11. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/sqlalchemy_contracts.py +111 -2
  12. {mainsequence-4.1.18 → mainsequence-4.2.1/mainsequence.egg-info}/PKG-INFO +2 -1
  13. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence.egg-info/requires.txt +1 -0
  14. {mainsequence-4.1.18 → mainsequence-4.2.1}/pyproject.toml +2 -1
  15. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_build_operations_hashing.py +6 -23
  16. mainsequence-4.2.1/tests/test_cli_migrations.py +644 -0
  17. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_filter_normalization.py +2 -0
  18. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_meta_table_migrations.py +256 -26
  19. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_meta_tables_client_models.py +56 -2
  20. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_meta_tables_sqlalchemy_contracts.py +97 -41
  21. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_run_configuration.py +27 -0
  22. mainsequence-4.1.18/tests/test_cli_migrations.py +0 -338
  23. {mainsequence-4.1.18 → mainsequence-4.2.1}/LICENSE +0 -0
  24. {mainsequence-4.1.18 → mainsequence-4.2.1}/README.md +0 -0
  25. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/AGENTS.md +0 -0
  26. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  27. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  28. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  29. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  30. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  31. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  32. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  33. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  34. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  35. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  36. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  37. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  38. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  39. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  40. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  41. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  42. {mainsequence-4.1.18 → mainsequence-4.2.1}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  43. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/__init__.py +0 -0
  44. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/__main__.py +0 -0
  45. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/bootstrap.py +0 -0
  46. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/__init__.py +0 -0
  47. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/api.py +0 -0
  48. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/browser_auth.py +0 -0
  49. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/cli.py +0 -0
  50. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/config.py +0 -0
  51. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/docker_utils.py +0 -0
  52. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/doctor.py +0 -0
  53. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/local_ops.py +0 -0
  54. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/model_filters.py +0 -0
  55. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/project_status.py +0 -0
  56. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/pydantic_cli.py +0 -0
  57. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/sdk_utils.py +0 -0
  58. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/ssh_utils.py +0 -0
  59. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/cli/ui.py +0 -0
  60. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/__init__.py +0 -0
  61. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/agent_runtime_models.py +0 -0
  62. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/base.py +0 -0
  63. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/client.py +0 -0
  64. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/__init__.py +0 -0
  65. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/app_component.py +0 -0
  66. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/connections.py +0 -0
  67. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/data_models.py +0 -0
  68. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/workspace.py +0 -0
  69. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  70. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/compute_validation.py +0 -0
  71. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  72. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  73. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  74. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  75. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/dtype_codec.py +0 -0
  76. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/exceptions.py +0 -0
  77. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/fastapi/__init__.py +0 -0
  78. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/fastapi/auth.py +0 -0
  79. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/metatables/__init__.py +0 -0
  80. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/models_foundry.py +0 -0
  81. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/models_helpers.py +0 -0
  82. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/models_user.py +0 -0
  83. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/client/utils.py +0 -0
  84. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/defaults.py +0 -0
  85. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/instrumentation/__init__.py +0 -0
  86. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/instrumentation/utils.py +0 -0
  87. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/logconf.py +0 -0
  88. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/__main__.py +0 -0
  89. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  90. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  91. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  92. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  93. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  94. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  95. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  96. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  97. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/future_registry.py +0 -0
  98. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/hashing.py +0 -0
  99. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  100. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence/runtime_flags.py +0 -0
  101. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence.egg-info/SOURCES.txt +0 -0
  102. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence.egg-info/dependency_links.txt +0 -0
  103. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence.egg-info/entry_points.txt +0 -0
  104. {mainsequence-4.1.18 → mainsequence-4.2.1}/mainsequence.egg-info/top_level.txt +0 -0
  105. {mainsequence-4.1.18 → mainsequence-4.2.1}/setup.cfg +0 -0
  106. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_auth_precedence.py +0 -0
  107. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_cli.py +0 -0
  108. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_cli_browser_auth.py +0 -0
  109. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_client.py +0 -0
  110. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_command_center_app_component_models.py +0 -0
  111. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_command_center_data_models.py +0 -0
  112. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_command_center_models.py +0 -0
  113. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  114. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_data_node_storage_dimension_queries.py +0 -0
  115. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_data_node_update_flow.py +0 -0
  116. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_dependency_extras.py +0 -0
  117. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_duckdb_interface_dimensions.py +0 -0
  118. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_logconf.py +0 -0
  119. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_models_user_request_bound_auth.py +0 -0
  120. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_pod_project_resolution.py +0 -0
  121. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_project_batch_jobs_from_file.py +0 -0
  122. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_secret_client_model.py +0 -0
  123. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_source_table_configuration.py +0 -0
  124. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_sqlite_interface_dimensions.py +0 -0
  125. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_update_runner_uid_runtime.py +0 -0
  126. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_update_statistics.py +0 -0
  127. {mainsequence-4.1.18 → mainsequence-4.2.1}/tests/test_update_uid_guards.py +0 -0
  128. {mainsequence-4.1.18 → 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.18
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
@@ -260,27 +267,43 @@ For contract evolution, define or update one selected
260
267
  - set `package`, `migration_namespace`, `script_location`, and `target_metadata`
261
268
  - set `alembic_registry` to an `AlembicVersionMetaTable` subclass
262
269
  - list the post-apply catalog scope in `metatable_models`
263
- - generate, render, dry-run, apply, and optionally refresh catalog bindings
270
+ - generate, render, dry-run, apply, and refresh catalog bindings
264
271
  through `mainsequence migrations ...` commands
265
272
 
266
273
  `alembic_version_meta_table_uid` is the UID of the catalog binding for Alembic's
267
274
  version table. It is not the UID of the table being migrated.
268
275
 
276
+ Application MetaTable catalog sync resolves existing rows by exact
277
+ `identifier`. If a model declares `__metatable_identifier__`, that value is the
278
+ global identity. If it does not, the SDK derives the identifier from
279
+ `[project].name` in `pyproject.toml` plus
280
+ `<model.__module__>.<model.__qualname__>`. Pin an explicit identifier when a
281
+ class is renamed or moved but must keep the same platform identity.
282
+
269
283
  Do not ask users to construct backend migration payloads, call low-level
270
284
  migration request models, or use SDK helper functions directly. The backend
271
285
  request shape is reference material in the tutorial; the user-facing path is:
272
286
 
273
287
  ```bash
274
- mainsequence migrations register-version-table --provider mainsequence_migrations:migration
275
- 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
276
290
  mainsequence migrations render --provider mainsequence_migrations:migration --to head
277
291
  mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head --dry-run
278
- mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head --apply --register-metatables
292
+ mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head
279
293
  ```
280
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
+
281
301
  The SQL must be Alembic-rendered from the selected provider. After SQL apply
282
302
  succeeds, register or refresh only the application MetaTable catalog bindings
283
- listed in `migration.metatable_models`.
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.
284
307
 
285
308
  Do not use SDK-managed migration artifact tables, artifact sync helpers, or custom
286
309
  `operations()` migration modules.
@@ -336,6 +359,7 @@ Do not claim success until you have checked:
336
359
  - migrations are scoped by an `AlembicMetaTableMigration` provider
337
360
  - the provider's Alembic version-table binding is registered before apply/current
338
361
  - post-apply catalog registration is scoped to `migration.metatable_models`
362
+ - catalog sync resolves application MetaTables by exact `identifier`
339
363
  - user-facing migration instructions stay on the documented CLI/provider lifecycle
340
364
 
341
365
  For related tables, also check:
@@ -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,18 +358,7 @@ 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
- apply: bool = typer.Option(False, "--apply", help="Apply after validating dry-run."),
314
361
  dry_run: bool = typer.Option(False, "--dry-run", help="Validate without executing SQL."),
315
- register_metatables: bool = typer.Option(
316
- False,
317
- "--register-metatables",
318
- help="Register provider MetaTable models after a successful apply.",
319
- ),
320
362
  sqlalchemy_url: str = typer.Option(
321
363
  "postgresql://",
322
364
  "--sqlalchemy-url",
@@ -327,11 +369,12 @@ def upgrade(
327
369
  ) -> None:
328
370
  """Dry-run or apply an Alembic-rendered SQL artifact through the backend."""
329
371
 
330
- if apply and dry_run:
331
- raise typer.BadParameter("Use either --apply or --dry-run, not both.")
332
-
333
372
  migration = _load_migration(provider)
334
- _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
+ )
335
378
  status = MetaTable.get_migration_status(_status_request(migration), timeout=timeout)
336
379
  current_revision = status.current_revision
337
380
  artifact = _render_artifact(
@@ -349,7 +392,7 @@ def upgrade(
349
392
  dry_run=True,
350
393
  )
351
394
  validation = MetaTable.apply_migration(validation_operation, timeout=timeout)
352
- if not apply:
395
+ if dry_run:
353
396
  _emit(validation, json_output=json_output)
354
397
  return
355
398
  if not validation.ok:
@@ -358,13 +401,21 @@ def upgrade(
358
401
 
359
402
  apply_operation = validation_operation.model_copy(update={"dry_run": False})
360
403
  result = MetaTable.apply_migration(apply_operation, timeout=timeout)
361
- registered = []
362
- if register_metatables and result.ok:
363
- registered = migration.register_metatables(timeout=timeout)
404
+ if not result.ok:
405
+ _emit(result, json_output=json_output)
406
+ raise typer.Exit(code=1)
407
+ applied_status = MetaTable.get_migration_status(_status_request(migration), timeout=timeout)
408
+ registered = migration.sync_metatable_catalog(timeout=timeout)
364
409
 
365
410
  if json_output or _json_output_enabled():
366
411
  _emit(
367
- {"validation": validation, "apply": result, "registered": registered}, json_output=True
412
+ {
413
+ "validation": validation,
414
+ "apply": result,
415
+ "status": applied_status,
416
+ "registered": registered,
417
+ },
418
+ json_output=True,
368
419
  )
369
420
  return
370
421
  _emit(result, json_output=False)
@@ -453,7 +453,13 @@ class MetaTableRegistrationRequest(BasePydanticModel):
453
453
  management_mode: MetaTableManagementMode
454
454
  storage_hash: str = Field(..., max_length=63, description="Canonical table storage hash.")
455
455
  table_contract: MetaTableContract | dict[str, Any]
456
- identifier: str | None = None
456
+ identifier: str | None = Field(
457
+ default=None,
458
+ description=(
459
+ "Optional logical MetaTable identifier. Non-empty values are globally "
460
+ "unique per organization and are used to resolve migrated MetaTables."
461
+ ),
462
+ )
457
463
  namespace: str | None = None
458
464
  description: str | None = None
459
465
  protect_from_deletion: bool = False
@@ -869,7 +875,13 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
869
875
  data_source: int | DynamicTableDataSource | dict[str, Any] | None = None
870
876
  data_source_uid: str | None = None
871
877
  storage_hash: str = Field(..., max_length=63, description="Canonical table storage hash.")
872
- identifier: str | None = None
878
+ identifier: str | None = Field(
879
+ default=None,
880
+ description=(
881
+ "Optional logical MetaTable identifier. Non-empty values are globally "
882
+ "unique per organization and are used to resolve migrated MetaTables."
883
+ ),
884
+ )
873
885
  namespace: str | None = None
874
886
  description: str | None = None
875
887
  labels: list[str] = Field(default_factory=list)
@@ -1387,7 +1399,13 @@ class TimeIndexMetaTableRegistrationRequest(BasePydanticModel):
1387
1399
  max_length=63,
1388
1400
  description="Canonical logical storage identity for the time-indexed MetaTable",
1389
1401
  )
1390
- identifier: str | None = Field(None, description="Optional published storage identifier")
1402
+ identifier: str | None = Field(
1403
+ None,
1404
+ description=(
1405
+ "Optional published storage identifier. Non-empty values are globally "
1406
+ "unique per organization."
1407
+ ),
1408
+ )
1391
1409
  namespace: str | None = Field(None, description="Optional published storage namespace")
1392
1410
  description: str | None = Field(None, description="Optional storage description")
1393
1411
  labels: list[str] = Field(default_factory=list)
@@ -1635,12 +1653,15 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
1635
1653
  FILTERSET_FIELDS: ClassVar[dict[str, list[str]]] = {
1636
1654
  "uid": ["in", "exact"],
1637
1655
  "update_hash": ["exact"],
1656
+ "remote_table__uid": ["exact", "in"],
1638
1657
  "remote_table__data_source__uid": ["exact", "in"],
1639
1658
  "related_table__namespace": ["contains", "in", "isnull"],
1640
1659
  }
1641
1660
  FILTER_VALUE_NORMALIZERS: ClassVar[dict[str, str]] = {
1642
1661
  "uid": "uid",
1643
1662
  "uid__in": "uid",
1663
+ "remote_table__uid": "uid",
1664
+ "remote_table__uid__in": "uid",
1644
1665
  "remote_table__data_source__uid": "uid",
1645
1666
  "remote_table__data_source__uid__in": "uid",
1646
1667
  }
@@ -2405,7 +2426,13 @@ class DataNodeUpdateDetails(BaseUpdateDetails, BasePydanticModel, BaseObjectOrm)
2405
2426
 
2406
2427
 
2407
2428
  class TableMetaData(BaseModel):
2408
- identifier: str = None
2429
+ identifier: str | None = Field(
2430
+ default=None,
2431
+ description=(
2432
+ "Optional logical MetaTable identifier. Non-empty values are globally "
2433
+ "unique per organization."
2434
+ ),
2435
+ )
2409
2436
  description: str | None = None
2410
2437
 
2411
2438
 
@@ -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,10 +65,18 @@ _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",
71
75
  ),
76
+ "resolve_metatable_identifier": (
77
+ ".sqlalchemy_contracts",
78
+ "resolve_metatable_identifier",
79
+ ),
72
80
  "render_packaged_alembic_migration": (
73
81
  ".migrations",
74
82
  "render_packaged_alembic_migration",