mainsequence 4.2.39__tar.gz → 4.2.40__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.39/mainsequence.egg-info → mainsequence-4.2.40}/PKG-INFO +1 -1
  2. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +6 -2
  3. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/migrations.py +28 -0
  4. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/sqlalchemy_contracts.py +49 -9
  5. {mainsequence-4.2.39 → mainsequence-4.2.40/mainsequence.egg-info}/PKG-INFO +1 -1
  6. {mainsequence-4.2.39 → mainsequence-4.2.40}/pyproject.toml +1 -1
  7. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_meta_table_migrations.py +67 -1
  8. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_meta_tables_sqlalchemy_contracts.py +64 -5
  9. {mainsequence-4.2.39 → mainsequence-4.2.40}/LICENSE +0 -0
  10. {mainsequence-4.2.39 → mainsequence-4.2.40}/README.md +0 -0
  11. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/AGENTS.md +0 -0
  12. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  13. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  14. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  15. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  16. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  17. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  18. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  19. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  20. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  21. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  22. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  23. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  24. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  25. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  26. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  27. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  28. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  29. {mainsequence-4.2.39 → mainsequence-4.2.40}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  30. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/__init__.py +0 -0
  31. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/__main__.py +0 -0
  32. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/bootstrap.py +0 -0
  33. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/__init__.py +0 -0
  34. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/api.py +0 -0
  35. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/browser_auth.py +0 -0
  36. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/cli.py +0 -0
  37. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/config.py +0 -0
  38. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/docker_utils.py +0 -0
  39. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/doctor.py +0 -0
  40. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/local_ops.py +0 -0
  41. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/migrations.py +0 -0
  42. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/model_filters.py +0 -0
  43. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/project_status.py +0 -0
  44. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/pydantic_cli.py +0 -0
  45. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/sdk_utils.py +0 -0
  46. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/ssh_utils.py +0 -0
  47. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/cli/ui.py +0 -0
  48. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/__init__.py +0 -0
  49. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/agent_runtime_models.py +0 -0
  50. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/base.py +0 -0
  51. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/client.py +0 -0
  52. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/__init__.py +0 -0
  53. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/app_component.py +0 -0
  54. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/connections.py +0 -0
  55. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/data_models.py +0 -0
  56. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/workspace.py +0 -0
  57. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  58. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/compute_validation.py +0 -0
  59. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  60. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  61. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  62. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  63. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/dtype_codec.py +0 -0
  64. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/exceptions.py +0 -0
  65. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/fastapi/__init__.py +0 -0
  66. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/fastapi/auth.py +0 -0
  67. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/metatables/__init__.py +0 -0
  68. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/metatables/core.py +0 -0
  69. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/models_foundry.py +0 -0
  70. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/models_helpers.py +0 -0
  71. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/models_user.py +0 -0
  72. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/client/utils.py +0 -0
  73. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/defaults.py +0 -0
  74. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/instrumentation/__init__.py +0 -0
  75. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/instrumentation/utils.py +0 -0
  76. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/logconf.py +0 -0
  77. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/__init__.py +0 -0
  78. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/__main__.py +0 -0
  79. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  80. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  81. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  82. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  83. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  84. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  85. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  86. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  87. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  88. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  89. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/future_registry.py +0 -0
  90. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/hashing.py +0 -0
  91. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  92. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/meta_tables/schema_names.py +0 -0
  93. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence/runtime_flags.py +0 -0
  94. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence.egg-info/SOURCES.txt +0 -0
  95. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence.egg-info/dependency_links.txt +0 -0
  96. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence.egg-info/entry_points.txt +0 -0
  97. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence.egg-info/requires.txt +0 -0
  98. {mainsequence-4.2.39 → mainsequence-4.2.40}/mainsequence.egg-info/top_level.txt +0 -0
  99. {mainsequence-4.2.39 → mainsequence-4.2.40}/setup.cfg +0 -0
  100. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_auth_precedence.py +0 -0
  101. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_build_operations_hashing.py +0 -0
  102. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_cli.py +0 -0
  103. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_cli_browser_auth.py +0 -0
  104. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_cli_migrations.py +0 -0
  105. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_client.py +0 -0
  106. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_command_center_app_component_models.py +0 -0
  107. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_command_center_data_models.py +0 -0
  108. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_command_center_models.py +0 -0
  109. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  110. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_data_node_storage_dimension_queries.py +0 -0
  111. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_data_node_update_flow.py +0 -0
  112. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_dependency_extras.py +0 -0
  113. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_duckdb_interface_dimensions.py +0 -0
  114. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_filter_normalization.py +0 -0
  115. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_logconf.py +0 -0
  116. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_meta_tables_client_models.py +0 -0
  117. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_models_user_request_bound_auth.py +0 -0
  118. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_pod_project_resolution.py +0 -0
  119. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_project_batch_jobs_from_file.py +0 -0
  120. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_run_configuration.py +0 -0
  121. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_schema_names.py +0 -0
  122. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_secret_client_model.py +0 -0
  123. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_source_table_configuration.py +0 -0
  124. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_sqlite_interface_dimensions.py +0 -0
  125. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_update_runner_uid_runtime.py +0 -0
  126. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_update_statistics.py +0 -0
  127. {mainsequence-4.2.39 → mainsequence-4.2.40}/tests/test_update_uid_guards.py +0 -0
  128. {mainsequence-4.2.39 → mainsequence-4.2.40}/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.39
