mainsequence 4.2.25__tar.gz → 4.2.38__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.2.25/mainsequence.egg-info → mainsequence-4.2.38}/PKG-INFO +1 -1
  2. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +20 -14
  3. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +52 -42
  4. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/cli.py +0 -3
  5. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/migrations.py +82 -25
  6. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/metatables/core.py +91 -206
  7. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/__init__.py +11 -6
  8. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/migrations.py +171 -421
  9. mainsequence-4.2.38/mainsequence/meta_tables/schema_names.py +304 -0
  10. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/sqlalchemy_contracts.py +238 -986
  11. {mainsequence-4.2.25 → mainsequence-4.2.38/mainsequence.egg-info}/PKG-INFO +1 -1
  12. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence.egg-info/SOURCES.txt +2 -0
  13. {mainsequence-4.2.25 → mainsequence-4.2.38}/pyproject.toml +1 -1
  14. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_cli.py +0 -3
  15. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_cli_migrations.py +171 -24
  16. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_meta_table_migrations.py +343 -245
  17. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_meta_tables_client_models.py +159 -219
  18. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_meta_tables_sqlalchemy_contracts.py +329 -315
  19. mainsequence-4.2.38/tests/test_schema_names.py +104 -0
  20. {mainsequence-4.2.25 → mainsequence-4.2.38}/LICENSE +0 -0
  21. {mainsequence-4.2.25 → mainsequence-4.2.38}/README.md +0 -0
  22. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/AGENTS.md +0 -0
  23. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  24. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  25. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  26. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  27. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  28. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  29. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  30. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  31. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  32. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  33. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  34. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  35. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  36. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  37. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  38. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  39. {mainsequence-4.2.25 → mainsequence-4.2.38}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  40. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/__init__.py +0 -0
  41. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/__main__.py +0 -0
  42. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/bootstrap.py +0 -0
  43. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/__init__.py +0 -0
  44. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/api.py +0 -0
  45. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/browser_auth.py +0 -0
  46. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/config.py +0 -0
  47. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/docker_utils.py +0 -0
  48. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/doctor.py +0 -0
  49. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/local_ops.py +0 -0
  50. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/model_filters.py +0 -0
  51. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/project_status.py +0 -0
  52. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/pydantic_cli.py +0 -0
  53. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/sdk_utils.py +0 -0
  54. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/ssh_utils.py +0 -0
  55. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/cli/ui.py +0 -0
  56. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/__init__.py +0 -0
  57. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/agent_runtime_models.py +0 -0
  58. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/base.py +0 -0
  59. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/client.py +0 -0
  60. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/__init__.py +0 -0
  61. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/app_component.py +0 -0
  62. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/connections.py +0 -0
  63. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/data_models.py +0 -0
  64. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/workspace.py +0 -0
  65. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  66. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/compute_validation.py +0 -0
  67. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  68. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  69. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  70. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  71. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/dtype_codec.py +0 -0
  72. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/exceptions.py +0 -0
  73. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/fastapi/__init__.py +0 -0
  74. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/fastapi/auth.py +0 -0
  75. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/metatables/__init__.py +0 -0
  76. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/models_foundry.py +0 -0
  77. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/models_helpers.py +0 -0
  78. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/models_user.py +0 -0
  79. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/client/utils.py +0 -0
  80. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/defaults.py +0 -0
  81. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/instrumentation/__init__.py +0 -0
  82. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/instrumentation/utils.py +0 -0
  83. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/logconf.py +0 -0
  84. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/__main__.py +0 -0
  85. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  86. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  87. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  88. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  89. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  90. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  91. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  92. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  93. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  94. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  95. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/future_registry.py +0 -0
  96. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/hashing.py +0 -0
  97. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  98. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence/runtime_flags.py +0 -0
  99. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence.egg-info/dependency_links.txt +0 -0
  100. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence.egg-info/entry_points.txt +0 -0
  101. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence.egg-info/requires.txt +0 -0
  102. {mainsequence-4.2.25 → mainsequence-4.2.38}/mainsequence.egg-info/top_level.txt +0 -0
  103. {mainsequence-4.2.25 → mainsequence-4.2.38}/setup.cfg +0 -0
  104. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_auth_precedence.py +0 -0
  105. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_build_operations_hashing.py +0 -0
  106. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_cli_browser_auth.py +0 -0
  107. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_client.py +0 -0
  108. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_command_center_app_component_models.py +0 -0
  109. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_command_center_data_models.py +0 -0
  110. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_command_center_models.py +0 -0
  111. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  112. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_data_node_storage_dimension_queries.py +0 -0
  113. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_data_node_update_flow.py +0 -0
  114. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_dependency_extras.py +0 -0
  115. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_duckdb_interface_dimensions.py +0 -0
  116. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_filter_normalization.py +0 -0
  117. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_logconf.py +0 -0
  118. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_run_configuration.py +0 -0
  122. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_secret_client_model.py +0 -0
  123. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_source_table_configuration.py +0 -0
  124. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_sqlite_interface_dimensions.py +0 -0
  125. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_update_runner_uid_runtime.py +0 -0
  126. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_update_statistics.py +0 -0
  127. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_update_uid_guards.py +0 -0
  128. {mainsequence-4.2.25 → mainsequence-4.2.38}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.2.25
