metaxy 0.0.0__py3-none-any.whl

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 metaxy might be problematic. Click here for more details.

Files changed (75) hide show
  1. metaxy/__init__.py +61 -0
  2. metaxy/_testing.py +542 -0
  3. metaxy/_utils.py +16 -0
  4. metaxy/_version.py +1 -0
  5. metaxy/cli/app.py +76 -0
  6. metaxy/cli/context.py +71 -0
  7. metaxy/cli/graph.py +576 -0
  8. metaxy/cli/graph_diff.py +290 -0
  9. metaxy/cli/list.py +42 -0
  10. metaxy/cli/metadata.py +271 -0
  11. metaxy/cli/migrations.py +862 -0
  12. metaxy/cli/push.py +55 -0
  13. metaxy/config.py +450 -0
  14. metaxy/data_versioning/__init__.py +24 -0
  15. metaxy/data_versioning/calculators/__init__.py +13 -0
  16. metaxy/data_versioning/calculators/base.py +97 -0
  17. metaxy/data_versioning/calculators/duckdb.py +186 -0
  18. metaxy/data_versioning/calculators/ibis.py +225 -0
  19. metaxy/data_versioning/calculators/polars.py +135 -0
  20. metaxy/data_versioning/diff/__init__.py +15 -0
  21. metaxy/data_versioning/diff/base.py +150 -0
  22. metaxy/data_versioning/diff/narwhals.py +108 -0
  23. metaxy/data_versioning/hash_algorithms.py +19 -0
  24. metaxy/data_versioning/joiners/__init__.py +9 -0
  25. metaxy/data_versioning/joiners/base.py +70 -0
  26. metaxy/data_versioning/joiners/narwhals.py +235 -0
  27. metaxy/entrypoints.py +309 -0
  28. metaxy/ext/__init__.py +1 -0
  29. metaxy/ext/alembic.py +326 -0
  30. metaxy/ext/sqlmodel.py +172 -0
  31. metaxy/ext/sqlmodel_system_tables.py +139 -0
  32. metaxy/graph/__init__.py +21 -0
  33. metaxy/graph/diff/__init__.py +21 -0
  34. metaxy/graph/diff/diff_models.py +399 -0
  35. metaxy/graph/diff/differ.py +740 -0
  36. metaxy/graph/diff/models.py +418 -0
  37. metaxy/graph/diff/rendering/__init__.py +18 -0
  38. metaxy/graph/diff/rendering/base.py +274 -0
  39. metaxy/graph/diff/rendering/cards.py +188 -0
  40. metaxy/graph/diff/rendering/formatter.py +805 -0
  41. metaxy/graph/diff/rendering/graphviz.py +246 -0
  42. metaxy/graph/diff/rendering/mermaid.py +320 -0
  43. metaxy/graph/diff/rendering/rich.py +165 -0
  44. metaxy/graph/diff/rendering/theme.py +48 -0
  45. metaxy/graph/diff/traversal.py +247 -0
  46. metaxy/graph/utils.py +58 -0
  47. metaxy/metadata_store/__init__.py +31 -0
  48. metaxy/metadata_store/_protocols.py +38 -0
  49. metaxy/metadata_store/base.py +1676 -0
  50. metaxy/metadata_store/clickhouse.py +161 -0
  51. metaxy/metadata_store/duckdb.py +167 -0
  52. metaxy/metadata_store/exceptions.py +43 -0
  53. metaxy/metadata_store/ibis.py +451 -0
  54. metaxy/metadata_store/memory.py +228 -0
  55. metaxy/metadata_store/sqlite.py +187 -0
  56. metaxy/metadata_store/system_tables.py +257 -0
  57. metaxy/migrations/__init__.py +34 -0
  58. metaxy/migrations/detector.py +153 -0
  59. metaxy/migrations/executor.py +208 -0
  60. metaxy/migrations/loader.py +260 -0
  61. metaxy/migrations/models.py +718 -0
  62. metaxy/migrations/ops.py +390 -0
  63. metaxy/models/__init__.py +0 -0
  64. metaxy/models/bases.py +6 -0
  65. metaxy/models/constants.py +24 -0
  66. metaxy/models/feature.py +665 -0
  67. metaxy/models/feature_spec.py +105 -0
  68. metaxy/models/field.py +25 -0
  69. metaxy/models/plan.py +155 -0
  70. metaxy/models/types.py +157 -0
  71. metaxy/py.typed +0 -0
  72. metaxy-0.0.0.dist-info/METADATA +247 -0
  73. metaxy-0.0.0.dist-info/RECORD +75 -0
  74. metaxy-0.0.0.dist-info/WHEEL +4 -0
  75. metaxy-0.0.0.dist-info/entry_points.txt +3 -0