3
+ Version: 4.2.40
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
@@ -137,7 +137,11 @@ first version, use Alembic. Keep the SDK model as a normal
137
137
  `PlatformManagedMetaTable` or `PlatformTimeIndexMetaData` catalog contract, and
138
138
  apply physical schema changes through the Alembic migration workflow.
139
139
 
140
- Schema must come from SQLAlchemy table metadata, usually `__table_args__ = {"schema": "public"}` or the tuple form ending in `{"schema": ...}`. Do not add a separate MetaTable-specific schema attribute.
140
+ Default-schema tables must leave SQLAlchemy `Table.schema` unset; do not write
141
+ `__table_args__ = {"schema": "public"}` for the default PostgreSQL schema. Set
142
+ schema metadata only for non-default schemas, using `__table_args__ = {"schema":
143
+ "custom_schema"}` or the tuple form ending in `{"schema": ...}`. Do not add a
144
+ separate MetaTable-specific schema attribute.
141
145
 
142
146
  Always declare `__metatable_description__` on the model. The description must
143
147
  explain the table's business intention, row grain, and expected use, not only
@@ -246,7 +250,7 @@ Use this pattern:
246
250
  ```python
247
251
  account_uid: Mapped[uuid.UUID] = mapped_column(
248
252
  Uuid,
249
- ForeignKey("public.sdk_examples__account.uid", ondelete="RESTRICT"),
253
+ ForeignKey("sdk_examples__account.uid", ondelete="RESTRICT"),
250
254
  nullable=False,
251
255
  )
252
256
  ```
@@ -27,9 +27,14 @@ from mainsequence.meta_tables.hashing import build_meta_table_storage_hash
27
27
  from mainsequence.meta_tables.sqlalchemy_contracts import (
28
28
  PlatformManagedMetaTable,
29
29
  PlatformTimeIndexMetaData,
30
+ _ensure_time_index_unique_grain_index,
31
+ _normalize_table_default_schema,
30
32
  _resolve_model_data_source_uid,
31
33
  _resolve_table,
34
+ _resolve_time_index_name,
35
+ _resolve_time_index_names,
32
36
  _table_name,
37
+ _validate_time_index_contract,
33
38
  platform_managed_migration_registration_context,
34
39
  )
35
40
 
@@ -268,6 +273,8 @@ class AlembicMetaTableMigration:
268
273
  self.after_register_metatables
269
274
  ):
270
275
  raise TypeError("after_register_metatables must be callable when provided.")
276
+ _normalize_provider_default_schemas(self.metatable_models)
277
+ _ensure_provider_time_index_grain_indexes(self.metatable_models)
271
278
 
272
279
  @property
273
280
  def alembic_version_table(self) -> str:
@@ -956,6 +963,27 @@ def _metadata_table_names(target_metadata: Any) -> list[str]:
956
963
  return list(dict.fromkeys(names))
957
964
 
958
965
 