3
+ Version: 4.2.38
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
@@ -139,7 +139,10 @@ import datetime
139
139
  from sqlalchemy import DateTime, Float, MetaData
140
140
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
141
141
 
142
- from mainsequence.meta_tables import PlatformTimeIndexMetaData
142
+ from mainsequence.meta_tables import PlatformTimeIndexMetaData, schema_table_name
143
+
144
+ PROJECT_NAME = "<project_name>"
145
+ PRICES_TABLE_NAME = schema_table_name(PROJECT_NAME, "prices")
143
146
 
144
147
 
145
148
  class Base(DeclarativeBase):
@@ -147,6 +150,7 @@ class Base(DeclarativeBase):
147
150
 
148
151
 
149
152
  class PricesTable(PlatformTimeIndexMetaData, Base):
153
+ __tablename__ = PRICES_TABLE_NAME
150
154
  __metatable_namespace__ = "<domain_namespace>"
151
155
  __metatable_identifier__ = "<project_name>.<table_identifier>"
152
156
  __metatable_extra_hash_components__ = {"storage_name": "<stable_storage_name>"}
@@ -378,23 +382,24 @@ changing it changes the dependency graph and update identity.
378
382
 
379
383
  Do not construct dependency graphs dynamically inside `update()`.
380
384
 
381
- ### 8. Foreign Keys Belong To The Storage Contract
385
+ ### 8. Foreign Keys Belong To SQLAlchemy And Alembic
382
386
 
383
387
  For new code, model foreign keys on the `PlatformTimeIndexMetaData` storage
384
- class, or route the storage-contract work to the MetaTable skill. When a
385
- DataNode storage table needs a platform-managed FK, use
386
- `MetaTableForeignKey(TargetModel, column=...)` on the storage class. Do not use
387
- `ForeignKey(Target.__table__.c.uid)`, table fullnames, or explicit target UID
388
- maps in DataNode examples.
388
+ class, or route the storage work to the MetaTable skill. When a DataNode storage
389
+ table needs a platform-managed FK, use normal SQLAlchemy `ForeignKey(...)` /
390
+ `ForeignKeyConstraint(...)` metadata on the storage class. Prefer
391
+ project-prefixed SQLAlchemy table names for explicit FK string targets so
392
+ projects sharing one schema do not collide. Generate those names with
393
+ `schema_table_name(project_or_app, concept)` from `mainsequence.meta_tables`.
389
394
 
390
- Do not ask users to name these foreign keys. Platform-managed
391
- `MetaTableForeignKey(...)` contracts store logical relationships only. Alembic,
392
- SQLAlchemy, and the database own physical FK constraint names.
395
+ Do not ask users to put FK target `MetaTable.uid` values into DataNode config or
396
+ MetaTable registration contracts. Alembic, SQLAlchemy, and the database own FK
397
+ DDL and physical FK constraint names.
393
398
 
394
399
  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.
400
+ The migration provider reserves the MetaTable rows, Alembic renders/applies FK
401
+ DDL from SQLAlchemy metadata, and catalog finalization refreshes the MetaTable
402
+ binding after upgrade.
398
403
 
