qodalis-cli-server-plugin-data-explorer-mssql 1.0.0b18__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.
- qodalis_cli_data_explorer_mssql/__init__.py +5 -0
- qodalis_cli_data_explorer_mssql/mssql_provider.py +209 -0
- qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/METADATA +21 -0
- qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/RECORD +5 -0
- qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""MS SQL Server-backed Data Explorer provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import pymssql
|
|
10
|
+
|
|
11
|
+
from qodalis_cli_server_abstractions import (
|
|
12
|
+
DataExplorerExecutionContext,
|
|
13
|
+
DataExplorerProviderOptions,
|
|
14
|
+
DataExplorerResult,
|
|
15
|
+
DataExplorerSchemaColumn,
|
|
16
|
+
DataExplorerSchemaResult,
|
|
17
|
+
DataExplorerSchemaTable,
|
|
18
|
+
IDataExplorerProvider,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_CS_RE = re.compile(r"(?P<key>[^=;]+)=(?P<value>[^;]*)", re.IGNORECASE)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_connection_string(connection_string: str) -> dict[str, str]:
|
|
25
|
+
"""Return a lower-cased key dict from an ADO.NET connection string."""
|
|
26
|
+
result: dict[str, str] = {}
|
|
27
|
+
for match in _CS_RE.finditer(connection_string):
|
|
28
|
+
key = match.group("key").strip().lower().replace(" ", "")
|
|
29
|
+
value = match.group("value").strip()
|
|
30
|
+
result[key] = value
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MssqlDataExplorerProvider(IDataExplorerProvider):
|
|
35
|
+
"""Executes SQL queries against a Microsoft SQL Server database.
|
|
36
|
+
|
|
37
|
+
Connection string format (ADO.NET style)::
|
|
38
|
+
|
|
39
|
+
Server=host,1433;Database=mydb;User Id=sa;Password=secret;TrustServerCertificate=true
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, connection_string: str) -> None:
|
|
43
|
+
self._connection_string = connection_string
|
|
44
|
+
params = _parse_connection_string(connection_string)
|
|
45
|
+
|
|
46
|
+
server_raw = params.get("server", "localhost")
|
|
47
|
+
if "," in server_raw:
|
|
48
|
+
host_part, port_part = server_raw.split(",", 1)
|
|
49
|
+
self._host = host_part.strip()
|
|
50
|
+
try:
|
|
51
|
+
self._port = int(port_part.strip())
|
|
52
|
+
except ValueError:
|
|
53
|
+
self._port = 1433
|
|
54
|
+
else:
|
|
55
|
+
self._host = server_raw
|
|
56
|
+
self._port = 1433
|
|
57
|
+
|
|
58
|
+
self._database = params.get("database", "master")
|
|
59
|
+
self._user = params.get("userid") or params.get("uid", "sa")
|
|
60
|
+
self._password = params.get("password") or params.get("pwd", "")
|
|
61
|
+
|
|
62
|
+
def _connect(self) -> pymssql.Connection:
|
|
63
|
+
"""Open a connection to the SQL Server instance."""
|
|
64
|
+
return pymssql.connect(
|
|
65
|
+
server=self._host,
|
|
66
|
+
port=self._port,
|
|
67
|
+
user=self._user,
|
|
68
|
+
password=self._password,
|
|
69
|
+
database=self._database,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _execute_sync(
|
|
73
|
+
self, context: DataExplorerExecutionContext
|
|
74
|
+
) -> DataExplorerResult:
|
|
75
|
+
"""Synchronous query execution, called via ``asyncio.to_thread``."""
|
|
76
|
+
start = time.monotonic()
|
|
77
|
+
conn: pymssql.Connection | None = None
|
|
78
|
+
try:
|
|
79
|
+
conn = self._connect()
|
|
80
|
+
cursor = conn.cursor()
|
|
81
|
+
cursor.execute(context.query, context.parameters or ())
|
|
82
|
+
|
|
83
|
+
if cursor.description:
|
|
84
|
+
columns = [desc[0] for desc in cursor.description]
|
|
85
|
+
rows_raw = cursor.fetchall()
|
|
86
|
+
rows = [list(row) for row in rows_raw]
|
|
87
|
+
return DataExplorerResult(
|
|
88
|
+
success=True,
|
|
89
|
+
source=context.options.name,
|
|
90
|
+
language=context.options.language,
|
|
91
|
+
default_output_format=context.options.default_output_format,
|
|
92
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
93
|
+
columns=columns,
|
|
94
|
+
rows=rows,
|
|
95
|
+
row_count=len(rows),
|
|
96
|
+
truncated=False,
|
|
97
|
+
error=None,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
conn.commit()
|
|
101
|
+
return DataExplorerResult(
|
|
102
|
+
success=True,
|
|
103
|
+
source=context.options.name,
|
|
104
|
+
language=context.options.language,
|
|
105
|
+
default_output_format=context.options.default_output_format,
|
|
106
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
107
|
+
columns=["changes"],
|
|
108
|
+
rows=[[cursor.rowcount]],
|
|
109
|
+
row_count=1,
|
|
110
|
+
truncated=False,
|
|
111
|
+
error=None,
|
|
112
|
+
)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
return DataExplorerResult(
|
|
115
|
+
success=False,
|
|
116
|
+
source=context.options.name,
|
|
117
|
+
language=context.options.language,
|
|
118
|
+
default_output_format=context.options.default_output_format,
|
|
119
|
+
execution_time=int((time.monotonic() - start) * 1000),
|
|
120
|
+
columns=None,
|
|
121
|
+
rows=[],
|
|
122
|
+
row_count=0,
|
|
123
|
+
truncated=False,
|
|
124
|
+
error=str(exc),
|
|
125
|
+
)
|
|
126
|
+
finally:
|
|
127
|
+
if conn is not None:
|
|
128
|
+
conn.close()
|
|
129
|
+
|
|
130
|
+
def _get_schema_sync(
|
|
131
|
+
self, options: DataExplorerProviderOptions
|
|
132
|
+
) -> DataExplorerSchemaResult | None:
|
|
133
|
+
"""Synchronous schema introspection, called via ``asyncio.to_thread``."""
|
|
134
|
+
conn: pymssql.Connection | None = None
|
|
135
|
+
try:
|
|
136
|
+
conn = self._connect()
|
|
137
|
+
cursor = conn.cursor()
|
|
138
|
+
|
|
139
|
+
cursor.execute(
|
|
140
|
+
"SELECT TABLE_NAME, TABLE_TYPE "
|
|
141
|
+
"FROM INFORMATION_SCHEMA.TABLES "
|
|
142
|
+
"WHERE TABLE_SCHEMA = 'dbo' "
|
|
143
|
+
"ORDER BY TABLE_NAME"
|
|
144
|
+
)
|
|
145
|
+
table_rows = cursor.fetchall()
|
|
146
|
+
|
|
147
|
+
tables: list[DataExplorerSchemaTable] = []
|
|
148
|
+
for table_name, table_type in table_rows:
|
|
149
|
+
cursor.execute(
|
|
150
|
+
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE "
|
|
151
|
+
"FROM INFORMATION_SCHEMA.COLUMNS "
|
|
152
|
+
"WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = %s "
|
|
153
|
+
"ORDER BY ORDINAL_POSITION",
|
|
154
|
+
(table_name,),
|
|
155
|
+
)
|
|
156
|
+
col_rows = cursor.fetchall()
|
|
157
|
+
|
|
158
|
+
cursor.execute(
|
|
159
|
+
"SELECT kcu.COLUMN_NAME "
|
|
160
|
+
"FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc "
|
|
161
|
+
"JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu "
|
|
162
|
+
" ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME "
|
|
163
|
+
" AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA "
|
|
164
|
+
" AND tc.TABLE_NAME = kcu.TABLE_NAME "
|
|
165
|
+
"WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' "
|
|
166
|
+
" AND tc.TABLE_SCHEMA = 'dbo' "
|
|
167
|
+
" AND tc.TABLE_NAME = %s",
|
|
168
|
+
(table_name,),
|
|
169
|
+
)
|
|
170
|
+
pk_rows = cursor.fetchall()
|
|
171
|
+
pk_columns = {row[0] for row in pk_rows}
|
|
172
|
+
|
|
173
|
+
columns = [
|
|
174
|
+
DataExplorerSchemaColumn(
|
|
175
|
+
name=col_name,
|
|
176
|
+
type=data_type or "varchar",
|
|
177
|
+
nullable=is_nullable.upper() == "YES",
|
|
178
|
+
primary_key=col_name in pk_columns,
|
|
179
|
+
)
|
|
180
|
+
for col_name, data_type, is_nullable in col_rows
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
normalised_type = (
|
|
184
|
+
"view" if table_type.strip().upper() == "VIEW" else "table"
|
|
185
|
+
)
|
|
186
|
+
tables.append(
|
|
187
|
+
DataExplorerSchemaTable(
|
|
188
|
+
name=table_name,
|
|
189
|
+
type=normalised_type,
|
|
190
|
+
columns=columns,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return DataExplorerSchemaResult(source=options.name, tables=tables)
|
|
195
|
+
finally:
|
|
196
|
+
if conn is not None:
|
|
197
|
+
conn.close()
|
|
198
|
+
|
|
199
|
+
async def execute_async(
|
|
200
|
+
self, context: DataExplorerExecutionContext
|
|
201
|
+
) -> DataExplorerResult:
|
|
202
|
+
"""Execute a SQL query against the SQL Server database."""
|
|
203
|
+
return await asyncio.to_thread(self._execute_sync, context)
|
|
204
|
+
|
|
205
|
+
async def get_schema_async(
|
|
206
|
+
self, options: DataExplorerProviderOptions
|
|
207
|
+
) -> DataExplorerSchemaResult | None:
|
|
208
|
+
"""Return the dbo schema (tables, views, and their columns)."""
|
|
209
|
+
return await asyncio.to_thread(self._get_schema_sync, options)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: qodalis-cli-server-plugin-data-explorer-mssql
|
|
3
|
+
Version: 1.0.0b18
|
|
4
|
+
Summary: MS SQL Server Data Explorer provider plugin for Qodalis CLI servers
|
|
5
|
+
Project-URL: Homepage, https://github.com/qodalis-solutions/cli-server-python
|
|
6
|
+
Project-URL: Repository, https://github.com/qodalis-solutions/cli-server-python
|
|
7
|
+
Project-URL: Issues, https://github.com/qodalis-solutions/cli-server-python/issues
|
|
8
|
+
Author-email: Nicolae Lupei <nicolae.lupei@qodalis.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: cli,data-explorer,mssql,plugin,qodalis,sql-server
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: pymssql>=2.3.0
|
|
21
|
+
Requires-Dist: qodalis-cli-server-abstractions
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
qodalis_cli_data_explorer_mssql/__init__.py,sha256=mDbSMloN-aeqfQy0j_u-dC-XeluO2FrkdVuPN_KVLDc,169
|
|
2
|
+
qodalis_cli_data_explorer_mssql/mssql_provider.py,sha256=uEFZFy3ydH0xAtA5HSXM7SPzzSJsAyzrbw5T_yM4cAE,7822
|
|
3
|
+
qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/METADATA,sha256=BqmGgNjFQQ98-xzaIcxLtTYzEeRXpiPY-VdxRgoCK6k,1041
|
|
4
|
+
qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
5
|
+
qodalis_cli_server_plugin_data_explorer_mssql-1.0.0b18.dist-info/RECORD,,
|