966
+ def _normalize_provider_default_schemas(models: Sequence[type[Any]]) -> None:
967
+ for model in models:
968
+ if _is_platform_managed_metatable_model(model):
969
+ _normalize_table_default_schema(_resolve_table(model))
970
+
971
+
972
+ def _ensure_provider_time_index_grain_indexes(models: Sequence[type[Any]]) -> None:
973
+ for model in models:
974
+ if not _is_platform_time_index_metatable_model(model):
975
+ continue
976
+ table = _resolve_table(model)
977
+ time_index_name = _resolve_time_index_name(model)
978
+ index_names = _resolve_time_index_names(model, time_index_name=time_index_name)
979
+ _validate_time_index_contract(
980
+ columns=list(table.columns),
981
+ time_index_name=time_index_name,
982
+ index_names=index_names,
983
+ )
984
+ _ensure_time_index_unique_grain_index(table=table, index_names=index_names)
985
+
986
+
959
987
  def _normalize_down_revision(value: Any) -> str | None:
960
988
  if value in (None, ""):
961
989
  return None
@@ -11,6 +11,7 @@ from uuid import UUID
11
11
  from sqlalchemy import Table as _SQLAlchemyTable
12
12
  from sqlalchemy import inspect as _sqlalchemy_inspect
13
13
  from sqlalchemy.orm import declared_attr as _sqlalchemy_declared_attr
14
+ from sqlalchemy.schema import BLANK_SCHEMA as _SQLALCHEMY_BLANK_SCHEMA
14
15
 
15
16
  from mainsequence.client.dtype_codec import (
16
17
  is_temporal_token,
@@ -35,6 +36,7 @@ DEFAULT_PLATFORM_MANAGED_PROVISIONING = {
35
36
  "if_not_exists": True,
36
37
  }
37
38
  SERVER_GENERATED_UUID_DEFAULT = "gen_random_uuid()"
39
+ DEFAULT_POSTGRES_SCHEMA = "public"
38
40
  _PLATFORM_MANAGED_MIGRATION_REGISTRATION_CONTEXT: contextvars.ContextVar[bool] = (
39
41
  contextvars.ContextVar(
40
42
  "mainsequence_platform_managed_migration_registration_context",
@@ -153,8 +155,14 @@ class PlatformManagedMetaTable:
153
155
 
154
156
  name, metadata, *table_items = args
155
157
  kwargs = dict(kwargs)
156
- schema = str(kwargs.get("schema") or _resolve_class_schema(cls, metadata=metadata))
157
- if not kwargs.get("schema"):
158
+ schema = _normalize_sqlalchemy_schema(
159
+ kwargs.get("schema")
160
+ if "schema" in kwargs
161
+ else _resolve_class_schema(cls, metadata=metadata)
162
+ )
163
+ if schema is None:
164
+ kwargs["schema"] = _SQLALCHEMY_BLANK_SCHEMA
165
+ else:
158
166
  kwargs["schema"] = schema
159
167
 
160
168
  from sqlalchemy import Table
@@ -308,8 +316,14 @@ class PlatformTimeIndexMetaData(PlatformManagedMetaTable):
308
316
 
309
317
  name, metadata, *table_items = args
310
318
  kwargs = dict(kwargs)
311
- schema = str(kwargs.get("schema") or _resolve_class_schema(cls, metadata=metadata))
312
- if not kwargs.get("schema"):
319
+ schema = _normalize_sqlalchemy_schema(
320
+ kwargs.get("schema")
321
+ if "schema" in kwargs
322
+ else _resolve_class_schema(cls, metadata=metadata)
323
+ )
324
+ if schema is None:
325
+ kwargs["schema"] = _SQLALCHEMY_BLANK_SCHEMA
326
+ else:
313
327
  kwargs["schema"] = schema
314
328
 
315
329
  columns = [item for item in table_items if _looks_like_column(item)]
@@ -416,7 +430,7 @@ def table_contract_from_sqlalchemy_model(
416
430
  include_physical_table_name: bool = True,
417
431
  ) -> MetaTableContract:
418
432
  table = _resolve_table(model_or_table)
419
- _resolve_schema(table, schema=schema)
433
+ resolved_schema = _resolve_schema(table, schema=schema)
420
434
  module, qualname = _resolve_model_path(
421
435
  model_or_table,
422
436
  table_model_module=table_model_module,
@@ -433,6 +447,7 @@ def table_contract_from_sqlalchemy_model(
433
447
  }
434
448
  },
435
449
  physical=MetaTablePhysicalContract(
450
+ schema_=resolved_schema,
436
451
  table_name=_table_name(table) if include_physical_table_name else None,
437
452
  ),
438
453
  columns=[
@@ -563,7 +578,10 @@ def time_indexed_registration_request_from_sqlalchemy_model(
563
578
  ),
564
579
  },
565
580
  },
566
- "physical": {"table_name": _table_name(table)},
581
+ "physical": {
582
+ "schema": resolved_schema,
583
+ "table_name": _table_name(table),
584
+ },
567
585
  "columns": column_contracts,
568
586
  },
569
587
  )