399
404
  Do not add DataNode configuration fields just to mutate storage metadata.
400
405
 
@@ -404,7 +409,8 @@ Production-quality table identifiers, descriptions, labels, column docs, and
404
409
  foreign-key metadata belong to the storage class/MetaTable registration path.
405
410
  Prefix explicit table identifiers, explicit physical table names, and Alembic
406
411
  version table names with the project or package name rather than using bare
407
- names that can collide across projects.
412
+ names that can collide across projects. Use `schema_table_name(...)` for
413
+ authored SQLAlchemy table names, including DataNode storage tables.
408
414
 
409
415
  Do not put schema or published table metadata on the DataNode configuration.
410
416
 
@@ -17,7 +17,7 @@ This skill is for schema-driven application tables registered through TS Manager
17
17
  - choose `platform_managed` or `external_registered` management mode
18
18
  - register platform-managed tables through the model class API
19
19
  - build registration requests from resolved SQLAlchemy metadata when inspection is useful
20
- - define indexes and foreign keys in the table contract
20
+ - define indexes and foreign keys in SQLAlchemy metadata for Alembic-owned DDL
21
21
  - design governed compiled SQL read and write operations
22
22
  - design provider-based Alembic contract evolution for MetaTables
23
23
  - run the documented `mainsequence migrations ...` lifecycle for Alembic-backed MetaTable changes
@@ -122,11 +122,15 @@ Keep the application table model as the authoring source for the neutral table c
122
122
 
123
123
  Do not hand-build contract fragments when the SQLAlchemy helper can derive them.
124
124
 
125
- ### 2. Use storage-hash physical names for backend-managed tables
125
+ ### 2. Use explicit project-prefixed table names
126
126
 
127
127
  For `platform_managed`, inherit from `PlatformManagedMetaTable`.
128
128
 
129
- The mixin derives the SQLAlchemy physical table name from storage-relevant configuration and table shape. Do not hand-write `__tablename__` for normal backend-managed tables.
129
+ Declare an explicit project-prefixed SQLAlchemy `__tablename__`. Use
130
+ `schema_table_name(project_or_app, concept)` from `mainsequence.meta_tables` to
131
+ generate that name. The mixin derives only the logical `storage_hash` from
132
+ storage-relevant configuration and table shape; it must not use that hash as the
133
+ SQLAlchemy table name.
130
134
 
131
135
  When a platform-managed table must support in-place contract migrations from its
132
136
  first version, use Alembic. Keep the SDK model as a normal
@@ -155,12 +159,21 @@ or experiment isolation.
155
159
  Prefix explicit table identifiers, explicit physical table names, and Alembic
156
160
  version table names with the project or package name. Bare names such as
157
161
  `Account`, `Asset`, or `alembic_version` can collide across projects sharing an
158
- organization or database schema.
162
+ organization or database schema. Prefer `schema_table_name(...)` over
163
+ hand-built f-strings so project/app prefixes, bounded length, and separators are
164
+ consistent.
159
165
 
160
166
  Register through the class API:
161
167
 
