Skip to content

Commit bde93d5

Browse files
Add schema to mssql (#11856)
1 parent 127d9f2 commit bde93d5

5 files changed

Lines changed: 110 additions & 46 deletions

File tree

mindsdb/integrations/handlers/mssql_handler/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Optional connection parameters include the following:
8585

8686
* `port`: The port number for connecting to the Microsoft SQL Server. Default is 1433.
8787
* `server`: The server name to connect to. Typically only used with named instances or Azure SQL Database.
88+
* `schema`: The schema in which objects are searched first. If specified, all table references without an explicit schema will be automatically qualified with this schema.
8889

8990
### ODBC Connection
9091

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
__title__ = 'MindsDB Microsoft SQL Server handler'
2-
__package_name__ = 'mindsdb_mssql_handler'
3-
__version__ = '0.0.1'
1+
__title__ = "MindsDB Microsoft SQL Server handler"
2+
__package_name__ = "mindsdb_mssql_handler"
3+
__version__ = "0.0.1"
44
__description__ = "MindsDB handler for Microsoft SQL Server"
5-
__author__ = 'MindsDB Inc'
6-
__github__ = 'https://github.com/mindsdb/mindsdb'
7-
__pypi__ = 'https://pypi.org/project/mindsdb/'
8-
__license__ = 'MIT'
9-
__copyright__ = 'Copyright 2022- mindsdb'
5+
__author__ = "MindsDB Inc"
6+
__github__ = "https://github.com/mindsdb/mindsdb"
7+
__pypi__ = "https://pypi.org/project/mindsdb/"
8+
__license__ = "MIT"
9+
__copyright__ = "Copyright 2022- mindsdb"

mindsdb/integrations/handlers/mssql_handler/__init__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,30 @@
22

33
from .__about__ import __version__ as version, __description__ as description
44
from .connection_args import connection_args, connection_args_example
5+
56
try:
67
from .mssql_handler import SqlServerHandler as Handler
8+
79
import_error = None
810
except Exception as e:
911
Handler = None
1012
import_error = e
1113

1214

13-
title = 'Microsoft SQL Server'
14-
name = 'mssql'
15+
title = "Microsoft SQL Server"
16+
name = "mssql"
1517
type = HANDLER_TYPE.DATA
16-
icon_path = 'icon.svg'
18+
icon_path = "icon.svg"
1719

1820
__all__ = [
19-
'Handler', 'version', 'name', 'type', 'title', 'description',
20-
'connection_args_example', 'connection_args', 'import_error', 'icon_path'
21+
"Handler",
22+
"version",
23+
"name",
24+
"type",
25+
"title",
26+
"description",
27+
"connection_args_example",
28+
"connection_args",
29+
"import_error",
30+
"icon_path",
2131
]

mindsdb/integrations/handlers/mssql_handler/connection_args.py

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,50 @@
55

66
connection_args = OrderedDict(
77
user={
8-
'type': ARG_TYPE.STR,
9-
'description': 'The user name used to authenticate with the Microsoft SQL Server.',
10-
'required': True,
11-
'label': 'User'
8+
"type": ARG_TYPE.STR,
9+
"description": "The user name used to authenticate with the Microsoft SQL Server.",
10+
"required": True,
11+
"label": "User",
1212
},
1313
password={
14-
'type': ARG_TYPE.PWD,
15-
'description': 'The password to authenticate the user with the Microsoft SQL Server.',
16-
'required': True,
17-
'label': 'Password',
18-
'secret': True
14+
"type": ARG_TYPE.PWD,
15+
"description": "The password to authenticate the user with the Microsoft SQL Server.",
16+
"required": True,
17+
"label": "Password",
18+
"secret": True,
1919
},
2020
database={
21-
'type': ARG_TYPE.STR,
22-
'description': 'The database name to use when connecting with the Microsoft SQL Server.',
23-
'required': True,
24-
'label': 'Database'
21+
"type": ARG_TYPE.STR,
22+
"description": "The database name to use when connecting with the Microsoft SQL Server.",
23+
"required": True,
24+
"label": "Database",
2525
},
2626
host={
27-
'type': ARG_TYPE.STR,
28-
'description': 'The host name or IP address of the Microsoft SQL Server.',
29-
'required': True,
30-
'label': 'Host'
27+
"type": ARG_TYPE.STR,
28+
"description": "The host name or IP address of the Microsoft SQL Server.",
29+
"required": True,
30+
"label": "Host",
3131
},
3232
port={
33-
'type': ARG_TYPE.INT,
34-
'description': 'The TCP/IP port of the Microsoft SQL Server. Must be an integer.',
35-
'required': False,
36-
'label': 'Port'
33+
"type": ARG_TYPE.INT,
34+
"description": "The TCP/IP port of the Microsoft SQL Server. Must be an integer.",
35+
"required": False,
36+
"label": "Port",
3737
},
3838
server={
39-
'type': ARG_TYPE.STR,
40-
'description': 'The server name of the Microsoft SQL Server. Typically only used with named instances or Azure SQL Database.',
41-
'required': False,
42-
'label': 'Server'
43-
}
39+
"type": ARG_TYPE.STR,
40+
"description": "The server name of the Microsoft SQL Server. Typically only used with named instances or Azure SQL Database.",
41+
"required": False,
42+
"label": "Server",
43+
},
44+
schema={
45+
"type": ARG_TYPE.STR,
46+
"description": "The schema in which objects are searched first. If not provided, all schemas will be queried.",
47+
"required": False,
48+
"label": "Schema",
49+
},
4450
)
4551

