4444logger = logging .getLogger (__name__ )
4545
4646
47+ def _is_pyodbc_sqlserver (conn : Any ) -> bool :
48+ """Return True if `conn` is a pyodbc connection to SQL Server."""
49+ if not safe_isinstance (conn , "pyodbc" , "Connection" ):
50+ return False
51+
52+ try :
53+ import pyodbc
54+ except ImportError :
55+ return False
56+
57+ try :
58+ dbms_name = str (conn .getinfo (pyodbc .SQL_DBMS_NAME ))
59+ except Exception :
60+ return False
61+
62+ upper_name = dbms_name .upper ()
63+ return "SQL SERVER" in upper_name or "AZURE SQL" in upper_name
64+
65+
4766class ConnectionWarning (UserWarning ):
4867 """
4968 Warning raised when there are issues in the Connections Pane relevant to the user.
@@ -322,6 +341,8 @@ def _wrap_connection(self, obj: Any) -> Connection:
322341 return GoogleBigQueryConnection (obj )
323342 elif safe_isinstance (obj , "snowflake.connector" , "SnowflakeConnection" ):
324343 return SnowflakeConnection (obj )
344+ elif _is_pyodbc_sqlserver (obj ) or safe_isinstance (obj , "pymssql" , "Connection" ):
345+ return SQLServerConnection (obj )
325346 elif safe_isinstance (obj , "databricks.sql.client" , "Connection" ):
326347 return DatabricksConnection (obj )
327348 else :
@@ -343,6 +364,8 @@ def object_is_supported(self, obj: Any) -> bool:
343364 )
344365 or safe_isinstance (obj , "snowflake.connector" , "SnowflakeConnection" )
345366 or safe_isinstance (obj , "databricks.sql.client" , "Connection" )
367+ or _is_pyodbc_sqlserver (obj )
368+ or safe_isinstance (obj , "pymssql" , "Connection" )
346369 )
347370 except Exception as err :
348371 logger .error (f"Error checking supported { err } " )
@@ -1159,6 +1182,233 @@ def _make_code(self):
11591182 return code
11601183
11611184
1185+ class SQLServerConnection (Connection ):
1186+ """Support for SQL Server connections to databases."""
1187+
1188+ def __init__ (self , conn : Any ):
1189+ self .conn = conn
1190+
1191+ try :
1192+ self .host = self ._fetch_one_value ("SELECT @@SERVERNAME" )
1193+ except Exception :
1194+ self .host = "<unknown>"
1195+
1196+ self .database : str | None = None
1197+ with contextlib .suppress (Exception ):
1198+ db_name = self ._fetch_one_value ("SELECT DB_NAME()" )
1199+ self .database = str (db_name ) if db_name not in (None , "" ) else None
1200+
1201+ self .type = "SQLServer" + (" (pyodbc)" if self ._is_pyodbc () else " (pymssql)" )
1202+ self .display_name = f"{ self .type } - { self .host } "
1203+
1204+ self .code = self ._make_code ()
1205+ self .icon = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzg2NjZfNjYyNykiPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDFfODY2Nl82NjI3KSI+CjxtYXNrIGlkPSJtYXNrMF84NjY2XzY2MjciIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSIwIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiPgo8cGF0aCBkPSJNNDggMEgwVjQ4SDQ4VjBaIiBmaWxsPSJ3aGl0ZSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfODY2Nl82NjI3KSI+CjxwYXRoIGQ9Ik0yMy45OTgyIDMzLjZDMzQuNjAxOSAzMy42IDQzLjE5ODIgMzYuODIzNCA0My4xOTgyIDQwLjhWMjYuMzk5OEM0My4xOTgyIDIyLjQyMzQgMzQuNjAxOSAxOS4xOTk4IDIzLjk5ODIgMTkuMTk5OEgxOS4xOTgyVjMzLjZIMjMuOTk4MloiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl84NjY2XzY2MjcpIi8+CjxwYXRoIGQ9Ik00My4xOTg4IDI2LjM5OThDNDMuMTk4OCAzMC4zNzYzIDM0LjYwMjggMzMuNTk5NyAyMy45OTg4IDMzLjU5OTdDMTcuNTMxOCAzMy41OTk3IDExLjgxMTYgMzIuNDAwOCA4LjMzMjY3IDMwLjU2MzJDNi40NDg4MyAyOS43MjggNC43OTg4MyAzMC45OTM2IDQuNzk4ODMgMzIuNzA5MVY0MC43OTk3QzQuNzk4ODMgNDQuNzc2MyAxMy4zOTUgNDcuOTk5NyAyMy45OTg4IDQ3Ljk5OTdDMzQuNjAyOCA0Ny45OTk3IDQzLjE5ODggNDQuNzc2MyA0My4xOTg4IDQwLjc5OTdWMjYuMzk5OFoiIGZpbGw9InVybCgjcGFpbnQxX3JhZGlhbF84NjY2XzY2MjcpIi8+CjxwYXRoIGQ9Ik00My4xOTg4IDI2LjM5OThDNDMuMTk4OCAzMC4zNzYzIDM0LjYwMjggMzMuNTk5NyAyMy45OTg4IDMzLjU5OTdDMTcuNTMxOCAzMy41OTk3IDExLjgxMTYgMzIuNDAwOCA4LjMzMjY3IDMwLjU2MzJDNi40NDg4MyAyOS43MjggNC43OTg4MyAzMC45OTM2IDQuNzk4ODMgMzIuNzA5MVY0MC43OTk3QzQuNzk4ODMgNDQuNzc2MyAxMy4zOTUgNDcuOTk5NyAyMy45OTg4IDQ3Ljk5OTdDMzQuNjAyOCA0Ny45OTk3IDQzLjE5ODggNDQuNzc2MyA0My4xOTg4IDQwLjc5OTdWMjYuMzk5OFoiIGZpbGw9InVybCgjcGFpbnQyX3JhZGlhbF84NjY2XzY2MjcpIi8+CjxwYXRoIGQ9Ik0yMy45OTg4IDBDMzQuNjAyNiAwIDQzLjE5ODkgMy4yMjM1NSA0My4xOTg5IDcuMlYxNS4zNTYyQzQzLjE5ODkgMTYuNDcxOSA0Mi4wMzYyIDE4LjQzMTMgMzkuNjYyMSAxNy40MzQ5QzM2LjE4MjYgMTUuNTk4MyAzMC40NjM5IDE0LjQgMjMuOTk4OCAxNC40QzEzLjM5NSAxNC40IDQuNzk4ODMgMTcuNjIzNSA0Ljc5ODgzIDIxLjZWNy4yQzQuNzk4ODMgMy4yMjM1NSAxMy4zOTUgMCAyMy45OTg4IDBaIiBmaWxsPSJ1cmwoI3BhaW50M19saW5lYXJfODY2Nl82NjI3KSIvPgo8cGF0aCBkPSJNMjMuOTk4OCAxNC4zOTk4QzEzLjM5NSAxNC4zOTk4IDQuNzk4ODMgMTEuMTc2MyA0Ljc5ODgzIDcuMTk5ODNWMjEuNTk5OEM0Ljc5ODgzIDI1LjU3NjMgMTMuMzk1IDI4LjggMjMuOTk4OCAyOC44SDI1LjE5ODhDMjcuMTg3MSAyOC44IDI4Ljc5ODkgMjcuMTg4IDI4Ljc5ODkgMjUuMTk5OFYxNy45OTk4QzI4Ljc5ODkgMTYuMDExNiAyNy4xODcxIDE0LjM5OTggMjUuMTk4OCAxNC4zOTk4SDIzLjk5ODhaIiBmaWxsPSJ1cmwoI3BhaW50NF9yYWRpYWxfODY2Nl82NjI3KSIvPgo8cGF0aCBkPSJNMjMuOTk4OCAxNC4zOTk4QzEzLjM5NSAxNC4zOTk4IDQuNzk4ODMgMTEuMTc2MyA0Ljc5ODgzIDcuMTk5ODNWMjEuNTk5OEM0Ljc5ODgzIDI1LjU3NjMgMTMuMzk1IDI4LjggMjMuOTk4OCAyOC44SDI1LjE5ODhDMjcuMTg3MSAyOC44IDI4Ljc5ODkgMjcuMTg4IDI4Ljc5ODkgMjUuMTk5OFYxNy45OTk4QzI4Ljc5ODkgMTYuMDExNiAyNy4xODcxIDE0LjM5OTggMjUuMTk4OCAxNC4zOTk4SDIzLjk5ODhaIiBmaWxsPSJ1cmwoI3BhaW50NV9yYWRpYWxfODY2Nl82NjI3KSIvPgo8L2c+CjwvZz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzg2NjZfNjYyNyIgeDE9IjM1Ljk5ODIiIHkxPSIxOS4xOTk4IiB4Mj0iMTUuOTMzNCIgeTI9IjM1LjgxOTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzAwOTRGMCIvPgo8c3RvcCBvZmZzZXQ9IjAuMjQzMDQ3IiBzdG9wLWNvbG9yPSIjMDA3OEQ0Ii8+CjxzdG9wIG9mZnNldD0iMC41ODQ0MDQiIHN0b3AtY29sb3I9IiMyMDUyQ0IiLz4KPHN0b3Agb2Zmc2V0PSIwLjgzMDYzOSIgc3RvcC1jb2xvcj0iIzMxMkE5QSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MV9yYWRpYWxfODY2Nl82NjI3IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDMzLjU5ODggMjkuNTQ5OSkgcm90YXRlKDE0My41OTEpIHNjYWxlKDI5LjgyMTEgNTMuMDE1MikiPgo8c3RvcCBzdG9wLWNvbG9yPSIjM0JENUZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwNzhENCIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50Ml9yYWRpYWxfODY2Nl82NjI3IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDM5LjU5ODggMjcuNTk5Nykgcm90YXRlKDE0Ni4zMSkgc2NhbGUoMjUuOTYgNDMuNzk2KSI+CjxzdG9wIHN0b3AtY29sb3I9IiNERUNCRkYiIHN0b3Atb3BhY2l0eT0iMC45Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0RFQ0JGRiIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQzX2xpbmVhcl84NjY2XzY2MjciIHgxPSIzMi4zOTg5IiB5MT0iMS4yIiB4Mj0iOS42NjgzNSIgeTI9IjI1Ljk0ODkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzBGQUZGRiIvPgo8c3RvcCBvZmZzZXQ9IjAuMTYyNzE0IiBzdG9wLWNvbG9yPSIjMDA5NEYwIi8+CjxzdG9wIG9mZnNldD0iMC41NjM4NzEiIHN0b3AtY29sb3I9IiMyMDUyQ0IiLz4KPHN0b3Agb2Zmc2V0PSIwLjc2NDI4MyIgc3RvcC1jb2xvcj0iIzMxMkE5QSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50NF9yYWRpYWxfODY2Nl82NjI3IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDIzLjY2NzggMTIuMTQ4OCkgcm90YXRlKDEzNy41Mykgc2NhbGUoMjQuNjYwNiAzOC40MTE3KSI+CjxzdG9wIHN0b3AtY29sb3I9IiMzQkQ1RkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OEQ0Ii8+CjwvcmFkaWFsR3JhZGllbnQ+CjxyYWRpYWxHcmFkaWVudCBpZD0icGFpbnQ1X3JhZGlhbF84NjY2XzY2MjciIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjcuNTk4OSAxMC43OTk4KSByb3RhdGUoMTQ5LjAzNikgc2NhbGUoMjAuOTkxNCAzMy42MDA1KSI+CjxzdG9wIHN0b3AtY29sb3I9IiNERUNCRkYiIHN0b3Atb3BhY2l0eT0iMC45Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0QxRDFGRiIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfODY2Nl82NjI3Ij4KPHBhdGggZD0iTTAgMTIuOEMwIDguMzE5NTggMCA2LjA3OTM3IDAuODcxOTQ4IDQuMzY4MDhDMS42Mzg5MyAyLjg2Mjc4IDIuODYyNzggMS42Mzg5MyA0LjM2ODA4IDAuODcxOTQ4QzYuMDc5MzcgMCA4LjMxOTU4IDAgMTIuOCAwSDM1LjJDMzkuNjgwNCAwIDQxLjkyMDYgMCA0My42MzE5IDAuODcxOTQ4QzQ1LjEzNzIgMS42Mzg5MyA0Ni4zNjExIDIuODYyNzggNDcuMTI4MSA0LjM2ODA4QzQ4IDYuMDc5MzcgNDggOC4zMTk1OCA0OCAxMi44VjM1LjJDNDggMzkuNjgwNCA0OCA0MS45MjA2IDQ3LjEyODEgNDMuNjMxOUM0Ni4zNjExIDQ1LjEzNzIgNDUuMTM3MiA0Ni4zNjExIDQzLjYzMTkgNDcuMTI4MUM0MS45MjA2IDQ4IDM5LjY4MDQgNDggMzUuMiA0OEgxMi44QzguMzE5NTggNDggNi4wNzkzNyA0OCA0LjM2ODA4IDQ3LjEyODFDMi44NjI3OCA0Ni4zNjExIDEuNjM4OTMgNDUuMTM3MiAwLjg3MTk0OCA0My42MzE5QzAgNDEuOTIwNiAwIDM5LjY4MDQgMCAzNS4yVjEyLjhaIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8Y2xpcFBhdGggaWQ9ImNsaXAxXzg2NjZfNjYyNyI+CjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K"
1206+
1207+ def disconnect (self ):
1208+ with contextlib .suppress (Exception ):
1209+ self .conn .close ()
1210+
1211+ def list_object_types (self ):
1212+ return {
1213+ "database" : ConnectionObjectInfo ({"contains" : None , "icon" : None }),
1214+ "schema" : ConnectionObjectInfo ({"contains" : None , "icon" : None }),
1215+ "table" : ConnectionObjectInfo ({"contains" : "data" , "icon" : None }),
1216+ "view" : ConnectionObjectInfo ({"contains" : "data" , "icon" : None }),
1217+ }
1218+
1219+ def list_objects (self , path : list [ObjectSchema ]):
1220+ if len (path ) == 0 :
1221+ rows = self ._execute ("SELECT name FROM sys.databases ORDER BY name;" )
1222+ return [ConnectionObject ({"name" : row [0 ], "kind" : "database" }) for row in rows ]
1223+
1224+ if len (path ) == 1 :
1225+ database = path [0 ]
1226+ if database .kind != "database" :
1227+ raise ValueError (
1228+ f"Invalid path. Expected it to include a database, but got '{ database .kind } '. Path: { path } "
1229+ )
1230+
1231+ rows = self ._execute (
1232+ f"SELECT name FROM { self ._qualify (database .name , 'sys' , 'schemas' )} ORDER BY name;"
1233+ )
1234+ return [ConnectionObject ({"name" : row [0 ], "kind" : "schema" }) for row in rows ]
1235+
1236+ if len (path ) == 2 :
1237+ database , schema = path
1238+ if database .kind != "database" or schema .kind != "schema" :
1239+ raise ValueError (
1240+ "Path must include a database and schema in this order. "
1241+ f"Got database.kind={ database .kind } , schema.kind={ schema .kind } . Path: { path } "
1242+ )
1243+
1244+ rows = self ._execute (
1245+ f"""
1246+ SELECT TABLE_NAME, TABLE_TYPE
1247+ FROM { self ._qualify (database .name , 'INFORMATION_SCHEMA' , 'TABLES' )}
1248+ WHERE TABLE_SCHEMA = { self ._quote_literal (schema .name )}
1249+ ORDER BY TABLE_NAME;
1250+ """
1251+ )
1252+
1253+ objects : list [ConnectionObject ] = []
1254+ for table_name , table_type in rows :
1255+ kind = "view" if "VIEW" in str (table_type ).upper () else "table"
1256+ objects .append (ConnectionObject ({"name" : table_name , "kind" : kind }))
1257+ return objects
1258+
1259+ raise ValueError (f"Path length must be at most 2, but got { len (path )} . Path: { path } " )
1260+
1261+ def list_fields (self , path : list [ObjectSchema ]):
1262+ if len (path ) != 3 :
1263+ raise ValueError (f"Path length must be 3, but got { len (path )} . Path: { path } " )
1264+
1265+ database , schema , table = path
1266+ if (
1267+ database .kind != "database"
1268+ or schema .kind != "schema"
1269+ or table .kind not in ["table" , "view" ]
1270+ ):
1271+ raise ValueError (
1272+ "Path must include a database, schema and table/view in this order. "
1273+ f"Got database.kind={ database .kind } , schema.kind={ schema .kind } , table.kind={ table .kind } . "
1274+ f"Path: { path } "
1275+ )
1276+
1277+ rows = self ._execute (
1278+ f"""
1279+ SELECT COLUMN_NAME, DATA_TYPE
1280+ FROM { self ._qualify (database .name , 'INFORMATION_SCHEMA' , 'COLUMNS' )}
1281+ WHERE TABLE_SCHEMA = { self ._quote_literal (schema .name )} AND TABLE_NAME = { self ._quote_literal (table .name )}
1282+ ORDER BY ORDINAL_POSITION;
1283+ """
1284+ )
1285+
1286+ return [ConnectionObjectFields ({"name" : name , "dtype" : dtype }) for name , dtype in rows ]
1287+
1288+ def preview_object (self , path : list [ObjectSchema ], var_name : str | None = None ):
1289+ try :
1290+ import pandas as pd
1291+ except ImportError as e :
1292+ raise ModuleNotFoundError ("Pandas is required for previewing SQL Server tables." ) from e
1293+
1294+ if len (path ) != 3 :
1295+ raise ValueError (f"Path length must be 3, but got { len (path )} . Path: { path } " )
1296+
1297+ database , schema , table = path
1298+ if (
1299+ database .kind != "database"
1300+ or schema .kind != "schema"
1301+ or table .kind not in ["table" , "view" ]
1302+ ):
1303+ raise ValueError (
1304+ "Path must include a database, schema and table/view in this order. "
1305+ f"Got database.kind={ database .kind } , schema.kind={ schema .kind } , table.kind={ table .kind } . "
1306+ f"Path: { path } "
1307+ )
1308+
1309+ qualified_name = self ._qualify (database .name , schema .name , table .name )
1310+ query = f"SELECT TOP 1000 * FROM { qualified_name } ;"
1311+ var_name = var_name or "conn"
1312+ preview_df = pd .read_sql (query , self .conn )
1313+ sql_string = (
1314+ f"# { table .name } = pd.read_sql({ query !r} , { var_name } ) "
1315+ f"# where { var_name } is your connection variable"
1316+ )
1317+ return preview_df , sql_string
1318+
1319+ def _execute (self , sql : str ) -> list [tuple [Any , ...]]:
1320+ cursor = self .conn .cursor ()
1321+ try :
1322+ cursor .execute (sql )
1323+ return cursor .fetchall ()
1324+ finally :
1325+ with contextlib .suppress (Exception ):
1326+ cursor .close ()
1327+
1328+ def _fetch_one_value (self , sql : str ) -> Any :
1329+ rows = self ._execute (sql )
1330+ return rows [0 ][0 ] if rows else None
1331+
1332+ def _qualify (self , * parts : str ) -> str :
1333+ return "." .join (self ._quote_identifier (part ) for part in parts )
1334+
1335+ def _quote_identifier (self , identifier : str ) -> str :
1336+ return f"[{ identifier .replace (']' , ']]' )} ]"
1337+
1338+ def _quote_literal (self , value : str ) -> str :
1339+ return "'" + value .replace ("'" , "''" ) + "'"
1340+
1341+ def _is_pyodbc (self ) -> bool :
1342+ return safe_isinstance (self .conn , "pyodbc" , "Connection" )
1343+
1344+ def _pyodbc_getinfo (self , attr : str ) -> str | None :
1345+ try :
1346+ import pyodbc # type: ignore
1347+ except ImportError :
1348+ return None
1349+
1350+ constant = getattr (pyodbc , attr , None )
1351+ if constant is None :
1352+ return None
1353+
1354+ if not safe_isinstance (self .conn , "pyodbc" , "Connection" ):
1355+ return None
1356+
1357+ try :
1358+ value = self .conn .getinfo (constant )
1359+ return str (value ) if value is not None else None
1360+ except Exception :
1361+ return None
1362+
1363+ def _pyodbc_connection_string (self ) -> str | None :
1364+ driver = self ._pyodbc_getinfo ("SQL_DRIVER_NAME" )
1365+ server = self ._pyodbc_getinfo ("SQL_SERVER_NAME" )
1366+ if server is None and self .host != "<unknown>" :
1367+ server = self .host
1368+ database = self ._pyodbc_getinfo ("SQL_DATABASE_NAME" ) or self .database
1369+
1370+ if driver is None and server is None and database is None :
1371+ return None
1372+
1373+ parts = []
1374+ if driver :
1375+ parts .append (f"DRIVER={{{ driver } }}" )
1376+ if server :
1377+ parts .append (f"SERVER={ server } " )
1378+ if database :
1379+ parts .append (f"DATABASE={ database } " )
1380+ parts .append ("Trusted_Connection=yes" )
1381+ return ";" .join (parts ) + ";"
1382+
1383+ def _default_connection_string (self ) -> str :
1384+ server = self .host if self .host != "<unknown>" else "<server>"
1385+ database = self .database or "<database>"
1386+ return (
1387+ "DRIVER={ODBC Driver 18 for SQL Server};"
1388+ f"SERVER={ server } ;"
1389+ f"DATABASE={ database } ;"
1390+ "Trusted_Connection=yes;"
1391+ )
1392+
1393+ def _make_code (self ):
1394+ if self ._is_pyodbc ():
1395+ conn_str = self ._pyodbc_connection_string () or self ._default_connection_string ()
1396+ return (
1397+ "import pyodbc\n " f"conn = pyodbc.connect({ conn_str !r} )\n " "%connection_show conn\n "
1398+ )
1399+
1400+ server = self .host if self .host != "<unknown>" else "<server>"
1401+ database = self .database or "<database>"
1402+ return (
1403+ "import pymssql\n "
1404+ "conn = pymssql.connect(\n "
1405+ f" server={ server !r} ,\n "
1406+ f" database={ database !r} ,\n "
1407+ ")\n "
1408+ "%connection_show conn\n "
1409+ )
1410+
1411+
11621412class DatabricksConnection (Connection ):
11631413 """Support for Databricks connections to databases."""
11641414
0 commit comments