162
168
  ```python
169
+ from mainsequence.meta_tables import PlatformManagedMetaTable, schema_table_name
170
+
171
+ PROJECT_NAME = "sdk_examples"
172
+ ACCOUNT_TABLE_NAME = schema_table_name(PROJECT_NAME, "account")
173
+
174
+
163
175
  class Account(PlatformManagedMetaTable, Base):
176
+ __tablename__ = ACCOUNT_TABLE_NAME
164
177
  __metatable_namespace__ = "sdk-examples"
165
178
  __metatable_identifier__ = "sdk_examples.Account"
166
179
  __metatable_extra_hash_components__ = {"storage_name": "account"}
@@ -215,28 +228,25 @@ Do not add generic labels such as `"meta-table"` or `"platform-managed"` to exam
215
228
  Do not add a `MAINSEQUENCE_META_TABLE_REGISTER` toggle in platform-managed
216
229
  examples. Platform-managed examples should be migration-first.
217
230
 
218
- ### 3. Register parent tables before child tables
231
+ ### 3. Keep foreign keys in SQLAlchemy/Alembic metadata
219
232
 
220
- Foreign-key contracts reference the target `MetaTable` UID.
233
+ For `PlatformManagedMetaTable`, define foreign keys with normal SQLAlchemy
234
+ `ForeignKey(...)` / `ForeignKeyConstraint(...)` metadata. Do not write explicit
235
+ target `MetaTable.uid` maps in the platform-managed path. Migration is the
236
+ lifecycle path: the SDK reserves provider MetaTable rows, Alembic renders and
237
+ applies FK/index DDL from SQLAlchemy metadata, and finalization refreshes the
238
+ catalog after upgrade.
221
239
 
222
- For `PlatformManagedMetaTable`, define foreign keys with
223
- `MetaTableForeignKey(TargetModel, column=...)`. Do not write raw SQLAlchemy
224
- table fullnames, `Parent.__table__.c.<column>` targets, or explicit target UID
225
- maps in the platform-managed path. Migration is the lifecycle path. Migration
226
- tooling resolves/registers unresolved target model classes, stores each
227
- returned `MetaTable` in a local process registry keyed by `storage_hash`, and
228
- uses the target `MetaTable.uid` in the child FK contract.
229
-
230
- Do not require users to provide foreign-key names. Platform-managed
231
- `MetaTableForeignKey(...)` contracts store logical relationships only. Alembic,
232
- SQLAlchemy, and the database own physical FK constraint names.
240
+ Prefer project-prefixed SQLAlchemy table names for explicit FK string targets.
241
+ Alembic, SQLAlchemy, and the database own physical FK/index names unless the
242
+ project explicitly names them in SQLAlchemy.
233
243
 
234
244
  Use this pattern:
235
245
 
236
246
  ```python
237
247
  account_uid: Mapped[uuid.UUID] = mapped_column(
238
248
  Uuid,
239
- MetaTableForeignKey(Account, column="uid", ondelete="RESTRICT"),
249
+ ForeignKey("public.sdk_examples__account.uid", ondelete="RESTRICT"),
240
250
  nullable=False,
241
251
  )
242
252
  ```
@@ -253,21 +263,19 @@ migration = AlembicMetaTableMigration(
253
263
  )
254
264
  ```
255
265
 
256
- Migration tooling registers `Account` first if `Asset` depends on it and it has
257
- not already been bound in the current process. The local registry prevents
258
- duplicate backend registration attempts for the same `storage_hash` and raises
259
- a clear error for recursive registration cycles.
266
+ Migration tooling reserves provider MetaTable rows in the provider-declared
267
+ model order. Include related parent and child models in the same selected
268
+ provider when Alembic must create or evolve their FK DDL.
260
269
 
261
- For `external_registered`, there is no platform-managed parent lookup. Register
262
- the parent first, then build the child registration request with
263
- `target_meta_tables={Account: account_meta_table}`:
270
+ For `external_registered`, register each external table contract directly. Do
271
+ not encode FK target MetaTable UIDs in the child registration request; FKs are
272
+ database DDL/introspection metadata, not SDK registration contract fields:
264
273
 
265
274
  ```python
266
275
  account_meta_table = MetaTable.register(account_request)
267
276
  asset_request = external_registered_registration_request_from_sqlalchemy_model(
268
277
  Asset,
269
278
  data_source_uid=data_source_uid,
270
- target_meta_tables={Account: account_meta_table},
271
279
  )
272
280
  asset_meta_table = MetaTable.register(asset_request)
273
281
  ```
@@ -284,6 +292,12 @@ SQLAlchemy class and calling normal registration again. Shape-addressed
284
292
  foreign keys, or constraints change, so new code cannot reliably recover the
285
293
  previous shape-derived table.
286
294
 
295
+ Do not modify Alembic revision files that have already been implemented/applied.
296
+ MetaTable migrations are database-backed history: once a revision may exist in a
297
+ database `alembic_version` table, changing that file corrupts the relationship
298
+ between source history and deployed state. For any follow-up schema change,
299
+ create a new Alembic revision on top of the current head.
300
+
287
301
  For contract evolution, define or update one selected
288
302
  `AlembicMetaTableMigration` provider:
289
303
 
@@ -292,21 +306,19 @@ For contract evolution, define or update one selected
292
306
  - set `package`, `migration_namespace`, `script_location`, and `target_metadata`
293
307
  - set `alembic_registry` to an `AlembicVersionMetaTable` subclass
