mainsequence 4.2.16__tar.gz → 4.2.25__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.

Potentially problematic release.


This version of mainsequence might be problematic. Click here for more details.

Files changed (126) hide show
  1. {mainsequence-4.2.16/mainsequence.egg-info → mainsequence-4.2.25}/PKG-INFO +2 -2
  2. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +7 -4
  3. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +12 -5
  4. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/migrations.py +247 -22
  5. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/metatables/core.py +28 -4
  6. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/migrations.py +407 -216
  7. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/sqlalchemy_contracts.py +27 -183
  8. {mainsequence-4.2.16 → mainsequence-4.2.25/mainsequence.egg-info}/PKG-INFO +2 -2
  9. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/requires.txt +1 -1
  10. {mainsequence-4.2.16 → mainsequence-4.2.25}/pyproject.toml +2 -2
  11. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli_migrations.py +93 -43
  12. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_table_migrations.py +385 -59
  13. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_tables_client_models.py +84 -0
  14. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_tables_sqlalchemy_contracts.py +60 -49
  15. {mainsequence-4.2.16 → mainsequence-4.2.25}/LICENSE +0 -0
  16. {mainsequence-4.2.16 → mainsequence-4.2.25}/README.md +0 -0
  17. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/AGENTS.md +0 -0
  18. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  19. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  20. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  21. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  22. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  23. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  24. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  25. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  26. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  27. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  28. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  29. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  30. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  31. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  32. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  33. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  34. {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  35. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/__init__.py +0 -0
  36. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/__main__.py +0 -0
  37. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/bootstrap.py +0 -0
  38. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/__init__.py +0 -0
  39. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/api.py +0 -0
  40. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/browser_auth.py +0 -0
  41. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/cli.py +0 -0
  42. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/config.py +0 -0
  43. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/docker_utils.py +0 -0
  44. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/doctor.py +0 -0
  45. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/local_ops.py +0 -0
  46. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/model_filters.py +0 -0
  47. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/project_status.py +0 -0
  48. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/pydantic_cli.py +0 -0
  49. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/sdk_utils.py +0 -0
  50. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/ssh_utils.py +0 -0
  51. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/ui.py +0 -0
  52. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/__init__.py +0 -0
  53. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/agent_runtime_models.py +0 -0
  54. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/base.py +0 -0
  55. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/client.py +0 -0
  56. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/__init__.py +0 -0
  57. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/app_component.py +0 -0
  58. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/connections.py +0 -0
  59. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/data_models.py +0 -0
  60. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/workspace.py +0 -0
  61. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  62. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/compute_validation.py +0 -0
  63. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  64. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  65. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  66. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  67. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/dtype_codec.py +0 -0
  68. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/exceptions.py +0 -0
  69. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/fastapi/__init__.py +0 -0
  70. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/fastapi/auth.py +0 -0
  71. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/metatables/__init__.py +0 -0
  72. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_foundry.py +0 -0
  73. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_helpers.py +0 -0
  74. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_user.py +0 -0
  75. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/utils.py +0 -0
  76. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/defaults.py +0 -0
  77. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/instrumentation/__init__.py +0 -0
  78. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/instrumentation/utils.py +0 -0
  79. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/logconf.py +0 -0
  80. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/__init__.py +0 -0
  81. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/__main__.py +0 -0
  82. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  83. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  84. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  85. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  86. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  87. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  88. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  89. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  90. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  91. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  92. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/future_registry.py +0 -0
  93. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/hashing.py +0 -0
  94. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  95. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/runtime_flags.py +0 -0
  96. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/SOURCES.txt +0 -0
  97. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/dependency_links.txt +0 -0
  98. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/entry_points.txt +0 -0
  99. {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/top_level.txt +0 -0
  100. {mainsequence-4.2.16 → mainsequence-4.2.25}/setup.cfg +0 -0
  101. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_auth_precedence.py +0 -0
  102. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_build_operations_hashing.py +0 -0
  103. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli.py +0 -0
  104. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli_browser_auth.py +0 -0
  105. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_client.py +0 -0
  106. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_app_component_models.py +0 -0
  107. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_data_models.py +0 -0
  108. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_models.py +0 -0
  109. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  110. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_node_storage_dimension_queries.py +0 -0
  111. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_node_update_flow.py +0 -0
  112. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_dependency_extras.py +0 -0
  113. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_duckdb_interface_dimensions.py +0 -0
  114. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_filter_normalization.py +0 -0
  115. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_logconf.py +0 -0
  116. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_models_user_request_bound_auth.py +0 -0
  117. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_pod_project_resolution.py +0 -0
  118. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_project_batch_jobs_from_file.py +0 -0
  119. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_run_configuration.py +0 -0
  120. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_secret_client_model.py +0 -0
  121. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_source_table_configuration.py +0 -0
  122. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_sqlite_interface_dimensions.py +0 -0
  123. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_runner_uid_runtime.py +0 -0
  124. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_statistics.py +0 -0
  125. {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_uid_guards.py +0 -0
  126. {mainsequence-4.2.16 → mainsequence-4.2.25}/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.16
3
+ Version: 4.2.25
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
@@ -68,7 +68,7 @@ Requires-Dist: pydantic
68
68
  Requires-Dist: pytz
69
69
  Requires-Dist: pyyaml
70
70
  Requires-Dist: requests
71
- Requires-Dist: sqlalchemy
71
+ Requires-Dist: sqlalchemy<3,>=2
72
72
  Requires-Dist: structlog
73
73
  Requires-Dist: tqdm
74
74
  Requires-Dist: typer
@@ -148,7 +148,7 @@ class Base(DeclarativeBase):
148
148
 
149
149
  class PricesTable(PlatformTimeIndexMetaData, Base):
150
150
  __metatable_namespace__ = "<domain_namespace>"
151
- __metatable_identifier__ = "<table_identifier>"
151
+ __metatable_identifier__ = "<project_name>.<table_identifier>"
152
152
  __metatable_extra_hash_components__ = {"storage_name": "<stable_storage_name>"}
153
153
  __metatable_description__ = (
154
154
  "Daily close prices keyed by asset unique identifier for portfolio and "
@@ -387,9 +387,9 @@ DataNode storage table needs a platform-managed FK, use
387
387
  `ForeignKey(Target.__table__.c.uid)`, table fullnames, or explicit target UID
388
388
  maps in DataNode examples.
389
389
 
390
- Do not ask users to name these foreign keys. `MetaTableForeignKey(...)` derives
391
- a stable contract name when `name` is omitted; `name=...` is only for deliberate
392
- overrides.
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.
393
393
 
394
394
  Registration of the storage class follows the MetaTable migration lifecycle.
395
395
  Migration tooling recursively resolves/registers unresolved FK target model
@@ -402,6 +402,9 @@ Do not add DataNode configuration fields just to mutate storage metadata.
402
402
 
403
403
  Production-quality table identifiers, descriptions, labels, column docs, and
404
404
  foreign-key metadata belong to the storage class/MetaTable registration path.
405
+ Prefix explicit table identifiers, explicit physical table names, and Alembic
406
+ version table names with the project or package name rather than using bare
407
+ names that can collide across projects.
405
408
 
406
409
  Do not put schema or published table metadata on the DataNode configuration.
407
410
 
@@ -152,12 +152,17 @@ table. Do not use it for labels, descriptions, runtime options, test isolation,
152
152
  backend UIDs, data-source UIDs, or updater scope. Use `hash_namespace` for test
153
153
  or experiment isolation.
154
154
 
155
+ Prefix explicit table identifiers, explicit physical table names, and Alembic
156
+ version table names with the project or package name. Bare names such as
157
+ `Account`, `Asset`, or `alembic_version` can collide across projects sharing an
158
+ organization or database schema.
159
+
155
160
  Register through the class API:
156
161
 
157
162
  ```python
158
163
  class Account(PlatformManagedMetaTable, Base):
159
164
  __metatable_namespace__ = "sdk-examples"
160
- __metatable_identifier__ = "Account"
165
+ __metatable_identifier__ = "sdk_examples.Account"
161
166
  __metatable_extra_hash_components__ = {"storage_name": "account"}
162
167
  __metatable_description__ = (
163
168
  "Customer account master records used to scope balances, holdings, and "
@@ -222,10 +227,9 @@ tooling resolves/registers unresolved target model classes, stores each
222
227
  returned `MetaTable` in a local process registry keyed by `storage_hash`, and
223
228
  uses the target `MetaTable.uid` in the child FK contract.
224
229
 
225
- Do not require users to provide foreign-key names. `MetaTableForeignKey(...)`
226
- accepts `name=...` only as an override; when omitted, the SDK derives a stable
227
- PostgreSQL-safe contract name from the child table and source column after the
228
- column is attached to the SQLAlchemy table.
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.
229
233
 
230
234
  Use this pattern:
231
235
 
@@ -300,6 +304,9 @@ global identity. If it does not, the SDK derives the identifier from
300
304
  `[project].name` in `pyproject.toml` plus
301
305
  `<model.__module__>.<model.__qualname__>`. Pin an explicit identifier when a
302
306
  class is renamed or moved but must keep the same platform identity.
307
+ When declaring an explicit identifier, explicit physical table name, or Alembic
308
+ version table name, prefix it with the project or package name rather than using
309
+ a bare table name.
303
310
 
304
311
  Do not ask users to construct backend migration payloads, call low-level
305
312
  migration request models, or use SDK helper functions directly. The backend
@@ -7,6 +7,7 @@ import re
7
7
  import sys
8
8
  from collections.abc import Mapping, Sequence
9
9
  from contextlib import contextmanager
10
+ from types import SimpleNamespace
10
11
  from typing import Any
11
12
 
12
13
  import click
@@ -19,6 +20,7 @@ from mainsequence.client.metatables import (
19
20
  from mainsequence.meta_tables.migrations import (
20
21
  AlembicMetaTableMigration,
21
22
  alembic_config_for_provider,
23
+ apply_mainsequence_migration_role,
22
24
  load_alembic_metatable_migration_provider,
23
25
  )
24
26
 
@@ -70,9 +72,7 @@ def _forward_alembic_logging():
70
72
  previous_state = [(logger, logger.level, logger.propagate) for logger in loggers]
71
73
  handler = _AlembicLogHandler()
72
74
  handler.setLevel(logging.DEBUG)
73
- handler.setFormatter(
74
- logging.Formatter("[alembic] %(levelname)s %(name)s: %(message)s")
75
- )
75
+ handler.setFormatter(logging.Formatter("[alembic] %(levelname)s %(name)s: %(message)s"))
76
76
  root_logger = logging.getLogger("alembic")
77
77
  root_logger.addHandler(handler)
78
78
  for logger in loggers:
@@ -120,6 +120,72 @@ def _load_alembic_command(command_name: str) -> Any:
120
120
  return command
121
121
 
122
122
 
123
+ def _emit_alembic_script_context(
124
+ config: Any,
125
+ *,
126
+ target_revision: str | None = None,
127
+ ) -> None:
128
+ script_location = config.get_main_option("script_location")
129
+ version_table = config.get_main_option("version_table")
130
+ version_table_schema = config.get_main_option("version_table_schema")
131
+ version_table_label = (
132
+ f"{version_table_schema}.{version_table}"
133
+ if version_table_schema not in (None, "")
134
+ else version_table
135
+ )
136
+ _emit_status(
137
+ "Alembic script context "
138
+ f"script_location={script_location} version_table={version_table_label}"
139
+ )
140
+ try:
141
+ from alembic.script import ScriptDirectory
142
+ except ImportError:
143
+ _emit_status("Alembic ScriptDirectory is unavailable.")
144
+ return
145
+
146
+ try:
147
+ script = ScriptDirectory.from_config(config)
148
+ except Exception as exc:
149
+ _emit_status(f"Alembic script directory could not be resolved: {exc}")
150
+ return
151
+
152
+ _emit_status(
153
+ "Alembic script directory resolved "
154
+ f"path={getattr(script, 'dir', None)} versions={getattr(script, 'versions', None)}"
155
+ )
156
+ try:
157
+ heads = [str(head) for head in script.get_heads()]
158
+ except Exception as exc:
159
+ _emit_status(f"Alembic heads could not be resolved: {exc}")
160
+ heads = []
161
+ else:
162
+ _emit_status(f"Alembic heads={','.join(heads) if heads else '<none>'}")
163
+
164
+ if target_revision in (None, "", "base"):
165
+ return
166
+ if target_revision in ("head", "heads"):
167
+ for head in heads:
168
+ _emit_revision_path(script, head)
169
+ return
170
+ _emit_revision_path(script, target_revision)
171
+
172
+
173
+ def _emit_revision_path(script: Any, revision: str) -> None:
174
+ try:
175
+ resolved = script.get_revision(revision)
176
+ except Exception as exc:
177
+ _emit_status(f"Alembic revision {revision} could not be resolved: {exc}")
178
+ return
179
+ if resolved is None:
180
+ _emit_status(f"Alembic revision {revision} was not found.")
181
+ return
182
+ _emit_status(
183
+ "Alembic revision resolved "
184
+ f"revision={resolved.revision} path={getattr(resolved, 'path', None)} "
185
+ f"down_revision={resolved.down_revision}"
186
+ )
187
+
188
+
123
189
  def _jsonable(value: Any) -> Any:
124
190
  if hasattr(value, "model_dump"):
125
191
  return value.model_dump(mode="json")
@@ -162,7 +228,11 @@ def _contract_physical_table_name(item: Any) -> Any:
162
228
  contract = _item_value(item, "table_contract")
163
229
  if contract is None:
164
230
  return None
165
- physical = contract.get("physical") if isinstance(contract, Mapping) else getattr(contract, "physical", None)
231
+ physical = (
232
+ contract.get("physical")
233
+ if isinstance(contract, Mapping)
234
+ else getattr(contract, "physical", None)
235
+ )
166
236
  if physical is None:
167
237
  return None
168
238
  if isinstance(physical, Mapping):
@@ -170,6 +240,32 @@ def _contract_physical_table_name(item: Any) -> Any:
170
240
  return getattr(physical, "table_name", None)
171
241
 
172
242
 
243
+ def _contract_physical_schema(item: Any) -> Any:
244
+ contract = _item_value(item, "table_contract")
245
+ if contract is None:
246
+ return None
247
+ physical = (
248
+ contract.get("physical")
249
+ if isinstance(contract, Mapping)
250
+ else getattr(contract, "physical", None)
251
+ )
252
+ if physical is None:
253
+ return None
254
+ if isinstance(physical, Mapping):
255
+ return (
256
+ physical.get("schema")
257
+ or physical.get("schema_")
258
+ or physical.get("table_schema")
259
+ or physical.get("schema_name")
260
+ )
261
+ return (
262
+ getattr(physical, "schema", None)
263
+ or getattr(physical, "schema_", None)
264
+ or getattr(physical, "table_schema", None)
265
+ or getattr(physical, "schema_name", None)
266
+ )
267
+
268
+
173
269
  def _meta_table_uid(item: Any) -> str | None:
174
270
  if item is None:
175
271
  return None
@@ -191,9 +287,7 @@ def _include_alembic_registry_in_scope(
191
287
  registry_uid = _meta_table_uid(registry_meta_table)
192
288
  if registry_uid in (None, ""):
193
289
  return
194
- prepared.meta_table_uids = list(
195
- dict.fromkeys([registry_uid, *list(prepared.meta_table_uids)])
196
- )
290
+ prepared.meta_table_uids = list(dict.fromkeys([registry_uid, *list(prepared.meta_table_uids)]))
197
291
 
198
292
 
199
293
  def _metatable_message(
@@ -210,13 +304,15 @@ def _metatable_message(
210
304
  or model_name
211
305
  )
212
306
  uid = _item_value(item, "meta_table_uid") or _item_value(item, "uid")
213
- physical_table_name = (
214
- _item_value(item, "physical_table_name")
215
- or _contract_physical_table_name(item)
307
+ physical_table_name = _item_value(item, "physical_table_name") or _contract_physical_table_name(
308
+ item
216
309
  )
217
310
  provisioning_status = _item_value(item, "provisioning_status")
218
311
  created = _item_value(item, "created")
219
312
  matched_by = _item_value(item, "matched_by")
313
+ finalized = _item_value(item, "finalized")
314
+ physical_table_exists = _item_value(item, "physical_table_exists")
315
+ error = _item_value(item, "error")
220
316
 
221
317
  parts = [
222
318
  f"POST {endpoint}",
@@ -234,6 +330,12 @@ def _metatable_message(
234
330
  parts.append(f"created={created}")
235
331
  if matched_by not in (None, ""):
236
332
  parts.append(f"matched_by={matched_by}")
333
+ if finalized is not None:
334
+ parts.append(f"finalized={finalized}")
335
+ if physical_table_exists is not None:
336
+ parts.append(f"physical_table_exists={physical_table_exists}")
337
+ if error not in (None, "", {}):
338
+ parts.append(f"error={json.dumps(error, sort_keys=True, default=str)}")
237
339
  return " ".join(parts)
238
340
 
239
341
 
@@ -252,18 +354,19 @@ def _emit_metatable_reservation_request(
252
354
  models: Sequence[type[Any]],
253
355
  tables: Sequence[Any],
254
356
  ) -> None:
255
- identifiers = []
357
+ table_names = []
256
358
  for model, table in zip(models, tables, strict=True):
257
- identifiers.append(
359
+ table_names.append(
258
360
  str(
259
- _item_value(table, "identifier")
361
+ _item_value(table, "physical_table_name")
362
+ or _item_value(table, "identifier")
260
363
  or getattr(model, "__metatable_identifier__", None)
261
364
  or getattr(model, "__name__", repr(model))
262
365
  )
263
366
  )
264
367
  _emit_status(
265
368
  f"Sending POST {RESERVE_MANAGED_ENDPOINT} request for {len(tables)} "
266
- f"MetaTables identifiers={','.join(identifiers)}"
369
+ f"MetaTables table_names={','.join(table_names)}"
267
370
  )
268
371
 
269
372
 
@@ -279,10 +382,12 @@ def _emit_metatable_reservation(model: type[Any], item: Any) -> None:
279
382
 
280
383
 
281
384
  def _emit_metatable_finalization(model: type[Any], item: Any) -> None:
385
+ finalized = _item_value(item, "finalized")
386
+ action = "finalized" if finalized is not False else "finalize-failed"
282
387
  _emit_progress(
283
388
  _metatable_message(
284
389
  endpoint=FINALIZE_MANAGED_ENDPOINT,
285
- action="finalized",
390
+ action=action,
286
391
  model=model,
287
392
  item=item,
288
393
  ),
@@ -295,6 +400,9 @@ def _prepare_alembic_config(
295
400
  timeout: float | None,
296
401
  ttl_seconds: int,
297
402
  alembic_output: _AlembicOutput,
403
+ stage_existing_schema_management: bool = True,
404
+ require_existing_contract_match: bool = True,
405
+ prepare_provider_metatables: bool = True,
298
406
  ) -> tuple[Any, Any]:
299
407
  _emit_status("Ensuring Alembic registry MetaTable...")
300
408
  registry_meta_table = migration.ensure_alembic_registry(
@@ -302,13 +410,23 @@ def _prepare_alembic_config(
302
410
  on_metatable_registered=_emit_metatable_registration,
303
411
  )
304
412
 
305
- _emit_status("Preparing platform-managed MetaTable reservations...")
306
- prepared = migration.prepare_for_alembic(
307
- timeout=timeout,
308
- on_metatable_reservation_request=_emit_metatable_reservation_request,
309
- on_metatable_reservation_status=_emit_status,
310
- on_metatable_reserved=_emit_metatable_reservation,
311
- )
413
+ if prepare_provider_metatables:
414
+ _emit_status("Preparing platform-managed MetaTable reservations...")
415
+ prepared = migration.prepare_for_alembic(
416
+ timeout=timeout,
417
+ stage_existing_schema_management=stage_existing_schema_management,
418
+ require_existing_contract_match=require_existing_contract_match,
419
+ on_metatable_reservation_request=_emit_metatable_reservation_request,
420
+ on_metatable_reservation_status=_emit_status,
421
+ on_metatable_reserved=_emit_metatable_reservation,
422
+ )
423
+ else:
424
+ _emit_status("Skipping provider MetaTable reservations for read-only Alembic command.")
425
+ prepared = SimpleNamespace(
426
+ data_source_uid=migration._resolve_provider_data_source_uid(),
427
+ meta_table_uids=[],
428
+ owner_role_name=None,
429
+ )
312
430
  _include_alembic_registry_in_scope(migration, prepared, registry_meta_table)
313
431
  _emit_status(
314
432
  "Prepared migration scope "
@@ -343,6 +461,103 @@ def _prepare_alembic_config(
343
461
  return prepared, config
344
462
 
345
463
 
464
+ def _prepared_physical_table_refs(prepared: Any) -> list[tuple[str | None, str]]:
465
+ refs: list[tuple[str | None, str]] = []
466
+ for item in getattr(prepared, "reserved_tables", []) or []:
467
+ table_name = _item_value(item, "physical_table_name") or _contract_physical_table_name(item)
468
+ if table_name in (None, ""):
469
+ continue
470
+ schema = _contract_physical_schema(item)
471
+ refs.append((str(schema) if schema not in (None, "") else "public", str(table_name)))
472
+ return list(dict.fromkeys(refs))
473
+
474
+
475
+ def _alembic_script_heads(config: Any) -> list[str]:
476
+ try:
477
+ from alembic.script import ScriptDirectory
478
+ except ImportError as exc:
479
+ raise RuntimeError("Alembic is required for migration commands.") from exc
480
+ with _forward_alembic_logging():
481
+ return [str(head) for head in ScriptDirectory.from_config(config).get_heads()]
482
+
483
+
484
+ def _assert_autogenerate_baseline_visible(prepared: Any, config: Any) -> None:
485
+ script_heads = _alembic_script_heads(config)
486
+ if not script_heads:
487
+ _emit_status("No existing Alembic heads; allowing initial autogenerate baseline.")
488
+ return
489
+
490
+ table_refs = _prepared_physical_table_refs(prepared)
491
+ if not table_refs:
492
+ _emit_status(
493
+ "No provider physical table names were available for autogenerate "
494
+ "baseline visibility check."
495
+ )
496
+ return
497
+
498
+ try:
499
+ from sqlalchemy import create_engine, inspect
500
+ from sqlalchemy.pool import NullPool
501
+ except ImportError as exc:
502
+ raise RuntimeError("SQLAlchemy is required for Alembic autogenerate preflight.") from exc
503
+
504
+ sqlalchemy_url = getattr(config, "attributes", {}).get("mainsequence_migration_sqlalchemy_url")
505
+ if sqlalchemy_url in (None, ""):
506
+ raise RuntimeError(
507
+ "Alembic autogenerate preflight cannot inspect the scoped database because "
508
+ "the migration SQLAlchemy URL was not attached to the Alembic config."
509
+ )
510
+
511
+ _emit_status(
512
+ "Checking Alembic autogenerate baseline visibility "
513
+ f"script_heads={','.join(script_heads)} table_count={len(table_refs)}..."
514
+ )
515
+ visible: list[tuple[str | None, str]] = []
516
+ missing: list[tuple[str | None, str]] = []
517
+ engine = create_engine(str(sqlalchemy_url), poolclass=NullPool)
518
+ try:
519
+ with engine.connect() as connection:
520
+ apply_mainsequence_migration_role(connection, config)
521
+ inspector = inspect(connection)
522
+ for schema, table_name in table_refs:
523
+ if inspector.has_table(table_name, schema=schema):
524
+ visible.append((schema, table_name))
525
+ else:
526
+ missing.append((schema, table_name))
527
+ finally:
528
+ engine.dispose()
529
+
530
+ if visible:
531
+ _emit_status(
532
+ "Alembic autogenerate baseline is visible "
533
+ f"existing_tables={len(visible)} missing_tables={len(missing)}."
534
+ )
535
+ if missing:
536
+ sample = ",".join(_format_table_ref(ref) for ref in missing[:5])
537
+ _emit_status(
538
+ "Some provider physical tables were not visible during preflight; "
539
+ f"Alembic may treat them as new if they are not intentional additions. "
540
+ f"sample={sample}"
541
+ )
542
+ return
543
+
544
+ sample = ",".join(_format_table_ref(ref) for ref in table_refs[:5])
545
+ raise RuntimeError(
546
+ "Refusing to autogenerate a migration because this provider already has "
547
+ f"Alembic head(s) {','.join(script_heads)}, but the scoped migration "
548
+ "connection cannot see any provider physical tables. Alembic would emit a "
549
+ "duplicate initial create-all migration instead of a schema diff. Apply the "
550
+ "existing baseline with `mainsequence migrations upgrade ... head`, or fix "
551
+ "the scoped migration connection/table visibility before running revision "
552
+ f"again. checked_sample={sample}"
553
+ )
554
+
555
+
556
+ def _format_table_ref(ref: tuple[str | None, str]) -> str:
557
+ schema, table_name = ref
558
+ return f"{schema}.{table_name}" if schema not in (None, "") else table_name
559
+
560
+
346
561
  def _next_sequential_revision_id(
347
562
  migration: AlembicMetaTableMigration,
348
563
  *,
@@ -411,7 +626,11 @@ def current(
411
626
  timeout=timeout,
412
627
  ttl_seconds=ttl_seconds,
413
628
  alembic_output=alembic_output,
629
+ stage_existing_schema_management=False,
630
+ require_existing_contract_match=False,
631
+ prepare_provider_metatables=False,
414
632
  )
633
+ _emit_alembic_script_context(config)
415
634
  _emit_status("Starting Alembic current now...")
416
635
  with _forward_alembic_logging():
417
636
  command.current(config, verbose=verbose)
@@ -462,7 +681,11 @@ def revision(
462
681
  timeout=timeout,
463
682
  ttl_seconds=ttl_seconds,
464
683
  alembic_output=alembic_output,
684
+ stage_existing_schema_management=False,
465
685
  )
686
+ _emit_alembic_script_context(config, target_revision=head)
687
+ if autogenerate:
688
+ _assert_autogenerate_baseline_visible(prepared, config)
466
689
  _emit_status(f"Starting Alembic revision now rev_id={resolved_rev_id}...")
467
690
  with _forward_alembic_logging():
468
691
  script = command.revision(
@@ -508,6 +731,7 @@ def upgrade(
508
731
  ttl_seconds=ttl_seconds,
509
732
  alembic_output=alembic_output,
510
733
  )
734
+ _emit_alembic_script_context(config, target_revision=target_revision)
511
735
  _emit_status(f"Starting Alembic upgrade now target={target_revision}...")
512
736
  with _forward_alembic_logging():
513
737
  command.upgrade(config, target_revision)
@@ -559,6 +783,7 @@ def downgrade(
559
783
  ttl_seconds=ttl_seconds,
560
784
  alembic_output=alembic_output,
561
785
  )
786
+ _emit_alembic_script_context(config, target_revision=target_revision)
562
787
  _emit_status(f"Starting Alembic downgrade now target={target_revision}...")
563
788
  with _forward_alembic_logging():
564
789
  command.downgrade(config, target_revision)
@@ -610,8 +610,9 @@ class ManagedMetaTableReservationTable(MetaTableRequestFields):
610
610
  table_contract: MetaTableContract | dict[str, Any] = Field(
611
611
  ...,
612
612
  description=(
613
- "Relational table contract used to reserve physical, index, and "
614
- "foreign-key names before Alembic renders SQL."
613
+ "Relational table contract used to reserve the physical table name "
614
+ "before Alembic renders SQL. Index and foreign-key names remain "
615
+ "client-authored or database-authored DDL metadata."
615
616
  ),
616
617
  )
617
618
  time_index_name: str | None = Field(
@@ -667,8 +668,9 @@ class ManagedMetaTableReservationItem(BasePydanticModel):
667
668
  table_contract: dict[str, Any] = Field(
668
669
  ...,
669
670
  description=(
670
- "Backend-normalized contract containing resolved physical table, "
671
- "index, and foreign-key names."
671
+ "Backend-normalized contract containing the resolved physical table "
672
+ "name. Index and foreign-key names are preserved only when authored "
673
+ "or observed; TS Manager does not generate them."
672
674
  ),
673
675
  )
674
676
  schema_management: dict[str, Any] | None = Field(
@@ -1415,6 +1417,28 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1415
1417
  raise_for_response(response, payload=request_payload)
1416
1418
  return response.json()
1417
1419
 
1420
+ @classmethod
1421
+ def filter_by_body(
1422
+ cls,
1423
+ *,
1424
+ timeout: int | float | tuple[float, float] | None = None,
1425
+ **filters: Any,
1426
+ ) -> list[MetaTable]:
1427
+ response_json = cls._post_action(
1428
+ "filter",
1429
+ filters,
1430
+ timeout=timeout,
1431
+ expected_statuses=(200,),
1432
+ )
1433
+ if isinstance(response_json, dict) and isinstance(
1434
+ response_json.get("results"),
1435
+ list,
1436
+ ):
1437
+ return [cls(**item) for item in response_json["results"]]
1438
+ if isinstance(response_json, list):
1439
+ return [cls(**item) for item in response_json]
1440
+ raise TypeError("MetaTable.filter_by_body expected a list or paginated response.")
1441
+
1418
1442
  def _post_detail_action(
1419
1443
  self,
1420
1444
  action_name: str,