soda-sqlserver 4.21.0__tar.gz → 4.22.0__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.
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/PKG-INFO +2 -2
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/pyproject.toml +2 -2
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver/common/data_sources/sqlserver_data_source.py +2 -1
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver/common/data_sources/sqlserver_data_source_connection.py +56 -1
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/PKG-INFO +2 -2
- soda_sqlserver-4.22.0/src/soda_sqlserver.egg-info/requires.txt +2 -0
- soda_sqlserver-4.21.0/src/soda_sqlserver.egg-info/requires.txt +0 -2
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/setup.cfg +0 -0
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver/test_helpers/sqlserver_data_source_test_helper.py +0 -0
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/SOURCES.txt +0 -0
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/dependency_links.txt +0 -0
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/entry_points.txt +0 -0
- {soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soda-sqlserver
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.22.0
|
|
4
4
|
Summary: Soda SQL Server V4
|
|
5
5
|
Author-email: "Soda Data N.V." <info@soda.io>
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: soda-core==4.
|
|
8
|
+
Requires-Dist: soda-core==4.22.0
|
|
9
9
|
Requires-Dist: pyodbc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "soda-sqlserver"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.22.0"
|
|
4
4
|
description = "Soda SQL Server V4"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
license = {text = "Proprietary"}
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
{name = "Soda Data N.V.", email = "info@soda.io"}
|
|
9
9
|
]
|
|
10
10
|
dependencies = [
|
|
11
|
-
"soda-core==4.
|
|
11
|
+
"soda-core==4.22.0",
|
|
12
12
|
"pyodbc",
|
|
13
13
|
]
|
|
14
14
|
|
|
@@ -318,7 +318,8 @@ class SqlServerSqlDialect(SqlDialect, sqlglot_dialect="tsql"):
|
|
|
318
318
|
# see: https://learn.microsoft.com/en-us/sql/relational-databases/collations/collation-and-unicode-support?view=sql-server-ver17
|
|
319
319
|
# CS: Case sensitive; AS: Accent sensitive
|
|
320
320
|
# The default is SQL_Latin1_General_Cp1_CI_AS (case-insensitive), we replcae with a case sensitive collation
|
|
321
|
-
|
|
321
|
+
pattern_literal: str = self.literal_string(f"%{regex_pattern}%")
|
|
322
|
+
return f"PATINDEX ({pattern_literal}, {expression} COLLATE SQL_Latin1_General_Cp1_CS_AS) > 0"
|
|
322
323
|
|
|
323
324
|
def supports_regex_advanced(self) -> bool:
|
|
324
325
|
return False
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import random
|
|
4
5
|
import struct
|
|
6
|
+
import time
|
|
5
7
|
from abc import ABC
|
|
6
8
|
from datetime import datetime, timedelta, timezone, tzinfo
|
|
7
|
-
from typing import Any, Literal, Optional, Union
|
|
9
|
+
from typing import Any, Callable, Literal, Optional, TypeVar, Union
|
|
8
10
|
|
|
9
11
|
import pyodbc
|
|
10
12
|
from pydantic import Field, SecretStr
|
|
11
13
|
from soda_core.__version__ import SODA_CORE_VERSION
|
|
12
14
|
from soda_core.common.data_source_connection import DataSourceConnection
|
|
15
|
+
from soda_core.common.data_source_results import QueryResult
|
|
13
16
|
from soda_core.common.exceptions import DataSourceConnectionException
|
|
14
17
|
from soda_core.common.logging_constants import soda_logger
|
|
15
18
|
from soda_core.model.data_source.data_source import DataSourceBase
|
|
@@ -20,6 +23,9 @@ from soda_core.model.data_source.data_source_connection_properties import (
|
|
|
20
23
|
logger: logging.Logger = soda_logger
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
CONTEXT_AUTHENTICATION_DESCRIPTION = "Use context authentication"
|
|
24
30
|
USER_DESCRIPTION = "Username for authentication"
|
|
25
31
|
DEFAULT_PORT = 1433
|
|
@@ -106,6 +112,16 @@ def handle_datetimeoffset(dto_value):
|
|
|
106
112
|
|
|
107
113
|
|
|
108
114
|
class SqlServerDataSourceConnection(DataSourceConnection):
|
|
115
|
+
# SQL Server resolves lock conflicts by killing one participant with error 1205
|
|
116
|
+
# (SQLSTATE 40001) and rolling its transaction back; Microsoft's guidance is to
|
|
117
|
+
# rerun the killed transaction. Concurrent DDL and catalog scans in the same
|
|
118
|
+
# database can deadlock on the system base tables, so statements are retried a
|
|
119
|
+
# few times before the error is propagated. The pause is drawn uniformly from
|
|
120
|
+
# an exponentially growing window (full jitter) so that the parties of a killed
|
|
121
|
+
# deadlock don't retry in lockstep and immediately deadlock again.
|
|
122
|
+
DEADLOCK_MAX_ATTEMPTS: int = 3
|
|
123
|
+
DEADLOCK_RETRY_BACKOFF_SECONDS: float = 0.1
|
|
124
|
+
|
|
109
125
|
def __init__(self, name: str, connection_properties: DataSourceConnectionProperties):
|
|
110
126
|
# Set before super().__init__(), which auto-opens the connection and
|
|
111
127
|
# populates these from the live server in _create_connection.
|
|
@@ -113,6 +129,45 @@ class SqlServerDataSourceConnection(DataSourceConnection):
|
|
|
113
129
|
self.engine_edition: Optional[int] = None
|
|
114
130
|
super().__init__(name, connection_properties)
|
|
115
131
|
|
|
132
|
+
def execute_query(self, sql: str, log_query: bool = True) -> QueryResult:
|
|
133
|
+
execute = super().execute_query
|
|
134
|
+
return self._execute_with_deadlock_retry(lambda: execute(sql, log_query))
|
|
135
|
+
|
|
136
|
+
def execute_update(self, sql: str, log_query: bool = True) -> int:
|
|
137
|
+
execute = super().execute_update
|
|
138
|
+
return self._execute_with_deadlock_retry(lambda: execute(sql, log_query))
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _is_deadlock_error(e: pyodbc.Error) -> bool:
|
|
142
|
+
# pyodbc error args are (sqlstate, message); 40001 is the serialization
|
|
143
|
+
# failure SQLSTATE that SQL Server raises for deadlock victims (1205).
|
|
144
|
+
return bool(e.args) and e.args[0] == "40001"
|
|
145
|
+
|
|
146
|
+
def _execute_with_deadlock_retry(self, operation: Callable[[], T]) -> T:
|
|
147
|
+
"""Assumes ``operation`` is a single-statement unit of work: recovery
|
|
148
|
+
issues a connection-wide rollback, discarding any earlier uncommitted
|
|
149
|
+
statements on this connection (SQL Server runs autocommit=False).
|
|
150
|
+
Only the buffered paths (execute_query/execute_update) are wrapped;
|
|
151
|
+
the callback/iterator paths (execute_query_one_by_one*,
|
|
152
|
+
execute_query_iterate) may have already delivered rows when a deadlock
|
|
153
|
+
strikes mid-fetch, so re-executing them would double-process rows."""
|
|
154
|
+
for attempt in range(1, max(self.DEADLOCK_MAX_ATTEMPTS, 1) + 1):
|
|
155
|
+
try:
|
|
156
|
+
return operation()
|
|
157
|
+
except pyodbc.Error as e:
|
|
158
|
+
if not self._is_deadlock_error(e) or attempt == self.DEADLOCK_MAX_ATTEMPTS:
|
|
159
|
+
raise
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"Deadlock victim on '{self.name}' "
|
|
162
|
+
f"(attempt {attempt}/{self.DEADLOCK_MAX_ATTEMPTS}), retrying: {e}"
|
|
163
|
+
)
|
|
164
|
+
try:
|
|
165
|
+
self.rollback()
|
|
166
|
+
except Exception as rollback_error:
|
|
167
|
+
logger.warning(f"Rollback after deadlock on '{self.name}' failed: {rollback_error}")
|
|
168
|
+
backoff_cap = self.DEADLOCK_RETRY_BACKOFF_SECONDS * (2 ** (attempt - 1))
|
|
169
|
+
time.sleep(random.uniform(0, backoff_cap))
|
|
170
|
+
|
|
116
171
|
# Normalize pyodbc.Row objects so downstream consumers see plain tuples.
|
|
117
172
|
def _format_rows(self, rows: list[tuple]) -> list[tuple]:
|
|
118
173
|
return [self._format_row(row) for row in rows]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soda-sqlserver
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.22.0
|
|
4
4
|
Summary: Soda SQL Server V4
|
|
5
5
|
Author-email: "Soda Data N.V." <info@soda.io>
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: soda-core==4.
|
|
8
|
+
Requires-Dist: soda-core==4.22.0
|
|
9
9
|
Requires-Dist: pyodbc
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{soda_sqlserver-4.21.0 → soda_sqlserver-4.22.0}/src/soda_sqlserver.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|