294
308
  - list the post-apply catalog scope in `metatable_models`
295
- - generate, render, dry-run, apply, and refresh catalog bindings
309
+ - generate revisions, apply migrations, and refresh catalog bindings
296
310
  through `mainsequence migrations ...` commands
297
311
 
298
312
  `alembic_version_meta_table_uid` is the UID of the catalog binding for Alembic's
299
313
  version table. It is not the UID of the table being migrated.
300
314
 
301
- Application MetaTable catalog sync resolves existing rows by exact
302
- `identifier`. If a model declares `__metatable_identifier__`, that value is the
303
- global identity. If it does not, the SDK derives the identifier from
304
- `[project].name` in `pyproject.toml` plus
305
- `<model.__module__>.<model.__qualname__>`. Pin an explicit identifier when a
306
- class is renamed or moved but must keep the same platform identity.
315
+ Application MetaTable catalog sync resolves existing rows by the authored
316
+ SQLAlchemy table name used by the provider model. Keep that table name stable
317
+ when a class is renamed or moved but must keep the same platform identity.
307
318
  When declaring an explicit identifier, explicit physical table name, or Alembic
308
319
  version table name, prefix it with the project or package name rather than using
309
- a bare table name.
320
+ a bare table name. Use `schema_table_name(project_or_app, concept)` for the
321
+ physical table and Alembic version table names.
310
322
 
311
323
  Do not ask users to construct backend migration payloads, call low-level
312
324
  migration request models, or use SDK helper functions directly. The backend
@@ -359,9 +371,8 @@ When reviewing an existing MetaTable workflow, look for:
359
371
  - backend-managed examples that use namespace environment variables instead of a plain `sdk-examples` namespace
360
372
  - duplicate schema sources outside SQLAlchemy table metadata
361
373
  - external tables registered with unstable physical names
362
- - platform-managed examples that manually sequence parent registration instead
363
- of relying on `MetaTableForeignKey(...)` recursive registration
364
- - external child registrations that do not map foreign-key targets to registered parent `MetaTable.uid` values
374
+ - platform-managed examples that try to sequence parent registration through FK metadata
375
+ - external child registrations that try to encode FK target MetaTable UIDs in registration contracts
365
376
  - contract changes attempted through normal registration instead of an Alembic migration
366
377
  - migration work that asks users to define backend payloads, artifact rows, or SDK request objects
367
378
  - compiled SQL operations without complete table scope
@@ -376,9 +387,9 @@ Do not claim success until you have checked:
376
387
  - the table has an intention-rich `__metatable_description__`
377
388
  - every mapped column has an intention-rich `info.description`
378
389
  - indexes are intentional
379
- - foreign keys resolve to the correct dependency targets
390
+ - foreign keys are present in SQLAlchemy metadata for Alembic when required
380
391
  - management mode is correct
381
- - backend-managed physical names match the storage hash
392
+ - authored physical names are explicit, project-prefixed SQLAlchemy table names
382
393
  - registration returns a `MetaTable.uid`
383
394
  - compiled SQL operations declare table scope
384
395
  - migrations use Alembic-rendered SQL
@@ -391,9 +402,8 @@ Do not claim success until you have checked:
391
402
  For related tables, also check:
392
403
 
393
404
  - aliases are readable
394
- - platform-managed child registration recursively resolves parent
395
- `MetaTableForeignKey(...)` targets
396
- - external child registration requests map FK targets to the registered parent UIDs
405
+ - platform-managed child tables and parent tables are included in the selected migration provider
406
+ - FK target strings use stable project-prefixed SQLAlchemy table names where explicit strings are authored
397
407
  - query results still match the expected response contract
398
408
 
399
409
  ## This Skill Must Stop And Escalate When
@@ -7038,9 +7038,6 @@ def _meta_table_detail_impl(meta_table_uid: str, timeout: int | None) -> None:
7038
7038
  ("Contract Version", str(meta_table.get("contract_version") or "-")),
7039
7039
  ("Table Contract", _format_json_value(meta_table.get("table_contract"))),
7040
7040
  ("Columns", _format_json_value(meta_table.get("columns"))),