4652
connection_args_example = OrderedDict(
47-
host='127.0.0.1',
48-
port=1433,
49-
user='sa',
50-
password='password',
51-
database='master'
53+
host="127.0.0.1", port=1433, user="sa", password="password", database="master", schema="dbo"
5254
)

mindsdb/integrations/handlers/mssql_handler/mssql_handler.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
from mindsdb_sql_parser import parse_sql
1010
from mindsdb_sql_parser.ast.base import ASTNode
11+
from mindsdb_sql_parser.ast import Identifier
1112

1213
from mindsdb.integrations.libs.base import MetaDatabaseHandler
14+
from mindsdb.integrations.utilities.query_traversal import query_traversal
1315
from mindsdb.utilities import log
1416
from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender
1517
from mindsdb.integrations.libs.response import (
@@ -169,6 +171,7 @@ def __init__(self, name, **kwargs):
169171
self.connection_args = kwargs.get("connection_data")
170172
self.dialect = "mssql"
171173
self.database = self.connection_args.get("database")
174+
self.schema = self.connection_args.get("schema")
172175
self.renderer = SqlalchemyRender("mssql")
173176

174177
# Determine if ODBC should be used
@@ -381,6 +384,26 @@ def native_query(self, query: str) -> Response:
381384

382385
return response
383386

387+
def _add_schema_to_tables(self, node, is_table=False, **kwargs):
388+
"""
389+
Callback for query_traversal that adds schema prefix to table identifiers.
390+
391+
Args:
392+
node: The AST node being visited
393+
is_table: True if this node represents a table reference
394+
**kwargs: Other arguments from query_traversal (parent_query, callstack, etc.)
395+
396+
Returns:
397+
None to keep traversing, or a replacement node
398+
Note: This is mostly a workaround for Minds but it should still work for FQE
399+
"""
400+
if is_table and isinstance(node, Identifier):
401+
# Only add schema if the identifier doesn't already have one (single part)
402+
if len(node.parts) == 1:
403+
node.parts.insert(0, self.schema)
404+
node.is_quoted.insert(0, False)
405+
return None
406+
384407
def query(self, query: ASTNode) -> Response:
385408
"""
386409
Executes a SQL query represented by an ASTNode and retrieves the data.
@@ -391,6 +414,9 @@ def query(self, query: ASTNode) -> Response:
391414
Returns:
392415
Response: The response from the `native_query` method, containing the result of the SQL query execution.
393416
"""
417+
# Add schema prefix to table identifiers if schema is configured
418+
if self.schema:
419+
query_traversal(query, self._add_schema_to_tables)
394420

395421
query_str = self.renderer.get_string(query, with_failback=True)
396422
logger.debug(f"Executing SQL query: {query_str}")
@@ -410,8 +436,11 @@ def get_tables(self) -> Response:
410436
table_name,
411437
table_type
412438
FROM {self.database}.INFORMATION_SCHEMA.TABLES
413-
WHERE TABLE_TYPE in ('BASE TABLE', 'VIEW');
439+
WHERE TABLE_TYPE in ('BASE TABLE', 'VIEW')
414440
"""
441+
if self.schema:
442+
query += f" AND table_schema = '{self.schema}'"
443+
415444
return self.native_query(query)
416445

417446
def get_columns(self, table_name) -> Response:
@@ -446,6 +475,10 @@ def get_columns(self, table_name) -> Response:
446475
WHERE
447476
table_name = '{table_name}'
448477
"""
478+
479+
if self.schema:
480+
query += f" AND table_schema = '{self.schema}'"
481+
449482
result = self.native_query(query)
450483
result.to_columns_table_response(map_type_fn=_map_type)
451484
return result
@@ -483,9 +516,13 @@ def meta_get_tables(self, table_names: list[str] | None = None) -> Response:
483516
AND p.index_id IN (0, 1)
484517
WHERE t.TABLE_TYPE IN ('BASE TABLE', 'VIEW')
485518
AND t.TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA')
486-
GROUP BY t.TABLE_NAME, t.TABLE_SCHEMA, t.TABLE_TYPE, ep.value
487519
"""
488520

521+
if self.schema:
522+
query += f" AND t.TABLE_SCHEMA = '{self.schema}'"
523+
524+
query += " GROUP BY t.TABLE_NAME, t.TABLE_SCHEMA, t.TABLE_TYPE, ep.value"
525+
489526
if table_names is not None and len(table_names) > 0:
490527
quoted_names = [f"'{t}'" for t in table_names]
491528
query += f" HAVING t.TABLE_NAME IN ({','.join(quoted_names)})"
@@ -525,6 +562,9 @@ def meta_get_columns(self, table_names: list[str] | None = None) -> Response:
525562
WHERE c.TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA')
526563
"""
527564

565+
if self.schema:
566+
query += f" AND c.TABLE_SCHEMA = '{self.schema}'"
567+
528568
if table_names is not None and len(table_names) > 0:
529569
quoted_names = [f"'{t}'" for t in table_names]
530570
query += f" AND c.TABLE_NAME IN ({','.join(quoted_names)})"
@@ -553,6 +593,10 @@ def meta_get_column_statistics(self, table_names: list[str] | None = None) -> Re
553593
quoted_names = [f"'{t}'" for t in table_names]
554594
table_filter = f" AND t.name IN ({','.join(quoted_names)})"
555595

596+
schema_filter = ""
597+
if self.schema:
598+
schema_filter = f" AND s.name = '{self.schema}'"
599+
556600
# Using OUTER APPLY to handle table-valued functions properly
557601
# This is equivalent to PostgreSQL's pg_stats view approach
558602
# Includes all statistics: auto-created, user-created, and index-based
@@ -589,6 +633,7 @@ def meta_get_column_statistics(self, table_names: list[str] | None = None) -> Re
589633
WHERE st.object_id IS NOT NULL
590634
) h
591635
WHERE s.name NOT IN ('sys', 'INFORMATION_SCHEMA')
636+
{schema_filter}
592637
{table_filter}
593638
ORDER BY t.name, c.name
594639
"""
@@ -620,6 +665,9 @@ def meta_get_primary_keys(self, table_names: list[str] | None = None) -> Respons
620665
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
621666
"""
622667

668+
if self.schema:
669+
query += f" AND tc.TABLE_SCHEMA = '{self.schema}'"
670+
623671
if table_names is not None and len(table_names) > 0:
624672
quoted_names = [f"'{t}'" for t in table_names]
625673
query += f" AND tc.TABLE_NAME IN ({','.join(quoted_names)})"
@@ -656,6 +704,9 @@ def meta_get_foreign_keys(self, table_names: list[str] | None = None) -> Respons
656704
WHERE s.name NOT IN ('sys', 'INFORMATION_SCHEMA')
657705
"""
658706

707+
if self.schema:
708+
query += f" AND s.name = '{self.schema}'"
709+
659710
if table_names is not None and len(table_names) > 0:
660711
quoted_names = [f"'{t}'" for t in table_names]
661712
query += f" AND OBJECT_NAME(fk.parent_object_id) IN ({','.join(quoted_names)})"

0 commit comments

Comments
 (0)