deev 2.0.2__tar.gz → 2.1.2__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 (107) hide show
  1. {deev-2.0.2/src/deev.egg-info → deev-2.1.2}/PKG-INFO +3 -1
  2. {deev-2.0.2 → deev-2.1.2}/pyproject.toml +4 -1
  3. {deev-2.0.2 → deev-2.1.2}/src/deev/__init__.py +3 -2
  4. {deev-2.0.2 → deev-2.1.2}/src/deev/common/connection_string.py +1 -0
  5. deev-2.1.2/src/deev/mssql/__init__.py +31 -0
  6. deev-2.1.2/src/deev/mssql/async_mssql_proxy_connection.py +54 -0
  7. deev-2.1.2/src/deev/mssql/async_mssql_proxy_cursor.py +70 -0
  8. deev-2.1.2/src/deev/mssql/async_mssql_table_adapter.py +146 -0
  9. deev-2.1.2/src/deev/mssql/async_mssql_transaction_context.py +135 -0
  10. deev-2.1.2/src/deev/mssql/mssql_ddl_generator.py +91 -0
  11. deev-2.1.2/src/deev/mssql/mssql_proxy_connection.py +73 -0
  12. deev-2.1.2/src/deev/mssql/mssql_proxy_cursor.py +119 -0
  13. deev-2.1.2/src/deev/mssql/mssql_table_adapter.py +407 -0
  14. deev-2.1.2/src/deev/mssql/mssql_transaction_context.py +399 -0
  15. deev-2.1.2/src/deev/mssql/mssql_type_mapper.py +75 -0
  16. {deev-2.0.2 → deev-2.1.2}/src/deev/translation/__init__.py +2 -0
  17. {deev-2.0.2 → deev-2.1.2}/src/deev/translation/utils.py +54 -3
  18. {deev-2.0.2 → deev-2.1.2}/src/deev/utils.py +135 -7
  19. {deev-2.0.2 → deev-2.1.2/src/deev.egg-info}/PKG-INFO +3 -1
  20. {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/SOURCES.txt +11 -0
  21. {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/requires.txt +3 -0
  22. {deev-2.0.2 → deev-2.1.2}/LICENSE +0 -0
  23. {deev-2.0.2 → deev-2.1.2}/README.md +0 -0
  24. {deev-2.0.2 → deev-2.1.2}/setup.cfg +0 -0
  25. {deev-2.0.2 → deev-2.1.2}/src/deev/SKILL.md +0 -0
  26. {deev-2.0.2 → deev-2.1.2}/src/deev/_immutable_mixin.py +0 -0
  27. {deev-2.0.2 → deev-2.1.2}/src/deev/_migration_data.py +0 -0
  28. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/__init__.py +0 -0
  29. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_proxy_connection.py +0 -0
  30. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_proxy_cursor.py +0 -0
  31. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_table_adapter.py +0 -0
  32. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/async_clickhouse_transaction_context.py +0 -0
  33. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_ddl_generator.py +0 -0
  34. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_proxy_connection.py +0 -0
  35. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_proxy_cursor.py +0 -0
  36. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_table_adapter.py +0 -0
  37. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_transaction_context.py +0 -0
  38. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/clickhouse_type_mapper.py +0 -0
  39. {deev-2.0.2 → deev-2.1.2}/src/deev/clickhouse/utils.py +0 -0
  40. {deev-2.0.2 → deev-2.1.2}/src/deev/common/__init__.py +0 -0
  41. {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_adapter.py +0 -0
  42. {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_connection.py +0 -0
  43. {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_cursor.py +0 -0
  44. {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_table_adapter.py +0 -0
  45. {deev-2.0.2 → deev-2.1.2}/src/deev/common/async_db_transaction_context.py +0 -0
  46. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_adapter.py +0 -0
  47. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_connection.py +0 -0
  48. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_context.py +0 -0
  49. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_cursor.py +0 -0
  50. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_error.py +0 -0
  51. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_migrator.py +0 -0
  52. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_parameters.py +0 -0
  53. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_table_adapter.py +0 -0
  54. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_transaction_context.py +0 -0
  55. {deev-2.0.2 → deev-2.1.2}/src/deev/common/db_type_mapper.py +0 -0
  56. {deev-2.0.2 → deev-2.1.2}/src/deev/common/description_field.py +0 -0
  57. {deev-2.0.2 → deev-2.1.2}/src/deev/common/noop_cursor.py +0 -0
  58. {deev-2.0.2 → deev-2.1.2}/src/deev/common/tlc_parser.py +0 -0
  59. {deev-2.0.2 → deev-2.1.2}/src/deev/db_migrate.py +0 -0
  60. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/__init__.py +0 -0
  61. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/entity_field_spec.py +0 -0
  62. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/entity_spec.py +0 -0
  63. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/index_options.py +0 -0
  64. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/index_order.py +0 -0
  65. {deev-2.0.2 → deev-2.1.2}/src/deev/entities/utils.py +0 -0
  66. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/__init__.py +0 -0
  67. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_proxy_connection.py +0 -0
  68. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_proxy_cursor.py +0 -0
  69. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_table_adapter.py +0 -0
  70. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/async_mongo_transaction_context.py +0 -0
  71. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_proxy_connection.py +0 -0
  72. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_proxy_cursor.py +0 -0
  73. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_table_adapter.py +0 -0
  74. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_transaction_context.py +0 -0
  75. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/mongo_type_mapper.py +0 -0
  76. {deev-2.0.2 → deev-2.1.2}/src/deev/mongodb/utils.py +0 -0
  77. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/__init__.py +0 -0
  78. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_proxy_connection.py +0 -0
  79. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_proxy_cursor.py +0 -0
  80. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_table_adapter.py +0 -0
  81. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/async_mysql_transaction_context.py +0 -0
  82. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_ddl_generator.py +0 -0
  83. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_proxy_connection.py +0 -0
  84. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_proxy_cursor.py +0 -0
  85. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_table_adapter.py +0 -0
  86. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_transaction_context.py +0 -0
  87. {deev-2.0.2 → deev-2.1.2}/src/deev/mysql/mysql_type_mapper.py +0 -0
  88. {deev-2.0.2 → deev-2.1.2}/src/deev/py.typed +0 -0
  89. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/__init__.py +0 -0
  90. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_proxy_connection.py +0 -0
  91. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_proxy_cursor.py +0 -0
  92. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_table_adapter.py +0 -0
  93. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/async_sqlite_transaction_context.py +0 -0
  94. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_ddl_generator.py +0 -0
  95. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_proxy_connection.py +0 -0
  96. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_proxy_cursor.py +0 -0
  97. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_table_adapter.py +0 -0
  98. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_transaction_context.py +0 -0
  99. {deev-2.0.2 → deev-2.1.2}/src/deev/sqlite/sqlite_type_mapper.py +0 -0
  100. {deev-2.0.2 → deev-2.1.2}/src/deev/translation/deev_json_decoder.py +0 -0
  101. {deev-2.0.2 → deev-2.1.2}/src/deev/translation/deev_json_encoder.py +0 -0
  102. {deev-2.0.2 → deev-2.1.2}/src/deev/validation/__init__.py +0 -0
  103. {deev-2.0.2 → deev-2.1.2}/src/deev/validation/utils.py +0 -0
  104. {deev-2.0.2 → deev-2.1.2}/src/deev/validation/validation_error.py +0 -0
  105. {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/dependency_links.txt +0 -0
  106. {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/entry_points.txt +0 -0
  107. {deev-2.0.2 → deev-2.1.2}/src/deev.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deev
3
- Version: 2.0.2
3
+ Version: 2.1.2
4
4
  Summary: ..an entity framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
@@ -21,6 +21,8 @@ Provides-Extra: clickhouse
21
21
  Requires-Dist: clickhouse-connect[async]>=1.6.0; extra == "clickhouse"
22
22
  Provides-Extra: mongodb
23
23
  Requires-Dist: pymongo; extra == "mongodb"
24
+ Provides-Extra: mssql
25
+ Requires-Dist: mssql-python>=1.15.0; extra == "mssql"
24
26
  Provides-Extra: mysql
25
27
  Requires-Dist: mysql-connector-python; extra == "mysql"
26
28
  Dynamic: license-file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deev"
3
- version = "2.0.2"
3
+ version = "2.1.2"
4
4
  description = "..an entity framework for Python."
5
5
  keywords = ["DB", "database", "entity framework", "mapper", "sqlite", "mysql", "mongodb", "clickhouse"]
6
6
  authors = [
@@ -27,6 +27,9 @@ clickhouse = [
27
27
  mongodb = [
28
28
  "pymongo"
29
29
  ]
30
+ mssql = [
31
+ "mssql-python (>=1.15.0)"
32
+ ]
30
33
  mysql = [
31
34
  "mysql-connector-python"
32
35
  ]
@@ -21,8 +21,8 @@ from .translation import hydrate, splat
21
21
  from .utils import connect
22
22
 
23
23
 
24
- __version__ = '2.0.2'
25
- __commit__ = 'a337fba'
24
+ __version__ = '2.1.2'
25
+ __commit__ = 'f71bbe9'
26
26
  __all__ = [
27
27
  '__version__',
28
28
  '__commit__',
@@ -36,6 +36,7 @@ __all__ = [
36
36
  'field',
37
37
  'hydrate',
38
38
  'mongodb',
39
+ 'mssql',
39
40
  'mysql',
40
41
  'splat',
41
42
  'translation',
@@ -14,6 +14,7 @@ _DSN_TO_PROVIDER: dict[str, str] = {
14
14
  'mongodb': 'mongodb',
15
15
  'mongodb+srv': 'mongodb',
16
16
  'clickhouse': 'clickhouse',
17
+ 'mssql': 'mssql',
17
18
  }
18
19
 
19
20
 
@@ -0,0 +1,31 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ try:
5
+ from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
6
+ from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
7
+ from .async_mssql_table_adapter import AsyncMSSQLTableAdapter
8
+ from .async_mssql_transaction_context import AsyncMSSQLTransactionContext
9
+ from .mssql_ddl_generator import MSSQLDDLGenerator
10
+ from .mssql_proxy_connection import MSSQLProxyConnection
11
+ from .mssql_proxy_cursor import MSSQLProxyCursor
12
+ from .mssql_table_adapter import MSSQLTableAdapter
13
+ from .mssql_transaction_context import MSSQLTransactionContext
14
+ from .mssql_type_mapper import MSSQLTypeMapper
15
+ except Exception:
16
+ # NOTE: if required packages are not enabled we expect this module to import without errors
17
+ pass
18
+
19
+
20
+ __all__ = [
21
+ 'AsyncMSSQLProxyConnection',
22
+ 'AsyncMSSQLProxyCursor',
23
+ 'AsyncMSSQLTableAdapter',
24
+ 'AsyncMSSQLTransactionContext',
25
+ 'MSSQLDDLGenerator',
26
+ 'MSSQLProxyConnection',
27
+ 'MSSQLProxyCursor',
28
+ 'MSSQLTableAdapter',
29
+ 'MSSQLTransactionContext',
30
+ 'MSSQLTypeMapper'
31
+ ]
@@ -0,0 +1,54 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ from mssql_python import Connection
8
+ from types import TracebackType
9
+ from typing import Any, Literal, Self
10
+
11
+ from ..common.async_db_connection import AsyncDbConnection
12
+ from ..common.async_db_cursor import AsyncDbCursor
13
+ from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
14
+ from .mssql_proxy_connection import MSSQLProxyConnection
15
+
16
+
17
+ class AsyncMSSQLProxyConnection(AsyncDbConnection):
18
+ """
19
+ Async shim that delegates to ``MSSQLProxyConnection``.
20
+
21
+ Since mssql_python has no native async API, all calls are forwarded to the
22
+ synchronous proxy using ``asyncio.to_thread``.
23
+ """
24
+
25
+ __sync_conn: MSSQLProxyConnection
26
+
27
+ def __init__(self, conn: Connection) -> None:
28
+ self.__sync_conn = MSSQLProxyConnection(conn)
29
+
30
+ @property
31
+ def mssql_connection(self) -> MSSQLProxyConnection:
32
+ return self.__sync_conn
33
+
34
+ async def cursor(self, *args: Any, **kwargs: Any) -> AsyncDbCursor:
35
+ return AsyncMSSQLProxyCursor(self.__sync_conn.cursor(*args, **kwargs)) # type: ignore[arg-type]
36
+
37
+ async def commit(self) -> None:
38
+ await asyncio.to_thread(self.__sync_conn.commit)
39
+
40
+ async def rollback(self) -> None:
41
+ await asyncio.to_thread(self.__sync_conn.rollback)
42
+
43
+ async def close(self) -> None:
44
+ await asyncio.to_thread(self.__sync_conn.close)
45
+
46
+ async def __aenter__(self) -> Self:
47
+ return self
48
+
49
+ async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> Literal[False]:
50
+ self.__sync_conn.__exit__(exc_type, exc, tb)
51
+ return False
52
+
53
+
54
+ __all__ = ['AsyncMSSQLProxyConnection']
@@ -0,0 +1,70 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ from mssql_python import Cursor
8
+ from typing import (
9
+ Any,
10
+ Sequence,
11
+ cast,
12
+ )
13
+
14
+ from ..common.async_db_cursor import AsyncDbCursor, AsyncDbCursorDescription
15
+ from ..common.db_parameters import DbParameters
16
+ from .mssql_proxy_cursor import MSSQLProxyCursor
17
+
18
+
19
+ class AsyncMSSQLProxyCursor(AsyncDbCursor):
20
+ """
21
+ Async shim that delegates to ``MSSQLProxyCursor``.
22
+
23
+ Wraps the synchronous deev cursor proxy and exposes an async API
24
+ using ``asyncio.to_thread`` for underlying mssql_python operations.
25
+ """
26
+ __sync_cursor: MSSQLProxyCursor
27
+ __cursor: Cursor
28
+ __sql_arg_expect: str
29
+ __sql_arg_subst: str
30
+
31
+ def __init__(self, provider_cursor: Cursor) -> None:
32
+ self.__cursor = provider_cursor
33
+ self.__sync_cursor = MSSQLProxyCursor(provider_cursor)
34
+ self.__sql_arg_expect = '%?'
35
+ self.__sql_arg_subst = '?'
36
+
37
+ @property
38
+ def description(self) -> AsyncDbCursorDescription:
39
+ return cast(AsyncDbCursorDescription, self.__cursor.description)
40
+
41
+ @property
42
+ def rowcount(self) -> int:
43
+ return self.__cursor.rowcount
44
+
45
+ async def execute(self, operation: str, parameters: DbParameters | None = None) -> None:
46
+ if parameters is not None:
47
+ operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
48
+ if parameters is None:
49
+ self.__cursor.execute(operation)
50
+ else:
51
+ self.__cursor.execute(operation, parameters)
52
+
53
+ async def executemany(self, operation: str, seq_of_parameters: Sequence[DbParameters]) -> None:
54
+ operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
55
+ self.__cursor.executemany(operation, [tuple(p) if isinstance(p, (tuple, list)) else dict(p) for p in seq_of_parameters]) # type: ignore[arg-type]
56
+
57
+ async def fetchone(self) -> tuple[Any, ...] | None:
58
+ return cast(tuple[Any, ...] | None, self.__cursor.fetchone())
59
+
60
+ async def fetchmany(self, size: int = 1) -> list[tuple[Any, ...]]:
61
+ return cast(list[tuple[Any, ...]], self.__cursor.fetchmany(size))
62
+
63
+ async def fetchall(self) -> list[tuple[Any, ...]]:
64
+ return cast(list[tuple[Any, ...]], self.__cursor.fetchall())
65
+
66
+ async def close(self) -> None:
67
+ await asyncio.to_thread(self.__cursor.close)
68
+
69
+
70
+ __all__ = ['AsyncMSSQLProxyCursor']
@@ -0,0 +1,146 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, AsyncGenerator, TypeVar, cast, get_args, get_origin
7
+
8
+ from ..common.async_db_table_adapter import AsyncDbTableAdapter
9
+ from ..common.db_context import AsyncDbContext
10
+ from ..common.db_parameters import DbParameters
11
+ from ..entities import get_entity_spec
12
+ from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
13
+ from .async_mssql_transaction_context import AsyncMSSQLTransactionContext
14
+ from .mssql_proxy_connection import MSSQLProxyConnection
15
+ from .mssql_table_adapter import MSSQLTableAdapter
16
+ from .mssql_type_mapper import MSSQLTypeMapper
17
+
18
+ TEntity = TypeVar('TEntity')
19
+
20
+
21
+ class AsyncMSSQLTableAdapter(AsyncDbTableAdapter[TEntity]):
22
+ """
23
+ Async shim that delegates to ``MSSQLTableAdapter``.
24
+
25
+ Since mssql_python has no native async API, all calls are forwarded to the
26
+ synchronous table adapter using ``asyncio.to_thread``.
27
+ """
28
+
29
+ __sync_adapter: MSSQLTableAdapter[TEntity]
30
+ __context: AsyncDbContext
31
+ __create_table: bool
32
+ __table_name: str | None
33
+
34
+ def __init__(
35
+ self,
36
+ context: AsyncDbContext,
37
+ *,
38
+ create_table: bool = False,
39
+ table_name: str | None = None
40
+ ) -> None:
41
+ self.__context = context if isinstance(context, (AsyncMSSQLProxyConnection, AsyncMSSQLTransactionContext)) else AsyncMSSQLProxyConnection(context) # type: ignore[arg-type]
42
+ self.__create_table = create_table is True
43
+ self.__table_name = table_name
44
+ self.__is_initialized = False
45
+
46
+ async def __deferred_init(self) -> None:
47
+ if not self.__is_initialized:
48
+ self.__sync_deferred_init()
49
+ self.__is_initialized = True
50
+ if self.__create_table is True:
51
+ await self.create_table()
52
+
53
+ def __sync_deferred_init(self) -> None:
54
+ entity_type = self.__get_typearg(self)
55
+ self.__entity_spec = get_entity_spec(entity_type)
56
+ self.__column_names = ', '.join([f'[{k}]' for k in self.__entity_spec.fields.keys()])
57
+ self.__dbtype_mapper = MSSQLTypeMapper(self.__entity_spec)
58
+
59
+ self.__sync_adapter = MSSQLTableAdapter[entity_type]( # type: ignore[valid-type]
60
+ self.mssql_connection,
61
+ create_table=self.__create_table,
62
+ table_name=self.__table_name,
63
+ )
64
+
65
+ @property
66
+ def primary_key(self) -> tuple[str, ...]:
67
+ if not hasattr(self, '_AsyncMSSQLTableAdapter__sync_adapter'):
68
+ self.__sync_deferred_init()
69
+ return self.__sync_adapter.primary_key
70
+
71
+ def __get_typearg(self, obj: object) -> type:
72
+ orig = getattr(obj, '__orig_class__', None)
73
+ if orig is not None:
74
+ args = get_args(orig)
75
+ if args:
76
+ return args[0]
77
+ for base in obj.__class__.__mro__:
78
+ for generic_base in getattr(base, '__orig_bases__', ()):
79
+ if get_origin(generic_base) is AsyncMSSQLTableAdapter:
80
+ args = get_args(generic_base)
81
+ if args is not None and len(args) > 0:
82
+ return args[0]
83
+ raise RuntimeError(
84
+ f'Could not determine the entity type for {obj.__class__.__qualname__}. '
85
+ 'Instantiate via the generic alias, e.g. AsyncMSSQLTableAdapter[MyEntity]().'
86
+ )
87
+
88
+ @property
89
+ def mssql_connection(self) -> MSSQLProxyConnection:
90
+ if isinstance(self.__context, AsyncMSSQLProxyConnection):
91
+ return self.__context.mssql_connection
92
+ elif isinstance(self.__context, AsyncMSSQLTransactionContext):
93
+ return cast(AsyncMSSQLProxyConnection, self.__context.connection).mssql_connection
94
+ raise RuntimeError(f'unsupported context type: {self.__context}')
95
+
96
+ async def create_table(self) -> None:
97
+ await self.__deferred_init()
98
+ self.__sync_adapter.create_table()
99
+ self.__create_table = False
100
+
101
+ async def create(self, entity: TEntity | None = None, **kwargs: Any) -> dict[str, Any]:
102
+ await self.__deferred_init()
103
+ return self.__sync_adapter.create(entity, **kwargs)
104
+
105
+ async def read(self, **kwargs: Any) -> TEntity | None:
106
+ await self.__deferred_init()
107
+ return self.__sync_adapter.read(**kwargs)
108
+
109
+ async def update(self, entity: TEntity) -> None:
110
+ await self.__deferred_init()
111
+ self.__sync_adapter.update(entity)
112
+
113
+ async def delete(self, **kwargs: Any) -> None:
114
+ await self.__deferred_init()
115
+ self.__sync_adapter.delete(**kwargs)
116
+
117
+ async def exists(self, **kwargs: Any) -> bool:
118
+ await self.__deferred_init()
119
+ return self.__sync_adapter.exists(**kwargs)
120
+
121
+ async def upsert(self, entity: TEntity) -> dict[str, Any]:
122
+ await self.__deferred_init()
123
+ return self.__sync_adapter.upsert(entity)
124
+
125
+ async def query( # type: ignore[override]
126
+ self,
127
+ where: str | None = None,
128
+ parameters: DbParameters | None = None,
129
+ orderby: str | None = None,
130
+ limit: int | None = None
131
+ ) -> AsyncGenerator[TEntity, None]:
132
+ await self.__deferred_init()
133
+ sync_gen = self.__sync_adapter.query(where, parameters, orderby, limit)
134
+ for e in sync_gen:
135
+ yield e
136
+
137
+ async def commit(self) -> None:
138
+ await self.__deferred_init()
139
+ self.__sync_adapter.commit()
140
+
141
+ async def rollback(self) -> None:
142
+ await self.__deferred_init()
143
+ self.__sync_adapter.rollback()
144
+
145
+
146
+ __all__ = ['AsyncMSSQLTableAdapter']
@@ -0,0 +1,135 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ from types import TracebackType
8
+ from typing import Any, AsyncGenerator, Literal, Self, cast
9
+
10
+ from ..common.async_db_connection import AsyncDbConnection
11
+ from ..common.async_db_cursor import AsyncDbCursor
12
+ from ..common.async_db_transaction_context import AsyncDbTransactionContext
13
+ from ..common.db_context import AsyncDbContext
14
+ from ..common.db_parameters import DbParameters
15
+ from .async_mssql_proxy_connection import AsyncMSSQLProxyConnection
16
+ from .async_mssql_proxy_cursor import AsyncMSSQLProxyCursor
17
+ from .mssql_transaction_context import MSSQLTransactionContext
18
+
19
+
20
+ class AsyncMSSQLTransactionContext(AsyncDbTransactionContext):
21
+ """
22
+ Async shim that delegates to ``MSSQLTransactionContext``.
23
+
24
+ Wraps the synchronous transaction context and exposes an async API
25
+ using direct method calls — no ``asyncio.to_thread``.
26
+ """
27
+
28
+ __context: AsyncDbContext | None
29
+ __inner_ctx: MSSQLTransactionContext | None
30
+ __owns_context: bool
31
+
32
+ def __init__(self, context: AsyncDbContext, *, owns_context: bool | None = None) -> None:
33
+ self.__owns_context = owns_context is True
34
+ self.__is_deev_context = isinstance(context, (AsyncMSSQLProxyConnection, AsyncMSSQLTransactionContext))
35
+ self.__context = context if self.__is_deev_context else AsyncMSSQLProxyConnection(context) # type: ignore[arg-type]
36
+ self.__inner_ctx = None
37
+
38
+ def __del__(self) -> None:
39
+ try:
40
+ try:
41
+ loop = asyncio.get_running_loop()
42
+ except RuntimeError:
43
+ loop = None
44
+ if loop and loop.is_running():
45
+ loop.create_task(self.close())
46
+ else:
47
+ asyncio.run(self.close())
48
+ except Exception:
49
+ pass
50
+
51
+ def __get_inner_ctx(self) -> MSSQLTransactionContext:
52
+ """Return the underlying sync transaction context, acquiring it if necessary."""
53
+ if self.__inner_ctx is None:
54
+ self.__inner_ctx = MSSQLTransactionContext(
55
+ context=cast(AsyncMSSQLProxyConnection, self.__context).mssql_connection,
56
+ owns_context=self.__owns_context
57
+ )
58
+ return self.__inner_ctx
59
+
60
+ async def __aenter__(self) -> Self:
61
+ innser_ctx = self.__get_inner_ctx()
62
+ innser_ctx.__enter__()
63
+ return self
64
+
65
+ async def __aexit__(
66
+ self,
67
+ exc_type: type[BaseException] | None = None,
68
+ exc_value: BaseException | None = None,
69
+ traceback: TracebackType | None = None
70
+ ) -> Literal[False]:
71
+ if self.__inner_ctx is not None:
72
+ self.__inner_ctx.__exit__(exc_type, exc_value, traceback)
73
+ await self.close()
74
+ return False
75
+
76
+ @property
77
+ def connection(self) -> AsyncDbConnection:
78
+ if isinstance(self.__context, AsyncDbTransactionContext):
79
+ return cast(AsyncDbTransactionContext, self.__context).connection
80
+ else:
81
+ return cast(AsyncDbConnection, self.__context)
82
+
83
+ @property
84
+ def transaction_name(self) -> str | None:
85
+ return self.__get_inner_ctx().transaction_name
86
+
87
+ @property
88
+ def savepoints(self) -> tuple[str, ...]:
89
+ return self.__get_inner_ctx().savepoints
90
+
91
+ async def begin_transaction(self, name: str | None = None) -> Self:
92
+ self.__get_inner_ctx().begin_transaction(name)
93
+ return self
94
+
95
+ async def create_savepoint(self, name: str | None = None) -> Self:
96
+ self.__get_inner_ctx().create_savepoint(name)
97
+ return self
98
+
99
+ async def rollback_savepoint(self, name: str | None = None) -> None:
100
+ self.__get_inner_ctx().rollback_savepoint(name)
101
+
102
+ async def close(self) -> None:
103
+ if self.__inner_ctx is not None:
104
+ self.__inner_ctx.close()
105
+
106
+ async def commit(self) -> None:
107
+ self.__get_inner_ctx().commit()
108
+
109
+ async def cursor(self) -> Any:
110
+ sync_cursor = self.__get_inner_ctx().cursor()
111
+ raw_cursor = sync_cursor._MSSQLProxyCursor__cursor # type: ignore[attr-defined]
112
+ return AsyncMSSQLProxyCursor(raw_cursor)
113
+
114
+ async def execute(self, sql: str, parameters: DbParameters | None = None, raw: bool | None = False) -> Any:
115
+ return self.__get_inner_ctx().execute(sql, parameters, raw)
116
+
117
+ async def execute_nonquery(self, sql: str, parameters: DbParameters | None = None) -> None:
118
+ self.__get_inner_ctx().execute_nonquery(sql, parameters)
119
+
120
+ async def execute_reader(self, sql: str, parameters: DbParameters | None = None) -> AsyncGenerator[tuple[Any, ...], None]: # type: ignore[override]
121
+ sync_gen = self.__get_inner_ctx().execute_reader(sql, parameters)
122
+ for row in sync_gen:
123
+ yield row
124
+
125
+ async def execute_scalar(self, sql: str, parameters: DbParameters | None = None) -> Any:
126
+ return self.__get_inner_ctx().execute_scalar(sql, parameters)
127
+
128
+ async def execute_script(self, sql: str, raw: bool = False) -> None:
129
+ self.__get_inner_ctx().execute_script(sql, raw)
130
+
131
+ async def rollback(self, name: str | None = None) -> None:
132
+ self.__get_inner_ctx().rollback(name)
133
+
134
+
135
+ __all__ = ['AsyncMSSQLTransactionContext']
@@ -0,0 +1,91 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ import hanaro
7
+ import logging
8
+
9
+ from ..entities import EntitySpec, IndexOrder
10
+ from .mssql_type_mapper import MSSQLTypeMapper
11
+
12
+
13
+ class MSSQLDDLGenerator:
14
+ """
15
+ Generates Data Definition Language (DDL) statements for Microsoft SQL Server.
16
+ """
17
+
18
+ __logger: logging.Logger
19
+
20
+ def __init__(
21
+ self,
22
+ ) -> None:
23
+ self.__logger = hanaro.get_logger()
24
+
25
+ def __generate_table_indexes_ddl(self, entity_spec: EntitySpec, table_name: str) -> list[str]:
26
+ """
27
+ Build ``CREATE INDEX`` statements for all secondary indexes.
28
+
29
+ :param entity_spec: The entity specification containing index metadata.
30
+ :param table_name: The target table name.
31
+ :returns: A list of ``CREATE INDEX`` SQL statements.
32
+ """
33
+ ddl = list[str]()
34
+ direction_map = {IndexOrder.ASCENDING: 'ASC', IndexOrder.DESCENDING: 'DESC'}
35
+ groups: dict[str, list[tuple[str, IndexOrder]]] = {}
36
+ for field_name, spec in entity_spec.fields.items():
37
+ if spec.index is None:
38
+ continue
39
+ idx_name = spec.index.name
40
+ direction = spec.index.direction or IndexOrder.ASCENDING
41
+ groups.setdefault(idx_name, []).append((field_name, direction))
42
+
43
+ for idx_name, col_specs in sorted(groups.items()):
44
+ col_specs.sort(key=lambda cs: cs[0]) # type: ignore
45
+ cols = ', '.join(f'[{name}] {direction_map[direction]}' for name, direction in col_specs)
46
+ ddl.append(f'CREATE INDEX [{idx_name}] ON [{table_name}] ({cols})')
47
+ return ddl
48
+
49
+ def generate_table_ddl(
50
+ self,
51
+ *,
52
+ entity_spec: EntitySpec,
53
+ table_name: str | None = None
54
+ ) -> list[str]:
55
+ """
56
+ Generate DDL statements to create the table and its indexes.
57
+
58
+ :param entity_spec: The entity specification describing the table structure.
59
+ :param table_name: Optional override for the table name.
60
+ :returns: A list of DDL SQL statements.
61
+ """
62
+ db_type_mapper = MSSQLTypeMapper(entity_spec)
63
+ ddl = list[str]()
64
+ table_name = entity_spec.table_name if table_name is None else table_name
65
+ if len(entity_spec.primary_key) == 1:
66
+ primary_key = entity_spec.primary_key[0]
67
+ id_dbtype = db_type_mapper.get_provider_type(primary_key)
68
+ columns = ', '.join([
69
+ f'[{k}] {db_type_mapper.get_provider_type(k)}'
70
+ for k in entity_spec.attrs.keys()
71
+ if k != primary_key
72
+ ])
73
+ auto_increment = f' IDENTITY(1,1)' if id_dbtype in ('INT', 'BIGINT', 'SMALLINT') else ''
74
+ ddl.append(f'CREATE TABLE [{table_name}] ({primary_key} {id_dbtype}{auto_increment}, {columns}, PRIMARY KEY ({primary_key}))')
75
+ else:
76
+ columns = ', '.join([
77
+ f'[{k}] {db_type_mapper.get_provider_type(k)}'
78
+ for k in entity_spec.attrs.keys()
79
+ ])
80
+ primary_key = (
81
+ f", PRIMARY KEY ({','.join(entity_spec.primary_key)})"
82
+ if len(entity_spec.primary_key) > 0
83
+ else ''
84
+ )
85
+ ddl.append(f'CREATE TABLE [{table_name}] ({columns}{primary_key})')
86
+ for e in self.__generate_table_indexes_ddl(entity_spec, table_name):
87
+ ddl.append(e)
88
+ return ddl
89
+
90
+
91
+ __all__ = ['MSSQLDDLGenerator']
@@ -0,0 +1,73 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ from mssql_python import Connection
7
+ from types import TracebackType
8
+ from typing import Any, Literal, Self
9
+
10
+ from ..common.db_connection import DbConnection
11
+ from ..common.db_cursor import DbCursor
12
+ from .mssql_proxy_cursor import MSSQLProxyCursor
13
+
14
+
15
+ class MSSQLProxyConnection(DbConnection):
16
+ """
17
+ DB-API 2.0 compliant connection interface for ``mssql_python``.
18
+ """
19
+
20
+ __connection: Connection
21
+
22
+ def __init__(self, provider_connection: Connection) -> None:
23
+ """
24
+ Initialize a new MSSQLProxyConnection.
25
+
26
+ :param provider_connection: A raw mssql_python Connection instance.
27
+ """
28
+ self.__connection = provider_connection
29
+
30
+ @property
31
+ def mssql_connection(self) -> Connection:
32
+ """
33
+ Access the underlying raw mssql_python connection.
34
+
35
+ :returns: The :class:`mssql_python.Connection` instance.
36
+ """
37
+ return self.__connection
38
+
39
+ def cursor(self, *args: Any, **kwargs: Any) -> DbCursor:
40
+ """
41
+ Create a new cursor for executing SQL statements.
42
+
43
+ :returns: A :class:`MSSQLProxyCursor` instance.
44
+ """
45
+ return MSSQLProxyCursor(self.__connection.cursor(*args, **kwargs))
46
+
47
+ def commit(self) -> None:
48
+ """Commit the current transaction."""
49
+ self.__connection.commit()
50
+
51
+ def rollback(self) -> None:
52
+ """Rollback the current transaction."""
53
+ self.__connection.rollback()
54
+
55
+ def close(self) -> None:
56
+ """Close the database connection."""
57
+ self.__connection.close()
58
+
59
+ def __enter__(self) -> Self:
60
+ return self
61
+
62
+ def __exit__(
63
+ self,
64
+ exc_type: type[BaseException] | None,
65
+ exc: BaseException | None,
66
+ tb: TracebackType | None,
67
+ /
68
+ ) -> Literal[False]:
69
+ self.__connection.__exit__(exc_type, exc, tb) # type: ignore[arg-type]
70
+ return False
71
+
72
+
73
+ __all__ = ['MSSQLProxyConnection']