@@ -744,7 +762,7 @@ def _table_name(table: Any) -> str:
744
762
  def _resolve_schema(table: Any, *, schema: str | None = None) -> str:
745
763
  resolved_schema = schema or getattr(table, "schema", None)
746
764
  if not resolved_schema:
747
- raise ValueError("MetaTable SQLAlchemy contracts require a SQLAlchemy table schema.")
765
+ return DEFAULT_POSTGRES_SCHEMA
748
766
  return str(resolved_schema)
749
767
 
750
768
 
@@ -755,7 +773,7 @@ def _resolve_class_namespace(cls: type[Any]) -> str:
755
773
  return str(resolved_namespace)
756
774
 
757
775
 
758
- def _resolve_class_schema(cls: type[Any], *, metadata: Any | None = None) -> str:
776
+ def _resolve_class_schema(cls: type[Any], *, metadata: Any | None = None) -> str | None:
759
777
  table_args = getattr(cls, "__table_args__", None)
760
778
  if isinstance(table_args, Mapping):
761
779
  schema = table_args.get("schema")
@@ -765,7 +783,29 @@ def _resolve_class_schema(cls: type[Any], *, metadata: Any | None = None) -> str
765
783
  schema = None
766
784
  if schema is None and metadata is not None:
767
785
  schema = getattr(metadata, "schema", None)
768
- return str(schema or "public")
786
+ return _normalize_sqlalchemy_schema(schema)
787
+
788
+
789
+ def _normalize_sqlalchemy_schema(schema: Any | None) -> str | None:
790
+ if schema is None:
791
+ return None
792
+ schema_name = str(schema).strip()
793
+ if schema_name in ("", DEFAULT_POSTGRES_SCHEMA):
794
+ return None
795
+ return schema_name
796
+
797
+
798
+ def _normalize_table_default_schema(table: Any) -> None:
799
+ if getattr(table, "schema", None) != DEFAULT_POSTGRES_SCHEMA:
800
+ return
801
+
802
+ metadata = getattr(table, "metadata", None)
803
+ if metadata is not None:
804
+ metadata._remove_table(table.name, table.schema)
805
+ table.schema = None
806
+ table.fullname = _table_name(table)
807
+ if metadata is not None:
808
+ metadata._add_table(table.name, table.schema, table)
769
809
 
770
810
 