7041
- ("Indexes", _format_json_value(meta_table.get("indexes_meta"))),
7042
- ("Foreign Keys", _format_json_value(meta_table.get("foreign_keys"))),
7043
- ("Incoming FKs", _format_json_value(meta_table.get("incoming_fks"))),
7044
7041
  ("Introspection", _format_json_value(meta_table.get("introspection_snapshot"))),
7045
7042
  ],
7046
7043
  )
@@ -16,6 +16,7 @@ import typer
16
16
  from mainsequence.client.metatables import (
17
17
  DynamicTableDataSource,
18
18
  DynamicTableDataSourceMigrationConnectionRequest,
19
+ TimeIndexMetaData,
19
20
  )
20
21
  from mainsequence.meta_tables.migrations import (
21
22
  AlembicMetaTableMigration,
@@ -26,7 +27,8 @@ from mainsequence.meta_tables.migrations import (
26
27
 
27
28
  migrations = typer.Typer(help="Alembic-owned MetaTable migration commands")
28
29
  REGISTER_ENDPOINT = "/orm/api/ts_manager/meta_table/register/"
29
- RESERVE_MANAGED_ENDPOINT = "/orm/api/ts_manager/meta_table/reserve-managed/"
30
+ METATABLE_COLLECTION_ENDPOINT = "/orm/api/ts_manager/meta_table/"
31
+ DYNAMIC_TABLE_COLLECTION_ENDPOINT = "/orm/api/ts_manager/dynamic_table/"
30
32
  FINALIZE_MANAGED_ENDPOINT = "/orm/api/ts_manager/meta_table/finalize-managed/"
31
33
  ALEMBIC_PROVIDER_RESET_ENDPOINT = "/orm/api/ts_manager/meta_table/alembic-provider-reset/"
32
34
 
@@ -339,6 +341,20 @@ def _metatable_message(
339
341
  return " ".join(parts)
340
342
 
341
343
 
344
+ def _collection_create_endpoint_for_items(items: Sequence[Any]) -> str:
345
+ if items and all(_item_value(item, "time_index_name") not in (None, "") for item in items):
346
+ return DYNAMIC_TABLE_COLLECTION_ENDPOINT
347
+ return METATABLE_COLLECTION_ENDPOINT
348
+
349
+
350
+ def _collection_create_endpoint_for_item(item: Any) -> str:
351
+ if isinstance(item, TimeIndexMetaData):
352
+ return DYNAMIC_TABLE_COLLECTION_ENDPOINT
353
+ if _item_value(item, "time_index_name") not in (None, ""):
354
+ return DYNAMIC_TABLE_COLLECTION_ENDPOINT
355
+ return METATABLE_COLLECTION_ENDPOINT
356
+
357
+
342
358
  def _emit_metatable_registration(model: type[Any], item: Any) -> None:
343
359
  _emit_progress(
344
360
  _metatable_message(
@@ -354,6 +370,7 @@ def _emit_metatable_reservation_request(
354
370
  models: Sequence[type[Any]],
355
371
  tables: Sequence[Any],
356
372
  ) -> None:
373
+ endpoint = _collection_create_endpoint_for_items(tables)
357
374
  table_names = []
358
375
  for model, table in zip(models, tables, strict=True):
359
376
  table_names.append(
@@ -365,7 +382,7 @@ def _emit_metatable_reservation_request(
365
382
  )
366
383
  )
367
384
  _emit_status(
368
- f"Sending POST {RESERVE_MANAGED_ENDPOINT} request for {len(tables)} "
385
+ f"Sending POST {endpoint} collection-create request for {len(tables)} "
369
386
  f"MetaTables table_names={','.join(table_names)}"
370
387
  )
371
388
 
@@ -373,7 +390,7 @@ def _emit_metatable_reservation_request(
373
390
  def _emit_metatable_reservation(model: type[Any], item: Any) -> None:
374
391
  _emit_progress(
375
392
  _metatable_message(
376
- endpoint=RESERVE_MANAGED_ENDPOINT,
393
+ endpoint=_collection_create_endpoint_for_item(item),
377
394
  action="reserved",
378
395
  model=model,
379
396
  item=item,
@@ -400,8 +417,6 @@ def _prepare_alembic_config(
400
417
  timeout: float | None,
401
418
  ttl_seconds: int,
402
419
  alembic_output: _AlembicOutput,
403
- stage_existing_schema_management: bool = True,
404
- require_existing_contract_match: bool = True,
405
420
  prepare_provider_metatables: bool = True,
406
421
  ) -> tuple[Any, Any]:
407
422
  _emit_status("Ensuring Alembic registry MetaTable...")
@@ -414,8 +429,6 @@ def _prepare_alembic_config(
414
429
  _emit_status("Preparing platform-managed MetaTable reservations...")
415
430
  prepared = migration.prepare_for_alembic(
416
431
  timeout=timeout,
417
- stage_existing_schema_management=stage_existing_schema_management,
418
- require_existing_contract_match=require_existing_contract_match,
419
432
  on_metatable_reservation_request=_emit_metatable_reservation_request,
420
433
  on_metatable_reservation_status=_emit_status,
421
434
  on_metatable_reserved=_emit_metatable_reservation,
@@ -435,20 +448,16 @@ def _prepare_alembic_config(
435
448
  )
436
449
  _emit_status(f"Loading DynamicTableDataSource uid={prepared.data_source_uid}...")
437
450
  data_source = DynamicTableDataSource.get_by_uid(prepared.data_source_uid)
438
- _emit_status(
439
- "Requesting scoped migration connection "
440
- f"meta_table_count={len(prepared.meta_table_uids)} ttl_seconds={ttl_seconds}..."
441
- )
451
+ _emit_status(f"Requesting migration connection ttl_seconds={ttl_seconds}...")
442
452
  connection = data_source.issue_migration_connection(
443
453
  DynamicTableDataSourceMigrationConnectionRequest(
444
454
  package=migration.package,
445
455
  migration_namespace=migration.migration_namespace,
446
- meta_table_uids=prepared.meta_table_uids,
447
456
  ttl_seconds=ttl_seconds,
448
457
  ),
449
458
  timeout=timeout,
450
459
  )
451
- _emit_status("Scoped migration connection acquired.")
460
+ _emit_status("Migration connection acquired.")
452
461
  _emit_status("Building Alembic config...")
453
462
  config = alembic_config_for_provider(
454
463
  migration,
@@ -461,6 +470,44 @@ def _prepare_alembic_config(
461
470
  return prepared, config
462
471
 
463
472
 
473
+ def _build_revision_alembic_config(
474
+ migration: AlembicMetaTableMigration,
475
+ *,
476
+ sqlalchemy_url: str | None,
477
+ requires_database: bool,
478
+ timeout: float | None,
479
+ ttl_seconds: int,
480
+ alembic_output: _AlembicOutput,
481
+ ) -> Any:
482
+ _emit_status("Building local Alembic config for revision...")
483
+ owner_role_name = None
484
+ if requires_database and sqlalchemy_url in (None, ""):
485
+ data_source_uid = migration._resolve_provider_data_source_uid()
486
+ _emit_status(f"Loading DynamicTableDataSource uid={data_source_uid} for revision...")
487
+ data_source = DynamicTableDataSource.get_by_uid(data_source_uid)
488
+ _emit_status(f"Requesting revision migration connection ttl_seconds={ttl_seconds}...")
489
+ connection = data_source.issue_migration_connection(
490
+ DynamicTableDataSourceMigrationConnectionRequest(
491
+ package=migration.package,
492
+ migration_namespace=migration.migration_namespace,
493
+ ttl_seconds=ttl_seconds,
494
+ ),
495
+ timeout=timeout,
496
+ )
497
+ _emit_status("Revision migration connection acquired.")
498
+ sqlalchemy_url = connection.uri
499
+ owner_role_name = connection.owner_role_name
500
+ config = alembic_config_for_provider(
501
+ migration,
502
+ sqlalchemy_url=sqlalchemy_url or "postgresql://",
503
+ owner_role_name=owner_role_name,
504
+ stdout=alembic_output,
505
+ output_buffer=alembic_output,
506
+ )
507
+ _emit_status("Local Alembic config built.")
508
+ return config
509
+
510
+
464
511
  def _prepared_physical_table_refs(prepared: Any) -> list[tuple[str | None, str]]:
465
512
  refs: list[tuple[str | None, str]] = []
466
513
  for item in getattr(prepared, "reserved_tables", []) or []:
@@ -544,11 +591,11 @@ def _assert_autogenerate_baseline_visible(prepared: Any, config: Any) -> None:
544
591
  sample = ",".join(_format_table_ref(ref) for ref in table_refs[:5])
545
592
  raise RuntimeError(
546
593
  "Refusing to autogenerate a migration because this provider already has "
547
- f"Alembic head(s) {','.join(script_heads)}, but the scoped migration "
594
+ f"Alembic head(s) {','.join(script_heads)}, but the migration "
548
595
  "connection cannot see any provider physical tables. Alembic would emit a "
549
596
  "duplicate initial create-all migration instead of a schema diff. Apply the "
550
597
  "existing baseline with `mainsequence migrations upgrade ... head`, or fix "
551
- "the scoped migration connection/table visibility before running revision "
598
+ "the migration connection/table visibility before running revision "
552
599
  f"again. checked_sample={sample}"
553
600
  )
554
601
 
@@ -626,8 +673,6 @@ def current(
626
673
  timeout=timeout,
627
674
  ttl_seconds=ttl_seconds,
628
675
  alembic_output=alembic_output,
629
- stage_existing_schema_management=False,
630
- require_existing_contract_match=False,
631
676
  prepare_provider_metatables=False,
632
677
  )
633
678
  _emit_alembic_script_context(config)
@@ -653,7 +698,7 @@ def revision(
653
698
  autogenerate: bool = typer.Option(
654
699
  True,
655
700
  "--autogenerate/--no-autogenerate",
656
- help="Use Alembic autogenerate against the reserved MetaTable metadata.",
701
+ help="Use Alembic autogenerate against the local database URL and provider metadata.",
657
702
  ),
658
703
  provider: str | None = typer.Option(
659
704
  None,
@@ -662,8 +707,22 @@ def revision(
662
707
  ),
663
708
  rev_id: str | None = typer.Option(None, "--rev-id", help="Explicit Alembic revision id."),
664
709
  head: str = typer.Option("head", "--head", help="Alembic head to base the revision on."),
665
- timeout: float | None = typer.Option(None, "--timeout"),
666
- ttl_seconds: int = typer.Option(900, "--ttl-seconds", min=1),
710
+ timeout: float | None = typer.Option(
711
+ None,
712
+ "--timeout",
713
+ help="Request timeout for the revision migration connection when --sqlalchemy-url is omitted.",
714
+ ),
715
+ ttl_seconds: int = typer.Option(
716
+ 900,
717
+ "--ttl-seconds",
718
+ min=1,
719
+ help="TTL for the revision migration connection when --sqlalchemy-url is omitted.",
720
+ ),
721
+ sqlalchemy_url: str | None = typer.Option(
722
+ None,
723
+ "--sqlalchemy-url",
724
+ help="Local database URL for Alembic autogenerate. Required when --autogenerate is used.",
725
+ ),
667
726
  json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
668
727
  ) -> None:
669
728
  """Create a normal Alembic revision for the selected provider."""
@@ -676,16 +735,15 @@ def revision(
676
735
  migration,
677
736
  alembic_output=alembic_output,
678
737
  )
679
- prepared, config = _prepare_alembic_config(
738
+ config = _build_revision_alembic_config(
680
739
  migration,
740
+ sqlalchemy_url=sqlalchemy_url,
741
+ requires_database=autogenerate,
681
742
  timeout=timeout,
682
743
  ttl_seconds=ttl_seconds,
683
744
  alembic_output=alembic_output,
684
- stage_existing_schema_management=False,
685
745
  )
686
746
  _emit_alembic_script_context(config, target_revision=head)
687
- if autogenerate:
688
- _assert_autogenerate_baseline_visible(prepared, config)
689
747
  _emit_status(f"Starting Alembic revision now rev_id={resolved_rev_id}...")
690
748
  with _forward_alembic_logging():
691
749
  script = command.revision(
@@ -702,7 +760,6 @@ def revision(
702
760
  "path": getattr(script, "path", None),
703
761
  "package": migration.package,
704
762
  "migration_namespace": migration.migration_namespace,
705
- "meta_table_uids": prepared.meta_table_uids,
706
763
  },
707
764
  json_output=json_output,
708
765
  )