weaverstack 0.1.1__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.
- weaver/__init__.py +59 -0
- weaver/build_bundle/__init__.py +109 -0
- weaver/build_bundle/aliases.py +325 -0
- weaver/build_bundle/bundle.py +359 -0
- weaver/build_bundle/catalogue_actions.py +275 -0
- weaver/build_bundle/changes.py +186 -0
- weaver/build_bundle/endpoints.py +83 -0
- weaver/build_bundle/executors/__init__.py +69 -0
- weaver/build_bundle/executors/alias.py +202 -0
- weaver/build_bundle/executors/base.py +132 -0
- weaver/build_bundle/executors/folder.py +71 -0
- weaver/build_bundle/executors/load_file.py +205 -0
- weaver/build_bundle/executors/spark_case.py +26 -0
- weaver/build_bundle/executors/spark_schema.py +60 -0
- weaver/build_bundle/executors/spark_sql.py +59 -0
- weaver/build_bundle/executors/spark_sql_batch.py +57 -0
- weaver/build_bundle/executors/spark_table.py +213 -0
- weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
- weaver/build_bundle/executors/tsql.py +81 -0
- weaver/build_bundle/incremental.py +288 -0
- weaver/build_bundle/installer.py +384 -0
- weaver/build_bundle/models.py +288 -0
- weaver/build_bundle/payloads.py +34 -0
- weaver/build_bundle/physical.py +625 -0
- weaver/build_bundle/planner.py +389 -0
- weaver/build_bundle/prune.py +620 -0
- weaver/build_bundle/report.py +108 -0
- weaver/build_bundle/stages.py +196 -0
- weaver/build_bundle/targets.py +272 -0
- weaver/build_bundle/workflow.py +585 -0
- weaver/catalogue/__init__.py +73 -0
- weaver/catalogue/builtin.py +238 -0
- weaver/catalogue/claims.py +121 -0
- weaver/catalogue/projection.py +437 -0
- weaver/catalogue/reader.py +152 -0
- weaver/catalogue/reconcile.py +231 -0
- weaver/catalogue/render.py +410 -0
- weaver/catalogue/state.py +660 -0
- weaver/catalogue/tables.py +648 -0
- weaver/config.py +178 -0
- weaver/declaration/__init__.py +171 -0
- weaver/declaration/columns.py +223 -0
- weaver/declaration/ddl.py +266 -0
- weaver/declaration/dependencies.py +544 -0
- weaver/declaration/graph.py +240 -0
- weaver/declaration/item_dependencies.py +292 -0
- weaver/declaration/load.py +191 -0
- weaver/declaration/metadata.py +1405 -0
- weaver/declaration/model.py +448 -0
- weaver/declaration/references.py +294 -0
- weaver/declaration/repository.py +959 -0
- weaver/declaration/schemas.py +135 -0
- weaver/declaration/source.py +674 -0
- weaver/declaration/spark_load.py +759 -0
- weaver/declaration/sql_shaping.py +591 -0
- weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
- weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
- weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
- weaver/declaration/templates/load/column_metadata.sql +40 -0
- weaver/declaration/templates/load/full_replace_body.sql +21 -0
- weaver/declaration/templates/load/install_load_procedure.sql +27 -0
- weaver/declaration/templates/load/load_procedure.sql +48 -0
- weaver/declaration/templates/load/primary_key_body.sql +113 -0
- weaver/declaration/tsql_ddl.py +468 -0
- weaver/declaration/tsql_load.py +417 -0
- weaver/declaration/warehouse_type_mapping.yml +93 -0
- weaver/diagnostics.py +247 -0
- weaver/errors.py +61 -0
- weaver/etl.py +469 -0
- weaver/fabric/__init__.py +107 -0
- weaver/fabric/auth.py +137 -0
- weaver/fabric/capacity.py +143 -0
- weaver/fabric/client.py +147 -0
- weaver/fabric/environment.py +460 -0
- weaver/fabric/livy.py +478 -0
- weaver/fabric/notebooks.py +201 -0
- weaver/fabric/onelake.py +263 -0
- weaver/fabric/resolution.py +344 -0
- weaver/fabric/resources.py +245 -0
- weaver/fabric/session.py +148 -0
- weaver/fabric/shortcuts.py +120 -0
- weaver/fabric/sql.py +118 -0
- weaver/fabric/store.py +198 -0
- weaver/initialise.py +209 -0
- weaver/lakehouse.py +386 -0
- weaver/load.py +474 -0
- weaver/load_execution.py +483 -0
- weaver/load_plan.py +912 -0
- weaver/load_report.py +330 -0
- weaver/load_resolution.py +386 -0
- weaver/locations.py +164 -0
- weaver/objects.py +392 -0
- weaver/operations.py +757 -0
- weaver/physical_wipe.py +369 -0
- weaver/push.py +76 -0
- weaver/resolution.py +292 -0
- weaver/runtime/__init__.py +30 -0
- weaver/runtime/folder_load.py +402 -0
- weaver/runtime/load_contract.py +245 -0
- weaver/runtime/load_result.py +104 -0
- weaver/runtime/spark_load.py +152 -0
- weaver/runtime/table_load.py +497 -0
- weaver/spark/__init__.py +49 -0
- weaver/spark/catalogue.py +245 -0
- weaver/spark/destination.py +195 -0
- weaver/spark/session.py +84 -0
- weaver/spark/tokens.py +138 -0
- weaver/sql/__init__.py +40 -0
- weaver/sql/authentication.py +38 -0
- weaver/sql/connection.py +90 -0
- weaver/sql/errors.py +25 -0
- weaver/sql/execution.py +123 -0
- weaver/sql/pool.py +174 -0
- weaver/sql/wipe.py +156 -0
- weaver/store.py +209 -0
- weaver/targets.py +257 -0
- weaver/task_logging.py +215 -0
- weaver/unbind.py +74 -0
- weaver/workspaces.py +175 -0
- weaver_cli/__init__.py +12 -0
- weaver_cli/__main__.py +7 -0
- weaver_cli/main.py +626 -0
- weaverstack-0.1.1.dist-info/METADATA +113 -0
- weaverstack-0.1.1.dist-info/RECORD +127 -0
- weaverstack-0.1.1.dist-info/WHEEL +4 -0
- weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
- weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
weaver/sql/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Driver-neutral Warehouse SQL execution backed by ``mssql-python``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .authentication import AccessTokenAuthentication, SqlAuthentication
|
|
6
|
+
from .connection import SqlEndpoint, build_connection_string, connect
|
|
7
|
+
from .errors import (
|
|
8
|
+
SqlConnectionError,
|
|
9
|
+
SqlError,
|
|
10
|
+
SqlExecutionError,
|
|
11
|
+
SqlPoolClosedError,
|
|
12
|
+
)
|
|
13
|
+
from .execution import PooledSqlExecutor, SqlExecutor, SqlRow
|
|
14
|
+
from .pool import (
|
|
15
|
+
DEFAULT_MAX_CONNECTIONS,
|
|
16
|
+
SqlConnectionLease,
|
|
17
|
+
SqlConnectionPool,
|
|
18
|
+
SqlPoolRegistry,
|
|
19
|
+
)
|
|
20
|
+
from .wipe import generate_warehouse_wipe_sql
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AccessTokenAuthentication",
|
|
24
|
+
"SqlAuthentication",
|
|
25
|
+
"SqlEndpoint",
|
|
26
|
+
"build_connection_string",
|
|
27
|
+
"connect",
|
|
28
|
+
"SqlError",
|
|
29
|
+
"SqlConnectionError",
|
|
30
|
+
"SqlExecutionError",
|
|
31
|
+
"SqlPoolClosedError",
|
|
32
|
+
"SqlExecutor",
|
|
33
|
+
"SqlRow",
|
|
34
|
+
"PooledSqlExecutor",
|
|
35
|
+
"SqlConnectionLease",
|
|
36
|
+
"SqlConnectionPool",
|
|
37
|
+
"SqlPoolRegistry",
|
|
38
|
+
"DEFAULT_MAX_CONNECTIONS",
|
|
39
|
+
"generate_warehouse_wipe_sql",
|
|
40
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Authentication records consumed by the common SQL connector.
|
|
2
|
+
|
|
3
|
+
Identity policy stays at the caller boundary. This module only converts a
|
|
4
|
+
fresh access token into the connection argument understood by
|
|
5
|
+
``mssql-python``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
from collections.abc import Callable, Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
# SQL_COPT_SS_ACCESS_TOKEN from msodbcsql.h. mssql-python accepts the same
|
|
16
|
+
# attrs_before shape as pyodbc and exposes this value as
|
|
17
|
+
# ConstantsDDBC.SQL_COPT_SS_ACCESS_TOKEN.
|
|
18
|
+
SQL_ACCESS_TOKEN_ATTRIBUTE = 1256
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SqlAuthentication(Protocol):
|
|
22
|
+
"""Authentication material for one new physical connection."""
|
|
23
|
+
|
|
24
|
+
def connection_arguments(self) -> Mapping[str, object]:
|
|
25
|
+
"""Return current driver keyword arguments."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class AccessTokenAuthentication:
|
|
30
|
+
"""Acquire and encode a fresh Entra access token per connection."""
|
|
31
|
+
|
|
32
|
+
token_provider: Callable[[], str]
|
|
33
|
+
|
|
34
|
+
def connection_arguments(self) -> Mapping[str, object]:
|
|
35
|
+
token = self.token_provider()
|
|
36
|
+
token_bytes = token.encode("utf-16-le")
|
|
37
|
+
packed = struct.pack(f"<I{len(token_bytes)}s", len(token_bytes), token_bytes)
|
|
38
|
+
return {"attrs_before": {SQL_ACCESS_TOKEN_ATTRIBUTE: packed}}
|
weaver/sql/connection.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Resolved SQL endpoints and common ``mssql-python`` connection creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from .authentication import SqlAuthentication
|
|
9
|
+
from .errors import SqlConnectionError
|
|
10
|
+
|
|
11
|
+
DEFAULT_SQL_PORT = 1433
|
|
12
|
+
DEFAULT_CONNECT_TIMEOUT = 60
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SqlEndpoint:
|
|
17
|
+
"""One resolved Warehouse SQL endpoint.
|
|
18
|
+
|
|
19
|
+
The optional Fabric identities make the pool key stable without treating a
|
|
20
|
+
display name or a credential-bearing connection string as identity.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
server: str
|
|
24
|
+
database: str
|
|
25
|
+
port: int = DEFAULT_SQL_PORT
|
|
26
|
+
workspace_id: str | None = None
|
|
27
|
+
warehouse_id: str | None = None
|
|
28
|
+
warehouse_name: str | None = None
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
if not self.server.strip():
|
|
32
|
+
raise ValueError("SQL server must not be empty")
|
|
33
|
+
if not self.database.strip():
|
|
34
|
+
raise ValueError("SQL database must not be empty")
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def pool_key(self) -> tuple[str, str, str, str, int]:
|
|
38
|
+
return (
|
|
39
|
+
self.workspace_id or "",
|
|
40
|
+
self.warehouse_id or "",
|
|
41
|
+
self.server.lower(),
|
|
42
|
+
self.database.lower(),
|
|
43
|
+
self.port,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return f"{self.server}:{self.port}/{self.database}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_connection_string(endpoint: SqlEndpoint) -> str:
|
|
51
|
+
"""The validated Fabric Warehouse connection-string shape."""
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
f"Server={endpoint.server},{endpoint.port};"
|
|
55
|
+
f"Database={endpoint.database};"
|
|
56
|
+
"Encrypt=yes;"
|
|
57
|
+
"TrustServerCertificate=no;"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
DriverConnector = Callable[..., Any]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def connect(
|
|
65
|
+
endpoint: SqlEndpoint,
|
|
66
|
+
authentication: SqlAuthentication,
|
|
67
|
+
*,
|
|
68
|
+
timeout: int = DEFAULT_CONNECT_TIMEOUT,
|
|
69
|
+
connector: DriverConnector | None = None,
|
|
70
|
+
):
|
|
71
|
+
"""Open one physical connection using current authentication material."""
|
|
72
|
+
|
|
73
|
+
if connector is None:
|
|
74
|
+
try:
|
|
75
|
+
from mssql_python import connect as connector
|
|
76
|
+
except ImportError as exc: # pragma: no cover - installation dependent
|
|
77
|
+
raise SqlConnectionError(
|
|
78
|
+
"mssql-python is required for Warehouse SQL execution"
|
|
79
|
+
) from exc
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
return connector(
|
|
83
|
+
build_connection_string(endpoint),
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
**dict(authentication.connection_arguments()),
|
|
86
|
+
)
|
|
87
|
+
except SqlConnectionError:
|
|
88
|
+
raise
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
raise SqlConnectionError(f"failed to connect to {endpoint}: {exc}") from exc
|
weaver/sql/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Weaver-facing SQL errors.
|
|
2
|
+
|
|
3
|
+
Driver exceptions do not escape this package. The normalised errors retain the
|
|
4
|
+
selected endpoint and chain the original exception for diagnosis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..errors import WeaverError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SqlError(WeaverError):
|
|
13
|
+
"""Base class for SQL connection and execution failures."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SqlConnectionError(SqlError):
|
|
17
|
+
"""Raised when a physical SQL connection cannot be opened."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SqlExecutionError(SqlError):
|
|
21
|
+
"""Raised when a SQL statement, script, or query fails."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SqlPoolClosedError(SqlError):
|
|
25
|
+
"""Raised when a caller tries to lease from a closed pool."""
|
weaver/sql/execution.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Shared SQL statement, script, query, and transaction handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from .errors import SqlError, SqlExecutionError
|
|
9
|
+
from .pool import SqlConnectionPool
|
|
10
|
+
|
|
11
|
+
SqlRow = dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SqlExecutor(Protocol):
|
|
15
|
+
"""The SQL surface used by Weaver operations."""
|
|
16
|
+
|
|
17
|
+
def execute(
|
|
18
|
+
self, statement: str, parameters: Sequence[object] | None = None
|
|
19
|
+
) -> None: ...
|
|
20
|
+
|
|
21
|
+
def execute_script(self, script: str) -> None: ...
|
|
22
|
+
|
|
23
|
+
def query(
|
|
24
|
+
self, statement: str, parameters: Sequence[object] | None = None
|
|
25
|
+
) -> Sequence[SqlRow]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PooledSqlExecutor:
|
|
29
|
+
"""Execute through one owned or injected bounded connection pool."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, pool: SqlConnectionPool, *, owns_pool: bool = False) -> None:
|
|
32
|
+
self.pool = pool
|
|
33
|
+
self.owns_pool = owns_pool
|
|
34
|
+
|
|
35
|
+
def execute(
|
|
36
|
+
self, statement: str, parameters: Sequence[object] | None = None
|
|
37
|
+
) -> None:
|
|
38
|
+
self._run(statement, parameters=parameters, query=False, drain=True)
|
|
39
|
+
|
|
40
|
+
def execute_script(self, script: str) -> None:
|
|
41
|
+
self._run(script, parameters=None, query=False, drain=True)
|
|
42
|
+
|
|
43
|
+
def query(
|
|
44
|
+
self, statement: str, parameters: Sequence[object] | None = None
|
|
45
|
+
) -> Sequence[SqlRow]:
|
|
46
|
+
return self._run(statement, parameters=parameters, query=True, drain=False)
|
|
47
|
+
|
|
48
|
+
def _run(
|
|
49
|
+
self,
|
|
50
|
+
statement: str,
|
|
51
|
+
*,
|
|
52
|
+
parameters: Sequence[object] | None,
|
|
53
|
+
query: bool,
|
|
54
|
+
drain: bool,
|
|
55
|
+
):
|
|
56
|
+
with self.pool.lease() as lease:
|
|
57
|
+
connection = lease.connection
|
|
58
|
+
cursor = None
|
|
59
|
+
try:
|
|
60
|
+
cursor = connection.cursor()
|
|
61
|
+
if parameters is None:
|
|
62
|
+
cursor.execute(statement)
|
|
63
|
+
else:
|
|
64
|
+
cursor.execute(statement, tuple(parameters))
|
|
65
|
+
|
|
66
|
+
if query:
|
|
67
|
+
if cursor.description is None:
|
|
68
|
+
rows = []
|
|
69
|
+
else:
|
|
70
|
+
columns = [column[0] for column in cursor.description]
|
|
71
|
+
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
72
|
+
connection.commit()
|
|
73
|
+
return rows
|
|
74
|
+
|
|
75
|
+
if drain:
|
|
76
|
+
_drain(cursor)
|
|
77
|
+
connection.commit()
|
|
78
|
+
return None
|
|
79
|
+
except SqlError:
|
|
80
|
+
lease.discard()
|
|
81
|
+
_rollback(connection)
|
|
82
|
+
raise
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
lease.discard()
|
|
85
|
+
_rollback(connection)
|
|
86
|
+
operation = "query" if query else "SQL execution"
|
|
87
|
+
raise SqlExecutionError(
|
|
88
|
+
f"{operation} failed on {self.pool.endpoint}: {exc}"
|
|
89
|
+
) from exc
|
|
90
|
+
finally:
|
|
91
|
+
if cursor is not None:
|
|
92
|
+
try:
|
|
93
|
+
cursor.close()
|
|
94
|
+
except Exception:
|
|
95
|
+
lease.discard()
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
if self.owns_pool:
|
|
99
|
+
self.pool.close()
|
|
100
|
+
|
|
101
|
+
def __enter__(self) -> "PooledSqlExecutor":
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, *exc) -> bool:
|
|
105
|
+
self.close()
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _drain(cursor) -> None:
|
|
110
|
+
"""Consume all result sets so multi-statement T-SQL can commit reliably."""
|
|
111
|
+
|
|
112
|
+
while True:
|
|
113
|
+
if cursor.description is not None:
|
|
114
|
+
cursor.fetchall()
|
|
115
|
+
if not cursor.nextset():
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _rollback(connection) -> None:
|
|
120
|
+
try:
|
|
121
|
+
connection.rollback()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
weaver/sql/pool.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Small, bounded, thread-safe SQL connection pools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Callable, Iterator
|
|
9
|
+
|
|
10
|
+
from .authentication import SqlAuthentication
|
|
11
|
+
from .connection import SqlEndpoint, connect
|
|
12
|
+
from .errors import SqlPoolClosedError
|
|
13
|
+
|
|
14
|
+
DEFAULT_MAX_CONNECTIONS = 4
|
|
15
|
+
ConnectionFactory = Callable[[SqlEndpoint, SqlAuthentication], Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SqlConnectionLease:
|
|
20
|
+
"""One exclusively leased physical connection."""
|
|
21
|
+
|
|
22
|
+
connection: Any
|
|
23
|
+
usable: bool = True
|
|
24
|
+
|
|
25
|
+
def discard(self) -> None:
|
|
26
|
+
"""Prevent this connection from returning to the idle pool."""
|
|
27
|
+
|
|
28
|
+
self.usable = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqlConnectionPool:
|
|
32
|
+
"""Bounded connection reuse owned by one workflow or Fabric session."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
endpoint: SqlEndpoint,
|
|
37
|
+
authentication: SqlAuthentication,
|
|
38
|
+
*,
|
|
39
|
+
max_connections: int = DEFAULT_MAX_CONNECTIONS,
|
|
40
|
+
connection_factory: ConnectionFactory = connect,
|
|
41
|
+
) -> None:
|
|
42
|
+
if max_connections < 1:
|
|
43
|
+
raise ValueError("max_connections must be at least one")
|
|
44
|
+
self.endpoint = endpoint
|
|
45
|
+
self.authentication = authentication
|
|
46
|
+
self.max_connections = max_connections
|
|
47
|
+
self._connection_factory = connection_factory
|
|
48
|
+
self._condition = threading.Condition()
|
|
49
|
+
self._idle: list[Any] = []
|
|
50
|
+
self._physical_count = 0
|
|
51
|
+
self._closed = False
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def lease(self) -> Iterator[SqlConnectionLease]:
|
|
55
|
+
"""Lease one connection exclusively, waiting when the pool is full."""
|
|
56
|
+
|
|
57
|
+
connection = self._acquire()
|
|
58
|
+
lease = SqlConnectionLease(connection)
|
|
59
|
+
try:
|
|
60
|
+
yield lease
|
|
61
|
+
finally:
|
|
62
|
+
self._release(lease)
|
|
63
|
+
|
|
64
|
+
def _acquire(self):
|
|
65
|
+
create = False
|
|
66
|
+
with self._condition:
|
|
67
|
+
while True:
|
|
68
|
+
if self._closed:
|
|
69
|
+
raise SqlPoolClosedError(f"SQL pool for {self.endpoint} is closed")
|
|
70
|
+
if self._idle:
|
|
71
|
+
return self._idle.pop()
|
|
72
|
+
if self._physical_count < self.max_connections:
|
|
73
|
+
# Reserve the slot before opening outside the lock.
|
|
74
|
+
self._physical_count += 1
|
|
75
|
+
create = True
|
|
76
|
+
break
|
|
77
|
+
self._condition.wait()
|
|
78
|
+
|
|
79
|
+
if create:
|
|
80
|
+
try:
|
|
81
|
+
return self._connection_factory(self.endpoint, self.authentication)
|
|
82
|
+
except Exception:
|
|
83
|
+
with self._condition:
|
|
84
|
+
self._physical_count -= 1
|
|
85
|
+
self._condition.notify()
|
|
86
|
+
raise
|
|
87
|
+
raise AssertionError("unreachable")
|
|
88
|
+
|
|
89
|
+
def _release(self, lease: SqlConnectionLease) -> None:
|
|
90
|
+
close = False
|
|
91
|
+
with self._condition:
|
|
92
|
+
if self._closed or not lease.usable:
|
|
93
|
+
self._physical_count -= 1
|
|
94
|
+
close = True
|
|
95
|
+
else:
|
|
96
|
+
self._idle.append(lease.connection)
|
|
97
|
+
self._condition.notify()
|
|
98
|
+
if close:
|
|
99
|
+
_close(lease.connection)
|
|
100
|
+
|
|
101
|
+
def close(self) -> None:
|
|
102
|
+
"""Close idle connections; leased connections close when returned."""
|
|
103
|
+
|
|
104
|
+
with self._condition:
|
|
105
|
+
if self._closed:
|
|
106
|
+
return
|
|
107
|
+
self._closed = True
|
|
108
|
+
idle, self._idle = self._idle, []
|
|
109
|
+
self._physical_count -= len(idle)
|
|
110
|
+
self._condition.notify_all()
|
|
111
|
+
for connection in idle:
|
|
112
|
+
_close(connection)
|
|
113
|
+
|
|
114
|
+
def __enter__(self) -> "SqlConnectionPool":
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def __exit__(self, *exc) -> bool:
|
|
118
|
+
self.close()
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SqlPoolRegistry:
|
|
123
|
+
"""One endpoint-keyed pool registry for an owning execution context."""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
max_connections: int = DEFAULT_MAX_CONNECTIONS,
|
|
129
|
+
connection_factory: ConnectionFactory = connect,
|
|
130
|
+
) -> None:
|
|
131
|
+
self.max_connections = max_connections
|
|
132
|
+
self.connection_factory = connection_factory
|
|
133
|
+
self._lock = threading.Lock()
|
|
134
|
+
self._pools: dict[tuple[str, str, str, str, int], SqlConnectionPool] = {}
|
|
135
|
+
self._closed = False
|
|
136
|
+
|
|
137
|
+
def pool(
|
|
138
|
+
self, endpoint: SqlEndpoint, authentication: SqlAuthentication
|
|
139
|
+
) -> SqlConnectionPool:
|
|
140
|
+
with self._lock:
|
|
141
|
+
if self._closed:
|
|
142
|
+
raise SqlPoolClosedError("SQL pool registry is closed")
|
|
143
|
+
key = endpoint.pool_key
|
|
144
|
+
if key not in self._pools:
|
|
145
|
+
self._pools[key] = SqlConnectionPool(
|
|
146
|
+
endpoint,
|
|
147
|
+
authentication,
|
|
148
|
+
max_connections=self.max_connections,
|
|
149
|
+
connection_factory=self.connection_factory,
|
|
150
|
+
)
|
|
151
|
+
return self._pools[key]
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
with self._lock:
|
|
155
|
+
if self._closed:
|
|
156
|
+
return
|
|
157
|
+
self._closed = True
|
|
158
|
+
pools, self._pools = tuple(self._pools.values()), {}
|
|
159
|
+
for pool in pools:
|
|
160
|
+
pool.close()
|
|
161
|
+
|
|
162
|
+
def __enter__(self) -> "SqlPoolRegistry":
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(self, *exc) -> bool:
|
|
166
|
+
self.close()
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _close(connection: Any) -> None:
|
|
171
|
+
try:
|
|
172
|
+
connection.close()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
weaver/sql/wipe.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Pure generation of the proven Fabric Warehouse wipe batch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_warehouse_wipe_sql() -> str:
|
|
7
|
+
"""Return deterministic SQL that removes supported user-created objects.
|
|
8
|
+
|
|
9
|
+
This is ported from the legacy Weaver Warehouse implementation:
|
|
10
|
+
``src/weaver_runtime/dbrep/sql/templates/admin/wipe.sql`` for object
|
|
11
|
+
enumeration and ordering, plus ``_drop_user_schemas`` in
|
|
12
|
+
``src/weaver_runtime/dbrep/sql/backend.py`` for schema cleanup.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
return _WAREHOUSE_WIPE_SQL
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_WAREHOUSE_WIPE_SQL = """\
|
|
19
|
+
set nocount on;
|
|
20
|
+
|
|
21
|
+
declare @weaver_sql nvarchar(max);
|
|
22
|
+
|
|
23
|
+
/* Foreign keys first so dependent tables can be dropped. */
|
|
24
|
+
select
|
|
25
|
+
@weaver_sql = string_agg(
|
|
26
|
+
convert(
|
|
27
|
+
nvarchar(max),
|
|
28
|
+
N'alter table '
|
|
29
|
+
+ quotename(object_schema_name(fk.parent_object_id))
|
|
30
|
+
+ N'.'
|
|
31
|
+
+ quotename(object_name(fk.parent_object_id))
|
|
32
|
+
+ N' drop constraint '
|
|
33
|
+
+ quotename(fk.name)
|
|
34
|
+
+ N';'
|
|
35
|
+
),
|
|
36
|
+
char(10)
|
|
37
|
+
) within group (order by fk.object_id)
|
|
38
|
+
from sys.foreign_keys as fk
|
|
39
|
+
where lower(schema_name(schema_id)) not in
|
|
40
|
+
(N'guest', N'information_schema', N'sys', N'queryinsights');
|
|
41
|
+
|
|
42
|
+
if @weaver_sql is not null
|
|
43
|
+
begin
|
|
44
|
+
exec sys.sp_executesql @weaver_sql;
|
|
45
|
+
end;
|
|
46
|
+
|
|
47
|
+
/* Views before the objects and tables they can depend on. */
|
|
48
|
+
set @weaver_sql = null;
|
|
49
|
+
select
|
|
50
|
+
@weaver_sql = string_agg(
|
|
51
|
+
convert(
|
|
52
|
+
nvarchar(max),
|
|
53
|
+
N'drop view '
|
|
54
|
+
+ quotename(schema_name(schema_id))
|
|
55
|
+
+ N'.'
|
|
56
|
+
+ quotename(name)
|
|
57
|
+
+ N';'
|
|
58
|
+
),
|
|
59
|
+
char(10)
|
|
60
|
+
) within group (order by object_id)
|
|
61
|
+
from sys.views
|
|
62
|
+
where lower(schema_name(schema_id)) not in
|
|
63
|
+
(N'guest', N'information_schema', N'sys', N'queryinsights');
|
|
64
|
+
|
|
65
|
+
if @weaver_sql is not null
|
|
66
|
+
begin
|
|
67
|
+
exec sys.sp_executesql @weaver_sql;
|
|
68
|
+
end;
|
|
69
|
+
|
|
70
|
+
/* Stored procedures. */
|
|
71
|
+
set @weaver_sql = null;
|
|
72
|
+
select
|
|
73
|
+
@weaver_sql = string_agg(
|
|
74
|
+
convert(
|
|
75
|
+
nvarchar(max),
|
|
76
|
+
N'drop procedure '
|
|
77
|
+
+ quotename(schema_name(schema_id))
|
|
78
|
+
+ N'.'
|
|
79
|
+
+ quotename(name)
|
|
80
|
+
+ N';'
|
|
81
|
+
),
|
|
82
|
+
char(10)
|
|
83
|
+
) within group (order by object_id)
|
|
84
|
+
from sys.procedures
|
|
85
|
+
where lower(schema_name(schema_id)) not in
|
|
86
|
+
(N'guest', N'information_schema', N'sys', N'queryinsights');
|
|
87
|
+
|
|
88
|
+
if @weaver_sql is not null
|
|
89
|
+
begin
|
|
90
|
+
exec sys.sp_executesql @weaver_sql;
|
|
91
|
+
end;
|
|
92
|
+
|
|
93
|
+
/* Functions (scalar, inline table-valued, multi-statement table-valued). */
|
|
94
|
+
set @weaver_sql = null;
|
|
95
|
+
select
|
|
96
|
+
@weaver_sql = string_agg(
|
|
97
|
+
convert(
|
|
98
|
+
nvarchar(max),
|
|
99
|
+
N'drop function '
|
|
100
|
+
+ quotename(schema_name(schema_id))
|
|
101
|
+
+ N'.'
|
|
102
|
+
+ quotename(name)
|
|
103
|
+
+ N';'
|
|
104
|
+
),
|
|
105
|
+
char(10)
|
|
106
|
+
) within group (order by object_id)
|
|
107
|
+
from sys.objects
|
|
108
|
+
where type in (N'FN', N'IF', N'TF', N'FS', N'FT')
|
|
109
|
+
and lower(schema_name(schema_id)) not in
|
|
110
|
+
(N'guest', N'information_schema', N'sys', N'queryinsights');
|
|
111
|
+
|
|
112
|
+
if @weaver_sql is not null
|
|
113
|
+
begin
|
|
114
|
+
exec sys.sp_executesql @weaver_sql;
|
|
115
|
+
end;
|
|
116
|
+
|
|
117
|
+
/* Tables. */
|
|
118
|
+
set @weaver_sql = null;
|
|
119
|
+
select
|
|
120
|
+
@weaver_sql = string_agg(
|
|
121
|
+
convert(
|
|
122
|
+
nvarchar(max),
|
|
123
|
+
N'drop table '
|
|
124
|
+
+ quotename(schema_name(schema_id))
|
|
125
|
+
+ N'.'
|
|
126
|
+
+ quotename(name)
|
|
127
|
+
+ N';'
|
|
128
|
+
),
|
|
129
|
+
char(10)
|
|
130
|
+
) within group (order by object_id)
|
|
131
|
+
from sys.tables
|
|
132
|
+
where lower(schema_name(schema_id)) not in
|
|
133
|
+
(N'guest', N'information_schema', N'sys', N'queryinsights');
|
|
134
|
+
|
|
135
|
+
if @weaver_sql is not null
|
|
136
|
+
begin
|
|
137
|
+
exec sys.sp_executesql @weaver_sql;
|
|
138
|
+
end;
|
|
139
|
+
|
|
140
|
+
/* User schemas last; the built-in and Fabric-owned schemas survive. */
|
|
141
|
+
set @weaver_sql = null;
|
|
142
|
+
select
|
|
143
|
+
@weaver_sql = string_agg(
|
|
144
|
+
convert(nvarchar(max), N'drop schema ' + quotename(name) + N';'),
|
|
145
|
+
char(10)
|
|
146
|
+
) within group (order by schema_id)
|
|
147
|
+
from sys.schemas
|
|
148
|
+
where lower(name) not in
|
|
149
|
+
(N'dbo', N'guest', N'information_schema', N'sys', N'queryinsights', N'_rsc')
|
|
150
|
+
and schema_id < 16384;
|
|
151
|
+
|
|
152
|
+
if @weaver_sql is not null
|
|
153
|
+
begin
|
|
154
|
+
exec sys.sp_executesql @weaver_sql;
|
|
155
|
+
end;
|
|
156
|
+
"""
|