771
811
  def _resolve_data_source_uid(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.2.39
3
+ Version: 4.2.40
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.2.39"
7
+ version = "4.2.40"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -16,7 +16,11 @@ from mainsequence.client.metatables import (
16
16
  MetaTable,
17
17
  TimeIndexMetaData,
18
18
  )
19
- from mainsequence.meta_tables import PlatformManagedMetaTable, PlatformTimeIndexMetaData
19
+ from mainsequence.meta_tables import (
20
+ PlatformManagedMetaTable,
21
+ PlatformTimeIndexMetaData,
22
+ schema_index_name,
23
+ )
20
24
  from mainsequence.meta_tables.migrations import (
21
25
  DEFAULT_ALEMBIC_VERSION_COLUMN_NAME,
22
26
  DEFAULT_ALEMBIC_VERSION_IDENTIFIER,
@@ -1005,6 +1009,68 @@ def test_prepare_for_alembic_routes_time_indexed_models_to_dynamic_table_bulk_cr
1005
1009
  assert "schema_management" not in row
1006
1010
 
1007
1011
 
1012
+ def test_provider_adds_time_index_grain_index_when_table_cls_is_overridden():
1013
+ class Base(DeclarativeBase):
1014
+ metadata = MetaData(schema="public")
1015
+
1016
+ class CustomTimeIndexMixin(PlatformTimeIndexMetaData):
1017
+ __abstract__ = True
1018
+
1019
+ @classmethod
1020
+ def __table_cls__(cls, *args, **kwargs):
1021
+ name, metadata, *table_items = args
1022
+ return Table(str(name), metadata, *table_items, **kwargs)
1023
+
1024
+ class ProjectAlembicVersion(AlembicVersionMetaTable):
1025
+ __metatable_uid__ = "registry-meta-table-uid"
1026
+ __metatable_data_source_uid__ = "data-source-uid"
1027
+
1028
+ class Prices(CustomTimeIndexMixin, Base):
1029
+ __tablename__ = "example_assets__prices"
1030
+ __metatable_data_source_uid__ = "data-source-uid"
1031
+ __metatable_namespace__ = "example.assets"
1032
+ __metatable_identifier__ = "Prices"
1033
+ __time_index_name__ = "time_index"
1034
+ __index_names__ = ["time_index", "asset_identifier"]
1035
+
1036
+ time_index: Mapped[datetime.datetime] = mapped_column(
1037
+ DateTime(timezone=True),
1038
+ nullable=False,
1039
+ )
1040
+ asset_identifier: Mapped[str] = mapped_column(String(255), nullable=False)
1041
+ close: Mapped[int] = mapped_column(Integer, nullable=True)
1042
+
1043
+ assert not Prices.__table__.indexes
1044
+ assert Prices.__table__.schema == "public"
1045
+ assert "public.example_assets__prices" in Base.metadata.tables
1046
+
1047
+ AlembicMetaTableMigration(
1048
+ package="sample",
1049
+ migration_namespace="markets",
1050
+ script_location="sample:migrations",
1051
+ target_metadata=Base.metadata,
1052
+ alembic_registry=ProjectAlembicVersion,
1053
+ metatable_models=[Prices],
1054
+ )
1055
+
1056
+ assert Prices.__table__.schema is None
1057
+ assert "example_assets__prices" in Base.metadata.tables
1058
+ assert "public.example_assets__prices" not in Base.metadata.tables
1059
+
1060
+ grain_indexes = [
1061
+ index
1062
+ for index in Prices.__table__.indexes
1063
+ if index.unique
1064
+ and [column.name for column in index.columns] == ["time_index", "asset_identifier"]
1065
+ ]
1066
+ assert len(grain_indexes) == 1
1067
+ assert grain_indexes[0].name == schema_index_name(
1068
+ "example_assets__prices",
1069
+ ["time_index", "asset_identifier"],
1070
+ unique=True,
1071
+ )
1072
+
1073
+
1008
1074
  def test_prepare_for_alembic_reserves_existing_table_name_with_provider_identity(monkeypatch):
1009
1075
  class Base(DeclarativeBase):
1010
1076
  metadata = MetaData()
@@ -710,6 +710,63 @@ def test_platform_managed_schema_resolves_from_sqlalchemy_table_args_only():
710
710
  assert sqlalchemy_contracts._resolve_class_schema(Account) == "table_args_schema"
711
711
 
712
712
 
713
+ def test_platform_managed_default_public_schema_stays_sqlalchemy_default():
714
+ pytest.importorskip("sqlalchemy")
715
+
716
+ from sqlalchemy import MetaData
717
+ from sqlalchemy import Uuid as SQLAlchemyUuid
718
+ from sqlalchemy.orm import DeclarativeBase, mapped_column
719
+
720
+ class Base(DeclarativeBase):
721
+ metadata = MetaData()
722
+
723
+ class Account(PlatformManagedMetaTable, Base):
724
+ __tablename__ = "example_assets__account"
725
+ __table_args__ = {"schema": "public"}
726
+ __metatable_namespace__ = "example.assets"
727
+ __metatable_identifier__ = "Account"
728
+
729
+ uid: Mapped[uuid.UUID] = mapped_column(SQLAlchemyUuid, primary_key=True)
730
+
731
+ assert Account.__table__.schema is None
732
+ assert sqlalchemy_contracts._resolve_schema(Account.__table__) == "public"
733
+
734
+ request = Account.build_registration_request(
735
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
736
+ )
737
+
738
+ assert request.table_contract.physical.schema_ == "public"
739
+
740
+
741
+ def test_platform_managed_default_public_metadata_schema_stays_sqlalchemy_default():
742
+ pytest.importorskip("sqlalchemy")
743
+
744
+ from sqlalchemy import MetaData
745
+ from sqlalchemy import Uuid as SQLAlchemyUuid
746
+ from sqlalchemy.orm import DeclarativeBase, mapped_column
747
+
748
+ class Base(DeclarativeBase):
749
+ metadata = MetaData(schema="public")
750
+
751
+ class Account(PlatformManagedMetaTable, Base):
752
+ __tablename__ = "example_assets__account"
753
+ __metatable_namespace__ = "example.assets"
754
+ __metatable_identifier__ = "Account"
755
+
756
+ uid: Mapped[uuid.UUID] = mapped_column(SQLAlchemyUuid, primary_key=True)
757
+
758
+ assert Account.__table__.schema is None
759
+ assert Account.__table__.fullname == "example_assets__account"
760
+ assert "example_assets__account" in Base.metadata.tables
761
+ assert "public.example_assets__account" not in Base.metadata.tables
762
+
763
+ request = Account.build_registration_request(
764
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
765
+ )
766
+
767
+ assert request.table_contract.physical.schema_ == "public"
768
+
769
+
713
770
  def test_time_index_optional_table_info_resolvers_allow_pending_declarative_class():
714
771
  class PendingTimeIndexTable:
715
772
  __time_index_name__ = "time_index"
@@ -1663,14 +1720,15 @@ def test_platform_managed_register_preserves_authored_sqlalchemy_table_name(
1663
1720
  assert Account.get_storage_hash() == storage_hash
1664
1721
  assert Account.get_physical_table_name() == "example_assets__account"
1665
1722
  assert Account.__table__.name == "example_assets__account"
1666
- assert Account.__table__.fullname == "public.example_assets__account"
1667
- assert Base.metadata.tables["public.example_assets__account"] is Account.__table__
1723
+ assert Account.__table__.schema is None
1724
+ assert Account.__table__.fullname == "example_assets__account"
1725
+ assert Base.metadata.tables["example_assets__account"] is Account.__table__
1668
1726
  assert f"public.{physical_table_name}" not in Base.metadata.tables
1669
1727
 
1670
1728
  compiled_sql = str(
1671
1729
  select(Account.__table__).compile(dialect=postgresql.dialect(paramstyle="pyformat"))
1672
1730
  )
1673
- assert "FROM public.example_assets__account" in compiled_sql
1731
+ assert "FROM example_assets__account" in compiled_sql
1674
1732
  assert physical_table_name not in compiled_sql
1675
1733
  assert storage_hash not in compiled_sql
1676
1734
 
@@ -1702,7 +1760,8 @@ def test_bound_parent_table_foreign_key_stays_sqlalchemy_only():
1702
1760
  )
1703
1761
  )
1704
1762
 
1705
- assert Account.__table__.fullname == "public.example_assets__account"
1763
+ assert Account.__table__.schema is None
1764
+ assert Account.__table__.fullname == "example_assets__account"
1706
1765
 
1707
1766
  class Asset(PlatformManagedMetaTable, Base):
1708
1767
  __tablename__ = "example_assets__asset"
@@ -1712,7 +1771,7 @@ def test_bound_parent_table_foreign_key_stays_sqlalchemy_only():
1712
1771
  uid: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True)
1713
1772
  account_uid: Mapped[uuid.UUID] = mapped_column(
1714
1773
  Uuid,
1715
- ForeignKey("public.example_assets__account.uid", ondelete="RESTRICT"),
1774
+ ForeignKey("example_assets__account.uid", ondelete="RESTRICT"),
1716
1775
  nullable=False,
1717
1776
  )
1718
1777
  symbol: Mapped[str] = mapped_column(String(64), nullable=False)
File without changes
File without changes
File without changes