sqlspec 0.26.0__py3-none-any.whl → 0.27.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 sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +55 -25
- sqlspec/_typing.py +62 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +52 -2
- sqlspec/adapters/adbc/driver.py +144 -45
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +25 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +352 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +138 -43
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +292 -84
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
- sqlspec/adapters/psqlpy/driver.py +101 -31
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +77 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +231 -60
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +95 -161
- sqlspec/driver/_common.py +133 -80
- sqlspec/driver/_sync.py +95 -162
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +14 -84
- sqlspec/utils/config_resolver.py +6 -6
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +3 -3
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/utils/type_guards.py
CHANGED
|
@@ -7,7 +7,9 @@ understand type narrowing, replacing defensive hasattr() and duck typing pattern
|
|
|
7
7
|
from collections.abc import Sequence
|
|
8
8
|
from collections.abc import Set as AbstractSet
|
|
9
9
|
from functools import lru_cache
|
|
10
|
-
from typing import TYPE_CHECKING, Any,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
from typing_extensions import is_typeddict
|
|
11
13
|
|
|
12
14
|
from sqlspec.typing import (
|
|
13
15
|
ATTRS_INSTALLED,
|
|
@@ -24,9 +26,9 @@ from sqlspec.typing import (
|
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from dataclasses import Field
|
|
29
|
+
from typing import TypeGuard
|
|
27
30
|
|
|
28
31
|
from sqlglot import exp
|
|
29
|
-
from typing_extensions import TypeGuard
|
|
30
32
|
|
|
31
33
|
from sqlspec._typing import AttrsInstanceStub, BaseModelStub, DTODataStub, StructStub
|
|
32
34
|
from sqlspec.builder import Select
|
|
@@ -100,6 +102,7 @@ __all__ = (
|
|
|
100
102
|
"is_indexable_row",
|
|
101
103
|
"is_iterable_parameters",
|
|
102
104
|
"is_limit_offset_filter",
|
|
105
|
+
"is_local_path",
|
|
103
106
|
"is_msgspec_struct",
|
|
104
107
|
"is_msgspec_struct_with_field",
|
|
105
108
|
"is_msgspec_struct_without_field",
|
|
@@ -117,8 +120,10 @@ __all__ = (
|
|
|
117
120
|
"is_select_builder",
|
|
118
121
|
"is_statement_filter",
|
|
119
122
|
"is_string_literal",
|
|
123
|
+
"is_typed_dict",
|
|
120
124
|
"is_typed_parameter",
|
|
121
125
|
"schema_dump",
|
|
126
|
+
"supports_arrow_native",
|
|
122
127
|
"supports_limit",
|
|
123
128
|
"supports_offset",
|
|
124
129
|
"supports_order_by",
|
|
@@ -126,6 +131,18 @@ __all__ = (
|
|
|
126
131
|
)
|
|
127
132
|
|
|
128
133
|
|
|
134
|
+
def is_typed_dict(obj: Any) -> "TypeGuard[type]":
|
|
135
|
+
"""Check if an object is a TypedDict class.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
obj: The object to check
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if the object is a TypedDict class, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
return is_typeddict(obj)
|
|
144
|
+
|
|
145
|
+
|
|
129
146
|
def is_statement_filter(obj: Any) -> "TypeGuard[StatementFilter]":
|
|
130
147
|
"""Check if an object implements the StatementFilter protocol.
|
|
131
148
|
|
|
@@ -434,7 +451,7 @@ def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[Str
|
|
|
434
451
|
|
|
435
452
|
|
|
436
453
|
@lru_cache(maxsize=500)
|
|
437
|
-
def _detect_rename_pattern(field_name: str, encode_name: str) -> "
|
|
454
|
+
def _detect_rename_pattern(field_name: str, encode_name: str) -> "str | None":
|
|
438
455
|
"""Detect the rename pattern by comparing field name transformations.
|
|
439
456
|
|
|
440
457
|
Args:
|
|
@@ -458,7 +475,7 @@ def _detect_rename_pattern(field_name: str, encode_name: str) -> "Optional[str]"
|
|
|
458
475
|
return None
|
|
459
476
|
|
|
460
477
|
|
|
461
|
-
def get_msgspec_rename_config(schema_type: type) -> "
|
|
478
|
+
def get_msgspec_rename_config(schema_type: type) -> "str | None":
|
|
462
479
|
"""Extract msgspec rename configuration from a struct type.
|
|
463
480
|
|
|
464
481
|
Analyzes field name transformations to detect the rename pattern used by msgspec.
|
|
@@ -611,7 +628,7 @@ def is_schema(obj: Any) -> "TypeGuard[SupportedSchemaModel]":
|
|
|
611
628
|
)
|
|
612
629
|
|
|
613
630
|
|
|
614
|
-
def is_schema_or_dict(obj: Any) -> "TypeGuard[
|
|
631
|
+
def is_schema_or_dict(obj: Any) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
|
|
615
632
|
"""Check if a value is a msgspec Struct, Pydantic model, or dict.
|
|
616
633
|
|
|
617
634
|
Args:
|
|
@@ -649,7 +666,7 @@ def is_schema_without_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSc
|
|
|
649
666
|
return not is_schema_with_field(obj, field_name)
|
|
650
667
|
|
|
651
668
|
|
|
652
|
-
def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[
|
|
669
|
+
def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
|
|
653
670
|
"""Check if a value is a msgspec Struct, Pydantic model, or dict with a specific field.
|
|
654
671
|
|
|
655
672
|
Args:
|
|
@@ -662,9 +679,7 @@ def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[Union[
|
|
|
662
679
|
return is_schema_with_field(obj, field_name) or is_dict_with_field(obj, field_name)
|
|
663
680
|
|
|
664
681
|
|
|
665
|
-
def is_schema_or_dict_without_field(
|
|
666
|
-
obj: Any, field_name: str
|
|
667
|
-
) -> "TypeGuard[Union[SupportedSchemaModel, dict[str, Any]]]":
|
|
682
|
+
def is_schema_or_dict_without_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
|
|
668
683
|
"""Check if a value is a msgspec Struct, Pydantic model, or dict without a specific field.
|
|
669
684
|
|
|
670
685
|
Args:
|
|
@@ -721,8 +736,8 @@ def extract_dataclass_fields(
|
|
|
721
736
|
obj: "DataclassProtocol",
|
|
722
737
|
exclude_none: bool = False,
|
|
723
738
|
exclude_empty: bool = False,
|
|
724
|
-
include: "
|
|
725
|
-
exclude: "
|
|
739
|
+
include: "AbstractSet[str] | None" = None,
|
|
740
|
+
exclude: "AbstractSet[str] | None" = None,
|
|
726
741
|
) -> "tuple[Field[Any], ...]":
|
|
727
742
|
"""Extract dataclass fields.
|
|
728
743
|
|
|
@@ -767,8 +782,8 @@ def extract_dataclass_items(
|
|
|
767
782
|
obj: "DataclassProtocol",
|
|
768
783
|
exclude_none: bool = False,
|
|
769
784
|
exclude_empty: bool = False,
|
|
770
|
-
include: "
|
|
771
|
-
exclude: "
|
|
785
|
+
include: "AbstractSet[str] | None" = None,
|
|
786
|
+
exclude: "AbstractSet[str] | None" = None,
|
|
772
787
|
) -> "tuple[tuple[str, Any], ...]":
|
|
773
788
|
"""Extract name-value pairs from a dataclass instance.
|
|
774
789
|
|
|
@@ -791,7 +806,7 @@ def dataclass_to_dict(
|
|
|
791
806
|
exclude_none: bool = False,
|
|
792
807
|
exclude_empty: bool = False,
|
|
793
808
|
convert_nested: bool = True,
|
|
794
|
-
exclude: "
|
|
809
|
+
exclude: "AbstractSet[str] | None" = None,
|
|
795
810
|
) -> "dict[str, Any]":
|
|
796
811
|
"""Convert a dataclass instance to a dictionary.
|
|
797
812
|
|
|
@@ -978,7 +993,7 @@ def has_attr(obj: Any, attr: str) -> bool:
|
|
|
978
993
|
return True
|
|
979
994
|
|
|
980
995
|
|
|
981
|
-
def get_node_this(node: "exp.Expression", default:
|
|
996
|
+
def get_node_this(node: "exp.Expression", default: Any | None = None) -> Any:
|
|
982
997
|
"""Safely get the 'this' attribute from a SQLGlot node.
|
|
983
998
|
|
|
984
999
|
Args:
|
|
@@ -1010,7 +1025,7 @@ def has_this_attribute(node: "exp.Expression") -> bool:
|
|
|
1010
1025
|
return True
|
|
1011
1026
|
|
|
1012
1027
|
|
|
1013
|
-
def get_node_expressions(node: "exp.Expression", default:
|
|
1028
|
+
def get_node_expressions(node: "exp.Expression", default: Any | None = None) -> Any:
|
|
1014
1029
|
"""Safely get the 'expressions' attribute from a SQLGlot node.
|
|
1015
1030
|
|
|
1016
1031
|
Args:
|
|
@@ -1042,7 +1057,7 @@ def has_expressions_attribute(node: "exp.Expression") -> bool:
|
|
|
1042
1057
|
return True
|
|
1043
1058
|
|
|
1044
1059
|
|
|
1045
|
-
def get_literal_parent(literal: "exp.Expression", default:
|
|
1060
|
+
def get_literal_parent(literal: "exp.Expression", default: Any | None = None) -> Any:
|
|
1046
1061
|
"""Safely get the 'parent' attribute from a SQLGlot literal.
|
|
1047
1062
|
|
|
1048
1063
|
Args:
|
|
@@ -1113,7 +1128,7 @@ def is_number_literal(literal: "exp.Literal") -> bool:
|
|
|
1113
1128
|
return False
|
|
1114
1129
|
|
|
1115
1130
|
|
|
1116
|
-
def get_initial_expression(context: Any) -> "
|
|
1131
|
+
def get_initial_expression(context: Any) -> "exp.Expression | None":
|
|
1117
1132
|
"""Safely get initial_expression from context.
|
|
1118
1133
|
|
|
1119
1134
|
Args:
|
|
@@ -1128,7 +1143,7 @@ def get_initial_expression(context: Any) -> "Optional[exp.Expression]":
|
|
|
1128
1143
|
return None
|
|
1129
1144
|
|
|
1130
1145
|
|
|
1131
|
-
def expression_has_limit(expr: "
|
|
1146
|
+
def expression_has_limit(expr: "exp.Expression | None") -> bool:
|
|
1132
1147
|
"""Check if an expression has a limit clause.
|
|
1133
1148
|
|
|
1134
1149
|
Args:
|
|
@@ -1160,7 +1175,7 @@ def get_value_attribute(obj: Any) -> Any:
|
|
|
1160
1175
|
return obj
|
|
1161
1176
|
|
|
1162
1177
|
|
|
1163
|
-
def get_param_style_and_name(param: Any) -> "tuple[
|
|
1178
|
+
def get_param_style_and_name(param: Any) -> "tuple[str | None, str | None]":
|
|
1164
1179
|
"""Safely get style and name attributes from a parameter.
|
|
1165
1180
|
|
|
1166
1181
|
Args:
|
|
@@ -1242,3 +1257,79 @@ def has_expression_and_parameters(obj: Any) -> bool:
|
|
|
1242
1257
|
True if the object has both attributes, False otherwise
|
|
1243
1258
|
"""
|
|
1244
1259
|
return hasattr(obj, "expression") and hasattr(obj, "parameters")
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
WINDOWS_DRIVE_PATTERN_LENGTH = 3
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def is_local_path(uri: str) -> bool:
|
|
1266
|
+
r"""Check if URI represents a local filesystem path.
|
|
1267
|
+
|
|
1268
|
+
Detects local paths including:
|
|
1269
|
+
- file:// URIs
|
|
1270
|
+
- Absolute paths (Unix: /, Windows: C:\\)
|
|
1271
|
+
- Relative paths (., .., ~)
|
|
1272
|
+
|
|
1273
|
+
Args:
|
|
1274
|
+
uri: URI or path string to check.
|
|
1275
|
+
|
|
1276
|
+
Returns:
|
|
1277
|
+
True if uri is a local path, False for remote URIs.
|
|
1278
|
+
|
|
1279
|
+
Examples:
|
|
1280
|
+
>>> is_local_path("file:///data/file.txt")
|
|
1281
|
+
True
|
|
1282
|
+
>>> is_local_path("/absolute/path")
|
|
1283
|
+
True
|
|
1284
|
+
>>> is_local_path("s3://bucket/key")
|
|
1285
|
+
False
|
|
1286
|
+
"""
|
|
1287
|
+
if not uri:
|
|
1288
|
+
return False
|
|
1289
|
+
|
|
1290
|
+
if "://" in uri and not uri.startswith("file://"):
|
|
1291
|
+
return False
|
|
1292
|
+
|
|
1293
|
+
if uri.startswith("file://"):
|
|
1294
|
+
return True
|
|
1295
|
+
|
|
1296
|
+
if uri.startswith("/"):
|
|
1297
|
+
return True
|
|
1298
|
+
|
|
1299
|
+
if uri.startswith((".", "~")):
|
|
1300
|
+
return True
|
|
1301
|
+
|
|
1302
|
+
if len(uri) >= WINDOWS_DRIVE_PATTERN_LENGTH and uri[1:3] == ":\\":
|
|
1303
|
+
return True
|
|
1304
|
+
|
|
1305
|
+
return "/" in uri or "\\" in uri
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def supports_arrow_native(backend: Any) -> bool:
|
|
1309
|
+
"""Check if storage backend supports native Arrow operations.
|
|
1310
|
+
|
|
1311
|
+
Some storage backends (like certain obstore stores) have native
|
|
1312
|
+
Arrow read/write support, which is faster than going through bytes.
|
|
1313
|
+
|
|
1314
|
+
Args:
|
|
1315
|
+
backend: Storage backend instance to check.
|
|
1316
|
+
|
|
1317
|
+
Returns:
|
|
1318
|
+
True if backend has native read_arrow/write_arrow methods.
|
|
1319
|
+
|
|
1320
|
+
Examples:
|
|
1321
|
+
>>> from sqlspec.storage.backends.obstore import ObStoreBackend
|
|
1322
|
+
>>> backend = ObStoreBackend("file:///tmp")
|
|
1323
|
+
>>> supports_arrow_native(backend)
|
|
1324
|
+
False
|
|
1325
|
+
"""
|
|
1326
|
+
from sqlspec.protocols import ObjectStoreProtocol
|
|
1327
|
+
|
|
1328
|
+
if not isinstance(backend, ObjectStoreProtocol):
|
|
1329
|
+
return False
|
|
1330
|
+
|
|
1331
|
+
try:
|
|
1332
|
+
store = backend.store # type: ignore[attr-defined]
|
|
1333
|
+
return callable(getattr(store, "read_arrow", None))
|
|
1334
|
+
except AttributeError:
|
|
1335
|
+
return False
|
sqlspec/utils/version.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Migration version parsing and comparison utilities.
|
|
2
|
+
|
|
3
|
+
Provides structured parsing of migration versions supporting both legacy sequential
|
|
4
|
+
(0001) and timestamp-based (20251011120000) formats with type-safe comparison.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"MigrationVersion",
|
|
16
|
+
"VersionType",
|
|
17
|
+
"convert_to_sequential_version",
|
|
18
|
+
"generate_conversion_map",
|
|
19
|
+
"generate_timestamp_version",
|
|
20
|
+
"get_next_sequential_number",
|
|
21
|
+
"is_sequential_version",
|
|
22
|
+
"is_timestamp_version",
|
|
23
|
+
"parse_version",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Regex patterns for version detection
|
|
29
|
+
SEQUENTIAL_PATTERN = re.compile(r"^(?!\d{14}$)(\d+)$")
|
|
30
|
+
TIMESTAMP_PATTERN = re.compile(r"^(\d{14})$")
|
|
31
|
+
EXTENSION_PATTERN = re.compile(r"^ext_(\w+)_(.+)$")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VersionType(Enum):
|
|
35
|
+
"""Migration version format type."""
|
|
36
|
+
|
|
37
|
+
SEQUENTIAL = "sequential"
|
|
38
|
+
TIMESTAMP = "timestamp"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class MigrationVersion:
|
|
43
|
+
"""Parsed migration version with structured comparison support.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
raw: Original version string (e.g., "0001", "20251011120000", "ext_litestar_0001").
|
|
47
|
+
type: Version format type (sequential or timestamp).
|
|
48
|
+
sequence: Numeric value for sequential versions (e.g., 1, 2, 42).
|
|
49
|
+
timestamp: Parsed datetime for timestamp versions (UTC).
|
|
50
|
+
extension: Extension name for extension-prefixed versions (e.g., "litestar").
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
raw: str
|
|
54
|
+
type: VersionType
|
|
55
|
+
sequence: "int | None"
|
|
56
|
+
timestamp: "datetime | None"
|
|
57
|
+
extension: "str | None"
|
|
58
|
+
|
|
59
|
+
def __lt__(self, other: "MigrationVersion") -> bool:
|
|
60
|
+
"""Compare versions supporting mixed formats.
|
|
61
|
+
|
|
62
|
+
Comparison Rules:
|
|
63
|
+
1. Extension migrations sort by extension name first, then version
|
|
64
|
+
2. Sequential < Timestamp (legacy migrations first)
|
|
65
|
+
3. Sequential vs Sequential: numeric comparison
|
|
66
|
+
4. Timestamp vs Timestamp: chronological comparison
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
other: Version to compare against.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if this version sorts before other.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
TypeError: If comparing against non-MigrationVersion.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(other, MigrationVersion):
|
|
78
|
+
return NotImplemented
|
|
79
|
+
|
|
80
|
+
if self.extension != other.extension:
|
|
81
|
+
if self.extension is None:
|
|
82
|
+
return True
|
|
83
|
+
if other.extension is None:
|
|
84
|
+
return False
|
|
85
|
+
return self.extension < other.extension
|
|
86
|
+
|
|
87
|
+
if self.type == other.type:
|
|
88
|
+
if self.type == VersionType.SEQUENTIAL:
|
|
89
|
+
return (self.sequence or 0) < (other.sequence or 0)
|
|
90
|
+
return (self.timestamp or datetime.min.replace(tzinfo=timezone.utc)) < (
|
|
91
|
+
other.timestamp or datetime.min.replace(tzinfo=timezone.utc)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return self.type == VersionType.SEQUENTIAL
|
|
95
|
+
|
|
96
|
+
def __le__(self, other: "MigrationVersion") -> bool:
|
|
97
|
+
"""Check if version is less than or equal to another.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
other: Version to compare against.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if this version is less than or equal to other.
|
|
104
|
+
"""
|
|
105
|
+
return self == other or self < other
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: object) -> bool:
|
|
108
|
+
"""Check version equality.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
other: Version to compare against.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if versions are equal.
|
|
115
|
+
"""
|
|
116
|
+
if not isinstance(other, MigrationVersion):
|
|
117
|
+
return NotImplemented
|
|
118
|
+
return self.raw == other.raw
|
|
119
|
+
|
|
120
|
+
def __hash__(self) -> int:
|
|
121
|
+
"""Hash version for use in sets and dicts.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Hash value based on raw version string.
|
|
125
|
+
"""
|
|
126
|
+
return hash(self.raw)
|
|
127
|
+
|
|
128
|
+
def __repr__(self) -> str:
|
|
129
|
+
"""Get string representation for debugging.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
String representation with type and value.
|
|
133
|
+
"""
|
|
134
|
+
if self.extension:
|
|
135
|
+
return f"MigrationVersion(ext={self.extension}, {self.type.value}={self.raw})"
|
|
136
|
+
return f"MigrationVersion({self.type.value}={self.raw})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_sequential_version(version_str: str) -> bool:
|
|
140
|
+
"""Check if version string is sequential format.
|
|
141
|
+
|
|
142
|
+
Sequential format: Any sequence of digits (0001, 42, 9999, 10000+).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
version_str: Version string to check.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if sequential format.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
>>> is_sequential_version("0001")
|
|
152
|
+
True
|
|
153
|
+
>>> is_sequential_version("42")
|
|
154
|
+
True
|
|
155
|
+
>>> is_sequential_version("10000")
|
|
156
|
+
True
|
|
157
|
+
>>> is_sequential_version("20251011120000")
|
|
158
|
+
False
|
|
159
|
+
"""
|
|
160
|
+
return bool(SEQUENTIAL_PATTERN.match(version_str))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_timestamp_version(version_str: str) -> bool:
|
|
164
|
+
"""Check if version string is timestamp format.
|
|
165
|
+
|
|
166
|
+
Timestamp format: 14-digit YYYYMMDDHHmmss (20251011120000).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
version_str: Version string to check.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if timestamp format.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> is_timestamp_version("20251011120000")
|
|
176
|
+
True
|
|
177
|
+
>>> is_timestamp_version("0001")
|
|
178
|
+
False
|
|
179
|
+
"""
|
|
180
|
+
if not TIMESTAMP_PATTERN.match(version_str):
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
|
|
185
|
+
except ValueError:
|
|
186
|
+
return False
|
|
187
|
+
else:
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def parse_version(version_str: str) -> MigrationVersion:
|
|
192
|
+
"""Parse version string into structured format.
|
|
193
|
+
|
|
194
|
+
Supports:
|
|
195
|
+
- Sequential: "0001", "42", "9999"
|
|
196
|
+
- Timestamp: "20251011120000"
|
|
197
|
+
- Extension: "ext_litestar_0001", "ext_litestar_20251011120000"
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
version_str: Version string to parse.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Parsed migration version.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If version format is invalid.
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
>>> v = parse_version("0001")
|
|
210
|
+
>>> v.type == VersionType.SEQUENTIAL
|
|
211
|
+
True
|
|
212
|
+
>>> v.sequence
|
|
213
|
+
1
|
|
214
|
+
|
|
215
|
+
>>> v = parse_version("20251011120000")
|
|
216
|
+
>>> v.type == VersionType.TIMESTAMP
|
|
217
|
+
True
|
|
218
|
+
|
|
219
|
+
>>> v = parse_version("ext_litestar_0001")
|
|
220
|
+
>>> v.extension
|
|
221
|
+
'litestar'
|
|
222
|
+
"""
|
|
223
|
+
extension_match = EXTENSION_PATTERN.match(version_str)
|
|
224
|
+
if extension_match:
|
|
225
|
+
extension_name = extension_match.group(1)
|
|
226
|
+
base_version = extension_match.group(2)
|
|
227
|
+
parsed = parse_version(base_version)
|
|
228
|
+
|
|
229
|
+
return MigrationVersion(
|
|
230
|
+
raw=version_str,
|
|
231
|
+
type=parsed.type,
|
|
232
|
+
sequence=parsed.sequence,
|
|
233
|
+
timestamp=parsed.timestamp,
|
|
234
|
+
extension=extension_name,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if is_timestamp_version(version_str):
|
|
238
|
+
dt = datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
|
|
239
|
+
return MigrationVersion(
|
|
240
|
+
raw=version_str, type=VersionType.TIMESTAMP, sequence=None, timestamp=dt, extension=None
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if is_sequential_version(version_str):
|
|
244
|
+
return MigrationVersion(
|
|
245
|
+
raw=version_str, type=VersionType.SEQUENTIAL, sequence=int(version_str), timestamp=None, extension=None
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
msg = f"Invalid migration version format: {version_str}. Expected sequential (0001) or timestamp (YYYYMMDDHHmmss)."
|
|
249
|
+
raise ValueError(msg)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def generate_timestamp_version() -> str:
|
|
253
|
+
"""Generate new timestamp version in UTC.
|
|
254
|
+
|
|
255
|
+
Format: YYYYMMDDHHmmss (14 digits).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Timestamp version string.
|
|
259
|
+
|
|
260
|
+
Examples:
|
|
261
|
+
>>> version = generate_timestamp_version()
|
|
262
|
+
>>> len(version)
|
|
263
|
+
14
|
|
264
|
+
>>> is_timestamp_version(version)
|
|
265
|
+
True
|
|
266
|
+
"""
|
|
267
|
+
return datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_next_sequential_number(migrations: "list[MigrationVersion]", extension: "str | None" = None) -> int:
|
|
271
|
+
"""Find highest sequential number and return next available.
|
|
272
|
+
|
|
273
|
+
Scans migrations for sequential versions and returns the next number in sequence.
|
|
274
|
+
When extension is specified, only that extension's migrations are considered.
|
|
275
|
+
When extension is None, only core (non-extension) migrations are considered.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
migrations: List of parsed migration versions.
|
|
279
|
+
extension: Optional extension name to filter by (e.g., "litestar", "adk").
|
|
280
|
+
None means core migrations only.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Next available sequential number (1 if no sequential migrations exist).
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
>>> v1 = parse_version("0001")
|
|
287
|
+
>>> v2 = parse_version("0002")
|
|
288
|
+
>>> get_next_sequential_number([v1, v2])
|
|
289
|
+
3
|
|
290
|
+
|
|
291
|
+
>>> get_next_sequential_number([])
|
|
292
|
+
1
|
|
293
|
+
|
|
294
|
+
>>> ext = parse_version("ext_litestar_0001")
|
|
295
|
+
>>> core = parse_version("0001")
|
|
296
|
+
>>> get_next_sequential_number([ext, core])
|
|
297
|
+
2
|
|
298
|
+
|
|
299
|
+
>>> ext1 = parse_version("ext_litestar_0001")
|
|
300
|
+
>>> get_next_sequential_number([ext1], extension="litestar")
|
|
301
|
+
2
|
|
302
|
+
"""
|
|
303
|
+
sequential = [
|
|
304
|
+
m.sequence for m in migrations if m.type == VersionType.SEQUENTIAL and m.extension == extension and m.sequence
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
if not sequential:
|
|
308
|
+
return 1
|
|
309
|
+
|
|
310
|
+
return max(sequential) + 1
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def convert_to_sequential_version(timestamp_version: MigrationVersion, sequence_number: int) -> str:
|
|
314
|
+
"""Convert timestamp MigrationVersion to sequential string format.
|
|
315
|
+
|
|
316
|
+
Preserves extension prefixes during conversion. Format uses zero-padded
|
|
317
|
+
4-digit numbers (0001, 0002, etc.).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
timestamp_version: Parsed timestamp version to convert.
|
|
321
|
+
sequence_number: Sequential number to assign.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Sequential version string with extension prefix if applicable.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If input is not a timestamp version.
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
>>> v = parse_version("20251011120000")
|
|
331
|
+
>>> convert_to_sequential_version(v, 3)
|
|
332
|
+
'0003'
|
|
333
|
+
|
|
334
|
+
>>> v = parse_version("ext_litestar_20251011120000")
|
|
335
|
+
>>> convert_to_sequential_version(v, 1)
|
|
336
|
+
'ext_litestar_0001'
|
|
337
|
+
|
|
338
|
+
>>> v = parse_version("0001")
|
|
339
|
+
>>> convert_to_sequential_version(v, 2)
|
|
340
|
+
Traceback (most recent call last):
|
|
341
|
+
...
|
|
342
|
+
ValueError: Can only convert timestamp versions to sequential
|
|
343
|
+
"""
|
|
344
|
+
if timestamp_version.type != VersionType.TIMESTAMP:
|
|
345
|
+
msg = "Can only convert timestamp versions to sequential"
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
|
|
348
|
+
seq_str = str(sequence_number).zfill(4)
|
|
349
|
+
|
|
350
|
+
if timestamp_version.extension:
|
|
351
|
+
return f"ext_{timestamp_version.extension}_{seq_str}"
|
|
352
|
+
|
|
353
|
+
return seq_str
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def generate_conversion_map(migrations: "list[tuple[str, Any]]") -> "dict[str, str]":
|
|
357
|
+
"""Generate mapping from timestamp versions to sequential versions.
|
|
358
|
+
|
|
359
|
+
Separates timestamp migrations from sequential, sorts timestamps chronologically,
|
|
360
|
+
and assigns sequential numbers starting after the highest existing sequential
|
|
361
|
+
number. Extension migrations maintain separate numbering within their namespace.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
migrations: List of tuples (version_string, migration_path).
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dictionary mapping old timestamp versions to new sequential versions.
|
|
368
|
+
|
|
369
|
+
Examples:
|
|
370
|
+
>>> migrations = [
|
|
371
|
+
... ("0001", Path("0001_init.sql")),
|
|
372
|
+
... ("0002", Path("0002_users.sql")),
|
|
373
|
+
... ("20251011120000", Path("20251011120000_products.sql")),
|
|
374
|
+
... ("20251012130000", Path("20251012130000_orders.sql")),
|
|
375
|
+
... ]
|
|
376
|
+
>>> result = generate_conversion_map(migrations)
|
|
377
|
+
>>> result
|
|
378
|
+
{'20251011120000': '0003', '20251012130000': '0004'}
|
|
379
|
+
|
|
380
|
+
>>> migrations = [
|
|
381
|
+
... ("20251011120000", Path("20251011120000_first.sql")),
|
|
382
|
+
... ("20251010090000", Path("20251010090000_earlier.sql")),
|
|
383
|
+
... ]
|
|
384
|
+
>>> result = generate_conversion_map(migrations)
|
|
385
|
+
>>> result
|
|
386
|
+
{'20251010090000': '0001', '20251011120000': '0002'}
|
|
387
|
+
|
|
388
|
+
>>> migrations = []
|
|
389
|
+
>>> generate_conversion_map(migrations)
|
|
390
|
+
{}
|
|
391
|
+
"""
|
|
392
|
+
if not migrations:
|
|
393
|
+
return {}
|
|
394
|
+
|
|
395
|
+
def _try_parse_version(version_str: str) -> "MigrationVersion | None":
|
|
396
|
+
"""Parse version string, returning None for invalid versions."""
|
|
397
|
+
try:
|
|
398
|
+
return parse_version(version_str)
|
|
399
|
+
except ValueError:
|
|
400
|
+
logger.warning("Skipping invalid migration version: %s", version_str)
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
parsed_versions = [v for version_str, _path in migrations if (v := _try_parse_version(version_str)) is not None]
|
|
404
|
+
|
|
405
|
+
timestamp_migrations = sorted([v for v in parsed_versions if v.type == VersionType.TIMESTAMP])
|
|
406
|
+
|
|
407
|
+
if not timestamp_migrations:
|
|
408
|
+
return {}
|
|
409
|
+
|
|
410
|
+
core_timestamps = [m for m in timestamp_migrations if m.extension is None]
|
|
411
|
+
ext_timestamps_by_name: dict[str, list[MigrationVersion]] = {}
|
|
412
|
+
for m in timestamp_migrations:
|
|
413
|
+
if m.extension:
|
|
414
|
+
ext_timestamps_by_name.setdefault(m.extension, []).append(m)
|
|
415
|
+
|
|
416
|
+
conversion_map: dict[str, str] = {}
|
|
417
|
+
|
|
418
|
+
if core_timestamps:
|
|
419
|
+
next_seq = get_next_sequential_number(parsed_versions)
|
|
420
|
+
for timestamp_version in core_timestamps:
|
|
421
|
+
sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
|
|
422
|
+
conversion_map[timestamp_version.raw] = sequential_version
|
|
423
|
+
next_seq += 1
|
|
424
|
+
|
|
425
|
+
for ext_name, ext_migrations in ext_timestamps_by_name.items():
|
|
426
|
+
ext_parsed = [v for v in parsed_versions if v.extension == ext_name]
|
|
427
|
+
next_seq = get_next_sequential_number(ext_parsed, extension=ext_name)
|
|
428
|
+
for timestamp_version in ext_migrations:
|
|
429
|
+
sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
|
|
430
|
+
conversion_map[timestamp_version.raw] = sequential_version
|
|
431
|
+
next_seq += 1
|
|
432
|
+
|
|
433
|
+
return conversion_map
|