metaxy/entrypoints.py ADDED
@@ -0,0 +1,309 @@
1
+ """Entrypoint discovery and loading for Metaxy features.
2
+
3
+ This module provides functionality to automatically discover and load Feature
4
+ classes from modules, supporting both:
5
+ - Config-based entrypoints (list of module paths)
6
+ - Environment-based entrypoints (environment variables starting with METAXY_ENTRYPOINT)
7
+ - Package-based entrypoints (via importlib.metadata)
8
+
9
+ Features are automatically registered to the active FeatureGraph when their
10
+ containing modules are imported (via the Feature metaclass).
11
+ """
12
+
13
+ import importlib
14
+ import logging
15
+ import os
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from metaxy.models.feature import FeatureGraph
20
+
21
+ # Conditional import for Python 3.10+ compatibility
22
+ from importlib.metadata import entry_points # type: ignore[import-not-found]
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Default entry point group name for package-based discovery
27
+ DEFAULT_ENTRY_POINT_GROUP = "metaxy.features"
28
+
29
+
30
+ class EntrypointLoadError(Exception):
31
+ """Raised when an entrypoint fails to load."""
32
+
33
+ pass
34
+
35
+
36
+ def load_module_entrypoint(
37
+ module_path: str,
38
+ *,
39
+ graph: "FeatureGraph | None" = None,
40
+ ) -> None:
41
+ """Load a single module entrypoint.
42
+
43
+ Imports the specified module, which should contain Feature class definitions.
44
+ Features will be automatically registered to the active graph via the
45
+ Feature metaclass.
46
+
47
+ Args:
48
+ module_path: Fully qualified module path (e.g., "myapp.features.video")
49
+ graph: Target graph. If None, uses FeatureGraph.get_active()
50
+
51
+ Raises:
52
+ EntrypointLoadError: If module import fails
53
+
54
+ Example:
55
+ >>> from metaxy.entrypoints import load_module_entrypoint
56
+ >>> load_module_entrypoint("myapp.features.core")
57
+ >>> # Features from myapp.features.core are now registered
58
+ """
59
+ from metaxy.models.feature import FeatureGraph
60
+
61
+ target_graph = graph or FeatureGraph.get_active()
62
+
63
+ try:
64
+ # Set graph as active during import so Features register to it
65
+ with target_graph.use():
66
+ logger.debug(f"Loading entrypoint module: {module_path}")
67
+ importlib.import_module(module_path)
68
+ logger.info(f"Successfully loaded entrypoint: {module_path}")
69
+ except ImportError as e:
70
+ raise EntrypointLoadError(
71
+ f"Failed to import entrypoint module '{module_path}': {e}"
72
+ ) from e
73
+ except Exception as e:
74
+ raise EntrypointLoadError(
75
+ f"Error loading entrypoint module '{module_path}': {e}"
76
+ ) from e
77
+
78
+
79
+ def load_entrypoints(
80
+ entrypoints: list[str],
81
+ *,
82
+ graph: "FeatureGraph | None" = None,
83
+ ) -> None:
84
+ """Load multiple module entrypoints from a list.
85
+
86
+ Args:
87
+ entrypoints: List of module paths to import
88
+ graph: Target graph. If None, uses FeatureGraph.get_active()
89
+
90
+ Raises:
91
+ EntrypointLoadError: If any module import fails
92
+
93
+ Example:
94
+ >>> from metaxy.entrypoints import load_config_entrypoints
95
+ >>> load_config_entrypoints([
96
+ ... "myapp.features.video",
97
+ ... "myapp.features.audio",
98
+ ... "myapp.features.text"
99
+ ... ])
100
+ """
101
+ from metaxy.models.feature import FeatureGraph
102
+
103
+ target_graph = graph or FeatureGraph.get_active()
104
+
105
+ logger.info(f"Loading {len(entrypoints)} entrypoints")
106
+
107
+ for module_path in entrypoints:
108
+ load_module_entrypoint(module_path, graph=target_graph)
109
+
110
+
111
+ def load_package_entrypoints(
112
+ group: str = DEFAULT_ENTRY_POINT_GROUP,
113
+ *,
114
+ graph: "FeatureGraph | None" = None,
115
+ ) -> None:
116
+ """Load entrypoints from installed packages using importlib.metadata.
117
+
118
+ Discovers and loads all entry points registered in the specified group.
119
+ This is the package-based entrypoint mechanism using standard Python
120
+ packaging infrastructure.
121
+
122
+ Packages declare entrypoints in their pyproject.toml:
123
+ [project.entry-points."metaxy.features"]
124
+ myfeature = "mypackage.features.module"
125
+
126
+ Args:
127
+ group: Entry point group name (default: "metaxy.features")
128
+ graph: Target graph. If None, uses FeatureGraph.get_active()
129
+
130
+ Raises:
131
+ EntrypointLoadError: If any entrypoint fails to load
132
+
133
+ Example:
134
+ >>> from metaxy.entrypoints import load_package_entrypoints
135
+ >>> # Discover and load all installed plugins
136
+ >>> load_package_entrypoints()
137
+ """
138
+ from metaxy.models.feature import FeatureGraph
139
+
140
+ target_graph = graph or FeatureGraph.get_active()
141
+
142
+ logger.info(f"Discovering package entrypoints in group: {group}")
143
+
144
+ # Discover entry points
145
+ # Note: Python 3.10+ returns SelectableGroups, 3.9 returns dict
146
+ discovered = entry_points()
147
+
148
+ # Handle different return types across Python versions
149
+ if hasattr(discovered, "select"):
150
+ # Python 3.10+: SelectableGroups with select() method
151
+ eps = discovered.select(group=group)
152
+ else:
153
+ # Python 3.9: dict-like interface
154
+ eps = discovered.get(group, [])
155
+
156
+ eps_list = list(eps)
157
+
158
+ if not eps_list:
159
+ logger.debug(f"No package entrypoints found in group: {group}")
160
+ return
161
+
162
+ logger.info(f"Found {len(eps_list)} package entrypoints in group: {group}")
163
+
164
+ for ep in eps_list:
165
+ try:
166
+ logger.debug(f"Loading package entrypoint: {ep.name} = {ep.value}")
167
+ # Load the entry point (imports the module)
168
+ with target_graph.use():
169
+ ep.load()
170
+ logger.info(f"Successfully loaded package entrypoint: {ep.name}")
171
+ except Exception as e:
172
+ raise EntrypointLoadError(
173
+ f"Failed to load package entrypoint '{ep.name}' ({ep.value}): {e}"
174
+ ) from e
175
+
176
+
177
+ def load_env_entrypoints() -> None:
178
+ """Load entrypoints from environment variables.
179
+
180
+ Discovers and loads all entry points from environment variables matching
181
+ the pattern METAXY_ENTRYPOINT*. Each variable should contain a
182
+ comma-separated list of module paths.
183
+
184
+ Environment variables:
185
+ METAXY_ENTRYPOINT="myapp.features.core,myapp.features.extra"
186
+ METAXY_ENTRYPOINT_PLUGINS="plugin1.features,plugin2.features"
187
+
188
+ Args:
189
+ graph: Target graph. If None, uses FeatureGraph.get_active()
190
+
191
+ Raises:
192
+ EntrypointLoadError: If any entrypoint fails to load
193
+
194
+ Example:
195
+ >>> import os
196
+ >>> os.environ["METAXY_ENTRYPOINT"] = "myapp.features.core"
197
+ >>> from metaxy.entrypoints import load_env_entrypoints
198
+ >>> load_env_entrypoints()
199
+ """
200
+
201
+ logger.info("Discovering environment-based entrypoints (METAXY_ENTRYPOINT*)")
202
+
203
+ # Find all environment variables matching METAXY_ENTRYPOINT*
204
+ env_vars = {
205
+ key: value
206
+ for key, value in os.environ.items()
207
+ if key.startswith("METAXY_ENTRYPOINT")
208
+ }
209
+
210
+ if not env_vars:
211
+ logger.debug("No environment entrypoints found (METAXY_ENTRYPOINT* not set)")
212
+ return
213
+
214
+ logger.info(f"Found {len(env_vars)} METAXY_ENTRYPOINT* environment variable(s)")
215
+
216
+ # Collect all module paths from all matching env vars
217
+ all_module_paths = []
218
+ for env_var, value in sorted(env_vars.items()):
219
+ logger.debug(f"Processing {env_var}={value}")
220
+ # Split by comma and strip whitespace
221
+ module_paths = [path.strip() for path in value.split(",") if path.strip()]
222
+ all_module_paths.extend(module_paths)
223
+
224
+ if not all_module_paths:
225
+ logger.debug("No module paths found in METAXY_ENTRYPOINT* variables")
226
+ return
227
+
228
+ logger.info(
229
+ f"Loading {len(all_module_paths)} module(s) from environment entrypoints"
230
+ )
231
+
232
+ # Load each module path
233
+ for module_path in all_module_paths:
234
+ load_module_entrypoint(module_path)
235
+
236
+
237
+ def load_features(
238
+ entrypoints: list[str] | None = None,
239
+ package_entrypoint_group: str = DEFAULT_ENTRY_POINT_GROUP,
240
+ *,
241
+ load_config: bool = True,
242
+ load_packages: bool = True,
243
+ load_env: bool = True,
244
+ ) -> "FeatureGraph":
245
+ """Discover and load all entrypoints from config, packages, and environment.
246
+
247
+ This is the main entry point for loading features. It combines config-based,
248
+ package-based, and environment-based entrypoint discovery.
249
+
250
+ Args:
251
+ entrypoints: List of module paths (optional)
252
+ package_entrypoint_group: Entry point group for package discovery
253
+ load_config: Whether to load config-based entrypoints (default: True)
254
+ load_packages: Whether to load package-based entrypoints (default: True)
255
+ load_env: Whether to load environment-based entrypoints (default: True)
256
+
257
+ Returns:
258
+ The graph that was populated (useful for testing/inspection)
259
+
260
+ Raises:
261
+ EntrypointLoadError: If any entrypoint fails to load
262
+
263
+ Example:
264
+ >>> from metaxy.entrypoints import load_features
265
+ >>>
266
+ >>> # Load from all sources
267
+ >>> graph = load_features(
268
+ ... entrypoints=["myapp.features.core"],
269
+ ... load_packages=True,
270
+ ... load_env=True
271
+ ... )
272
+ >>>
273
+ >>> # Load only from config
274
+ >>> graph = load_features(
275
+ ... entrypoints=["myapp.features.core"],
276
+ ... load_packages=False,
277
+ ... load_env=False
278
+ ... )
279
+ """
280
+ from metaxy.config import MetaxyConfig
281
+ from metaxy.models.feature import FeatureGraph
282
+
283
+ target_graph = FeatureGraph.get_active()
284
+
285
+ logger.info("Starting entrypoint discovery and loading")
286
+
287
+ # Load explicit entrypoints
288
+ if entrypoints:
289
+ load_entrypoints(entrypoints)
290
+
291
+ # Load config-based entrypoints
292
+ if load_config:
293
+ config = MetaxyConfig.load(search_parents=True)
294
+ load_entrypoints(config.entrypoints)
295
+
296
+ # Load package-based entrypoints
297
+ if load_packages:
298
+ load_package_entrypoints(package_entrypoint_group)
299
+
300
+ # Load environment-based entrypoints
301
+ if load_env:
302
+ load_env_entrypoints()
303
+
304
+ num_features = len(target_graph.features_by_key)
305
+ logger.info(
306
+ f"Entrypoint loading complete. Registry now contains {num_features} features."
307
+ )
308
+
309
+ return target_graph
metaxy/ext/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Integrations with third-party software."""
metaxy/ext/alembic.py ADDED
@@ -0,0 +1,326 @@
1
+ """Alembic integration helpers for metaxy.
2
+
3
+ This module provides utilities for including metaxy system tables
4
+ in Alembic migrations when using SQLModel integration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from sqlalchemy import MetaData
13
+
14
+
15
+ def get_metaxy_metadata() -> MetaData:
16
+ """Get SQLAlchemy metadata containing metaxy system tables.
17
+
18
+ This function returns the metadata object that should be included
19
+ in your Alembic configuration to manage metaxy system tables.
20
+
21
+ Returns:
22
+ SQLAlchemy MetaData containing metaxy system table definitions
23
+
24
+ Raises:
25
+ ImportError: If SQLModel is not installed
26
+
27
+ Example:
28
+ >>> # In your alembic/env.py:
29
+ >>> from metaxy.ext.alembic import get_metaxy_metadata
30
+ >>> from sqlalchemy import MetaData
31
+ >>>
32
+ >>> # Combine your app metadata with metaxy metadata
33
+ >>> target_metadata = MetaData()
34
+ >>> target_metadata.reflect(get_metaxy_metadata())
35
+ >>> target_metadata.reflect(my_app.metadata)
36
+ """
37
+ from metaxy.ext.sqlmodel_system_tables import get_system_metadata
38
+
39
+ return get_system_metadata()
40
+
41
+
42
+ def include_metaxy_tables(target_metadata: MetaData) -> MetaData:
43
+ """Include metaxy system tables in existing metadata.
44
+
45
+ This is a convenience function that adds metaxy system tables
46
+ to your existing SQLAlchemy metadata object.
47
+
48
+ Args:
49
+ target_metadata: Your application's metadata object
50
+
51
+ Returns:
52
+ The same metadata object with metaxy tables added
53
+
54
+ Example:
55
+ >>> # In your alembic/env.py:
56
+ >>> from metaxy.ext.alembic import include_metaxy_tables
57
+ >>> from myapp.models import metadata
58
+ >>>
59
+ >>> # Add metaxy tables to your metadata
60
+ >>> target_metadata = include_metaxy_tables(metadata)
61
+ """
62
+ metaxy_metadata = get_metaxy_metadata()
63
+
64
+ # Copy tables from metaxy metadata to target metadata
65
+ for table_name, table in metaxy_metadata.tables.items():
66
+ if table_name not in target_metadata.tables:
67
+ # Create a new table with the same definition in target metadata
68
+ table.to_metadata(target_metadata)
69
+
70
+ return target_metadata
71
+
72
+
73
+ def check_sqlmodel_enabled() -> bool:
74
+ """Check if SQLModel integration is enabled in the configuration.
75
+
76
+ Returns:
77
+ True if SQLModel integration is enabled, False otherwise
78
+
79
+ Example:
80
+ >>> from metaxy.ext.alembic import check_sqlmodel_enabled
81
+ >>> if check_sqlmodel_enabled():
82
+ ... # SQLModel is enabled, include metaxy tables
83
+ ... include_metaxy_tables(metadata)
84
+ """
85
+ try:
86
+ from metaxy.config import MetaxyConfig
87
+
88
+ config = MetaxyConfig.get()
89
+ # Check if SQLModel configuration exists and is enabled
90
+ # The infer_db_table_names setting indicates SQLModel is being used
91
+ return hasattr(config.ext, "sqlmodel")
92
+ except Exception:
93
+ return False
94
+
95
+
96
+ def generate_alembic_env_template() -> str:
97
+ """Generate a template for Alembic env.py with metaxy integration.
98
+
99
+ Returns:
100
+ String containing example Alembic env.py configuration
101
+
102
+ Example:
103
+ >>> from metaxy.ext.alembic import generate_alembic_env_template
104
+ >>> template = generate_alembic_env_template()
105
+ >>> print(template) # Shows example configuration
106
+ """
107
+ return '''"""Alembic env.py template with metaxy integration.
108
+
109
+ This template shows how to include metaxy system tables in your
110
+ Alembic migrations when using SQLModel integration.
111
+ """
112
+
113
+ from logging.config import fileConfig
114
+
115
+ from sqlalchemy import engine_from_config
116
+ from sqlalchemy import pool
117
+ from alembic import context
118
+
119
+ # Import your models here
120
+ # from myapp import models
121
+
122
+ # Metaxy integration
123
+ from metaxy.ext.alembic import get_metaxy_metadata, include_metaxy_tables
124
+ from metaxy.config import MetaxyConfig
125
+
126
+ # this is the Alembic Config object
127
+ config = context.config
128
+
129
+ # Interpret the config file for Python logging
130
+ if config.config_file_name is not None:
131
+ fileConfig(config.config_file_name)
132
+
133
+ # Load metaxy configuration
134
+ metaxy_config = MetaxyConfig.load()
135
+
136
+ # Add your model's MetaData object here for 'autogenerate' support
137
+ # from myapp import mymodel
138
+ # target_metadata = mymodel.Base.metadata
139
+
140
+ # Option 1: Include metaxy tables in your existing metadata
141
+ # from myapp.models import metadata
142
+ # target_metadata = include_metaxy_tables(metadata)
143
+
144
+ # Option 2: Combine multiple metadata objects
145
+ from sqlalchemy import MetaData
146
+ target_metadata = MetaData()
147
+
148
+ # Add metaxy system tables (if using SQLModel integration)
149
+ if metaxy_config.ext.sqlmodel:
150
+ metaxy_metadata = get_metaxy_metadata()
151
+ for table_name, table in metaxy_metadata.tables.items():
152
+ table.to_metadata(target_metadata)
153
+
154
+ # Add your application tables
155
+ # for table_name, table in myapp_metadata.tables.items():
156
+ # table.to_metadata(target_metadata)
157
+
158
+
159
+ def run_migrations_offline() -> None:
160
+ """Run migrations in 'offline' mode.
161
+
162
+ This configures the context with just a URL
163
+ and not an Engine, though an Engine is acceptable
164
+ here as well. By skipping the Engine creation
165
+ we don't even need a DBAPI to be available.
166
+
167
+ Calls to context.execute() here emit the given string to the
168
+ script output.
169
+ """
170
+ url = config.get_main_option("sqlalchemy.url")
171
+ context.configure(
172
+ url=url,
173
+ target_metadata=target_metadata,
174
+ literal_binds=True,
175
+ dialect_opts={"paramstyle": "named"},
176
+ )
177
+
178
+ with context.begin_transaction():
179
+ context.run_migrations()
180
+
181
+
182
+ def run_migrations_online() -> None:
183
+ """Run migrations in 'online' mode.
184
+
185
+ In this scenario we need to create an Engine
186
+ and associate a connection with the context.
187
+ """
188
+ connectable = engine_from_config(
189
+ config.get_section(config.config_ini_section, {}),
190
+ prefix="sqlalchemy.",
191
+ poolclass=pool.NullPool,
192
+ )
193
+
194
+ with connectable.connect() as connection:
195
+ context.configure(connection=connection, target_metadata=target_metadata)
196
+
197
+ with context.begin_transaction():
198
+ context.run_migrations()
199
+
200
+
201
+ if context.is_offline_mode():
202
+ run_migrations_offline()
203
+ else:
204
+ run_migrations_online()
205
+ '''
206
+
207
+
208
+ def create_initial_migration_script() -> str:
209
+ """Generate an Alembic migration script for creating metaxy system tables.
210
+
211
+ Returns:
212
+ String containing example migration script
213
+
214
+ Example:
215
+ >>> from metaxy.ext.alembic import create_initial_migration_script
216
+ >>> script = create_initial_migration_script()
217
+ >>> # Save to alembic/versions/001_create_metaxy_tables.py
218
+ """
219
+ return '''"""Create metaxy system tables.
220
+
221
+ Revision ID: create_metaxy_system_tables
222
+ Revises:
223
+ Create Date: 2024-01-01 00:00:00.000000
224
+
225
+ """
226
+ from typing import Sequence, Union
227
+
228
+ from alembic import op
229
+ import sqlalchemy as sa
230
+ from sqlalchemy.dialects import postgresql
231
+
232
+ # revision identifiers, used by Alembic.
233
+ revision: str = 'create_metaxy_system_tables'
234
+ down_revision: Union[str, None] = None
235
+ branch_labels: Union[str, Sequence[str], None] = None
236
+ depends_on: Union[str, Sequence[str], None] = None
237
+
238
+
239
+ def upgrade() -> None:
240
+ """Create metaxy system tables."""
241
+
242
+ # Create feature_versions table
243
+ op.create_table(
244
+ 'metaxy-system__feature_versions',
245
+ sa.Column('feature_key', sa.String(), nullable=False),
246
+ sa.Column('snapshot_version', sa.String(), nullable=False),
247
+ sa.Column('feature_version', sa.String(), nullable=False),
248
+ sa.Column('recorded_at', sa.DateTime(), nullable=False),
249
+ sa.Column('feature_spec', sa.String(), nullable=False),
250
+ sa.Column('feature_class_path', sa.String(), nullable=False),
251
+ sa.PrimaryKeyConstraint('feature_key', 'snapshot_version')
252
+ )
253
+ op.create_index('idx_feature_versions_recorded', 'metaxy-system__feature_versions', ['recorded_at'])
254
+ op.create_index('idx_feature_versions_lookup', 'metaxy-system__feature_versions', ['feature_key', 'feature_version'])
255
+
256
+ # Create migration_events table
257
+ op.create_table(
258
+ 'metaxy-system__migration_events',
259
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
260
+ sa.Column('migration_id', sa.String(), nullable=False),
261
+ sa.Column('event_type', sa.String(), nullable=False),
262
+ sa.Column('timestamp', sa.DateTime(), nullable=False),
263
+ sa.Column('feature_key', sa.String(), nullable=False),
264
+ sa.Column('rows_affected', sa.Integer(), nullable=False),
265
+ sa.Column('error_message', sa.String(), nullable=False),
266
+ sa.PrimaryKeyConstraint('id')
267
+ )
268
+ op.create_index('idx_migration_events_lookup', 'metaxy-system__migration_events', ['migration_id', 'event_type'])
269
+ op.create_index('idx_migration_events_feature', 'metaxy-system__migration_events', ['migration_id', 'feature_key'])
270
+ op.create_index(op.f('ix_metaxy-system__migration_events_migration_id'), 'metaxy-system__migration_events', ['migration_id'])
271
+ op.create_index(op.f('ix_metaxy-system__migration_events_timestamp'), 'metaxy-system__migration_events', ['timestamp'])
272
+
273
+
274
+ def downgrade() -> None:
275
+ """Drop metaxy system tables."""
276
+ op.drop_index(op.f('ix_metaxy-system__migration_events_timestamp'), table_name='metaxy-system__migration_events')
277
+ op.drop_index(op.f('ix_metaxy-system__migration_events_migration_id'), table_name='metaxy-system__migration_events')
278
+ op.drop_index('idx_migration_events_feature', table_name='metaxy-system__migration_events')
279
+ op.drop_index('idx_migration_events_lookup', table_name='metaxy-system__migration_events')
280
+ op.drop_table('metaxy-system__migration_events')
281
+
282
+ op.drop_index('idx_feature_versions_lookup', table_name='metaxy-system__feature_versions')
283
+ op.drop_index('idx_feature_versions_recorded', table_name='metaxy-system__feature_versions')
284
+ op.drop_table('metaxy-system__feature_versions')
285
+ '''
286
+
287
+
288
+ def get_table_creation_sql(dialect: str = "postgresql") -> dict[str, str]:
289
+ """Get SQL statements for creating metaxy system tables.
290
+
291
+ Args:
292
+ dialect: SQL dialect ('postgresql', 'mysql', 'sqlite', etc.)
293
+
294
+ Returns:
295
+ Dictionary mapping table name to CREATE TABLE SQL statement
296
+
297
+ Example:
298
+ >>> from metaxy.ext.alembic import get_table_creation_sql
299
+ >>> sql_statements = get_table_creation_sql("postgresql")
300
+ >>> print(sql_statements["feature_versions"])
301
+ """
302
+ from sqlalchemy import create_engine
303
+ from sqlalchemy.schema import CreateTable
304
+
305
+ # Get metaxy metadata
306
+ metadata = get_metaxy_metadata()
307
+
308
+ # Create a mock engine for the specified dialect
309
+ if dialect == "postgresql":
310
+ engine_url = "postgresql:///"
311
+ elif dialect == "mysql":
312
+ engine_url = "mysql:///"
313
+ elif dialect == "sqlite":
314
+ engine_url = "sqlite:///"
315
+ else:
316
+ engine_url = f"{dialect}:///"
317
+
318
+ engine = create_engine(engine_url)
319
+
320
+ # Generate CREATE TABLE statements
321
+ sql_statements = {}
322
+ for table_name, table in metadata.tables.items():
323
+ create_statement = CreateTable(table).compile(engine)
324
+ sql_statements[table_name] = str(create_statement)
325
+
326
+ return sql_statements