Skip to content

Commit c6ff084

Browse files
Add external language support (#82)
Add external language support - users can add a language_name to run on an already installed external language.
1 parent 2070d75 commit c6ff084

32 files changed

+385
-264
lines changed

Python/buildandinstall.cmd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
del /q dist\*
22
python.exe setup.py sdist --formats=zip
3-
python.exe -m pip install --upgrade --upgrade-strategy only-if-needed --find-links=dist sqlmlutils
3+
python.exe setup.py bdist_wheel
4+
python.exe -m pip install --upgrade --upgrade-strategy only-if-needed --find-links=dist sqlmlutils

Python/dist/sqlmlutils-1.0.3.zip

-26.6 KB
Binary file not shown.

Python/dist/sqlmlutils-1.1.0.zip

26.8 KB
Binary file not shown.

Python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
setup(
77
name='sqlmlutils',
88
packages=['sqlmlutils', 'sqlmlutils/packagemanagement'],
9-
version='1.0.3',
9+
version='1.1.0',
1010
url='https://github.com/Microsoft/sqlmlutils/Python',
1111
license='MIT License',
1212
description='A client side package for working with SQL Server',

Python/sqlmlutils/packagemanagement/packagesqlbuilder.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
class CreateLibraryBuilder(SQLBuilder):
1111

12-
def __init__(self, pkg_name: str, pkg_filename: str, scope: Scope):
12+
def __init__(self, pkg_name: str, pkg_filename: str, scope: Scope, language_name: str):
1313
self._name = clean_library_name(pkg_name)
14+
self._language_name = language_name
1415
self._filename = pkg_filename
1516
self._scope = scope
1617

@@ -23,9 +24,8 @@ def params(self):
2324

2425
@property
2526
def base_script(self) -> str:
26-
sqlpkgname = self._name
2727
authorization = _get_authorization(self._scope)
28-
dummy_spees = _get_dummy_spees()
28+
dummy_spees = _get_dummy_spees(self._language_name)
2929

3030
return """
3131
set NOCOUNT on
@@ -38,30 +38,39 @@ def base_script(self) -> str:
3838
3939
-- Create the library
4040
CREATE EXTERNAL LIBRARY [{sqlpkgname}] {authorization}
41-
FROM (CONTENT = ?) WITH (LANGUAGE = 'Python');
41+
FROM (CONTENT = ?) WITH (LANGUAGE = '{language_name}');
4242
4343
-- Dummy SPEES
4444
{dummy_spees}
4545
""".format(
46-
sqlpkgname=sqlpkgname,
46+
sqlpkgname=self._name,
4747
authorization=authorization,
48-
dummy_spees=dummy_spees
48+
dummy_spees=dummy_spees,
49+
language_name=self._language_name
4950
)
5051

5152

5253
class CheckLibraryBuilder(SQLBuilder):
5354

54-
def __init__(self, pkg_name: str, scope: Scope):
55+
def __init__(self, pkg_name: str, scope: Scope, language_name: str):
5556
self._name = clean_library_name(pkg_name)
57+
self._language_name = language_name
5658
self._scope = scope
59+
60+
if self._language_name == "Python":
61+
self._private_path_env = "MRS_EXTLIB_USER_PATH"
62+
self._public_path_env = "MRS_EXTLIB_SHARED_PATH"
63+
else:
64+
self._private_path_env = "PRIVATELIBPATH"
65+
self._public_path_env = "PUBLICLIBPATH"
5766

5867
@property
5968
def params(self):
6069
return """
6170
import os
6271
import re
63-
_ENV_NAME_USER_PATH = "MRS_EXTLIB_USER_PATH"
64-
_ENV_NAME_SHARED_PATH = "MRS_EXTLIB_SHARED_PATH"
72+
_ENV_NAME_USER_PATH = "{private_path_env}"
73+
_ENV_NAME_SHARED_PATH = "{public_path_env}"
6574
6675
def _is_dist_info_file(name, file):
6776
return re.match(name + r"-.*egg", file) or re.match(name + r"-.*dist-info", file)
@@ -92,29 +101,33 @@ def package_exists_in_scope(sql_package_name: str, scope=None) -> bool:
92101
# Check that the package exists in scope.
93102
# For some reason this check works but there is a bug in pyODBC when asserting this is True.
94103
assert package_exists_in_scope("{name}", "{scope}") != False
95-
""".format(name=self._name, scope=self._scope._name)
104+
""".format(private_path_env=self._private_path_env,
105+
public_path_env=self._public_path_env,
106+
name=self._name,
107+
scope=self._scope._name)
96108

97109
@property
98110
def base_script(self) -> str:
99111
return """
100112
-- Check to make sure the package was installed
101113
BEGIN TRY
102-
exec sp_execute_external_script
103-
@language = N'Python',
114+
EXEC sp_execute_external_script
115+
@language = N'{language_name}',
104116
@script = ?
105117
print('Package successfully installed.')
106118
END TRY
107119
BEGIN CATCH
108120
print('Package installation failed.');
109121
THROW;
110122
END CATCH
111-
"""
123+
""".format(language_name = self._language_name)
112124

113125

114126
class DropLibraryBuilder(SQLBuilder):
115127

116-
def __init__(self, sql_package_name: str, scope: Scope):
128+
def __init__(self, sql_package_name: str, scope: Scope, language_name: str):
117129
self._name = clean_library_name(sql_package_name)
130+
self._language_name = language_name
118131
self._scope = scope
119132

120133
@property
@@ -126,10 +139,9 @@ def base_script(self) -> str:
126139
""".format(
127140
name=self._name,
128141
auth=_get_authorization(self._scope),
129-
dummy_spees=_get_dummy_spees()
142+
dummy_spees=_get_dummy_spees(self._language_name)
130143
)
131144

132-
133145
def clean_library_name(pkgname: str):
134146
return pkgname.replace("-", "_").lower()
135147

@@ -138,9 +150,9 @@ def _get_authorization(scope: Scope) -> str:
138150
return "AUTHORIZATION dbo" if scope == Scope.public_scope() else ""
139151

140152

141-
def _get_dummy_spees() -> str:
153+
def _get_dummy_spees(language_name: str) -> str:
142154
return """
143-
exec sp_execute_external_script
144-
@language = N'Python',
155+
EXEC sp_execute_external_script
156+
@language = N'{language_name}',
145157
@script = N''
146-
"""
158+
""".format(language_name = language_name)

Python/sqlmlutils/packagemanagement/pipdownloader.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
class PipDownloader:
1414

15-
def __init__(self, connection: ConnectionInfo, downloaddir: str, targetpackage: str):
15+
def __init__(self, connection: ConnectionInfo, downloaddir: str, targetpackage: str, language_name: str):
1616
self._connection = connection
1717
self._downloaddir = downloaddir
1818
self._targetpackage = targetpackage
19-
server_info = SQLPythonExecutor(connection).execute_function_in_sql(servermethods.get_server_info)
19+
self._language_name = language_name
20+
server_info = SQLPythonExecutor(connection, self._language_name).execute_function_in_sql(servermethods.get_server_info)
2021
globals().update(server_info)
2122

2223
def download(self):

Python/sqlmlutils/packagemanagement/servermethods.py

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66

77
from sqlmlutils.packagemanagement.scope import Scope
88

9-
_ENV_NAME_USER_PATH = "MRS_EXTLIB_USER_PATH"
10-
_ENV_NAME_SHARED_PATH = "MRS_EXTLIB_SHARED_PATH"
11-
129
def show_installed_packages():
1310
import pkg_resources
1411
return [(d.project_name, d.version) for d in pkg_resources.working_set]
@@ -30,40 +27,3 @@ def get_server_info():
3027
"abi_tag": pep425tags.get_abi_tag(), #'cp37m'
3128
"platform": sysconfig.get_platform().replace("-","_") #'win_amd64', 'linux_x86_64'
3229
}
33-
34-
35-
def check_package_install_success(sql_package_name: str) -> bool:
36-
return package_exists_in_scope(sql_package_name)
37-
38-
39-
def package_files_in_scope(scope=Scope.private_scope()):
40-
envdir = _ENV_NAME_SHARED_PATH if scope == Scope.public_scope() or os.environ.get(_ENV_NAME_USER_PATH, "") == "" \
41-
else _ENV_NAME_USER_PATH
42-
path = os.environ.get(envdir, "")
43-
if os.path.isdir(path):
44-
return os.listdir(path)
45-
return []
46-
47-
48-
def package_exists_in_scope(sql_package_name: str, scope=None) -> bool:
49-
if scope is None:
50-
# default to user path for every user but DBOs
51-
scope = Scope.public_scope() if (os.environ.get(_ENV_NAME_USER_PATH, "") == "") else Scope.private_scope()
52-
package_files = package_files_in_scope(scope)
53-
return any([_is_package_match(sql_package_name, package_file) for package_file in package_files])
54-
55-
56-
def _is_dist_info_file(name, file):
57-
return re.match(name + r'-.*egg', file) or re.match(name + r'-.*dist-info', file)
58-
59-
60-
def _is_package_match(package_name, file):
61-
package_name = package_name.lower()
62-
file = file.lower()
63-
return file == package_name or file == package_name + ".py" or \
64-
_is_dist_info_file(package_name, file) or \
65-
("-" in package_name and
66-
(package_name.split("-")[0] == file or _is_dist_info_file(package_name.replace("-", "_"), file)))
67-
68-
69-

Python/sqlmlutils/packagemanagement/sqlpackagemanager.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919

2020
class SQLPackageManager:
2121

22-
def __init__(self, connection_info: ConnectionInfo):
22+
def __init__(self, connection_info: ConnectionInfo, language_name: str = "Python"):
23+
"""Initialize a SQLPackageManager to manage packages on the SQL Server.
24+
25+
:param connection_info: The ConnectionInfo object that holds the connection string and other information.
26+
:param language_name: The name of the language to be executed in sp_execute_external_script, if using EXTERNAL LANGUAGE.
27+
"""
2328
self._connection_info = connection_info
24-
self._pyexecutor = SQLPythonExecutor(connection_info)
29+
self._pyexecutor = SQLPythonExecutor(connection_info, language_name=language_name)
30+
self._language_name = language_name
2531

2632
def install(self,
2733
package: str,
@@ -104,29 +110,32 @@ def _get_default_scope(self):
104110
return Scope.public_scope() if is_sysadmin == 1 else Scope.private_scope()
105111

106112
def _get_packages_by_user(self, owner='', scope: Scope=Scope.private_scope()):
107-
has_user = (owner != '')
113+
scope_num = 1 if scope == Scope.private_scope() else 0
114+
115+
if scope_num == 0 and owner == '':
116+
owner = "dbo"
108117

109118
query = "DECLARE @principalId INT; \
110119
DECLARE @currentUser NVARCHAR(128); \
111120
SELECT @currentUser = "
112121

113-
if has_user:
122+
if owner != '':
114123
query += "?;\n"
115124
else:
116125
query += "CURRENT_USER;\n"
117126

118-
scope_num = 1 if scope == Scope.private_scope() else 0
119-
120127
query += "SELECT @principalId = USER_ID(@currentUser); \
121128
SELECT name, language, scope \
122129
FROM sys.external_libraries AS elib \
123130
WHERE elib.principal_id=@principalId \
124-
AND elib.language='Python' AND elib.scope={scope_num} \
125-
ORDER BY elib.name ASC;".format(scope_num=scope_num)
131+
AND elib.language='{language_name}' AND elib.scope={scope_num} \
132+
ORDER BY elib.name ASC; \
133+
GO".format(language_name=self._language_name,
134+
scope_num=scope_num)
126135
return self._pyexecutor.execute_sql_query(query, owner)
127136

128137
def _drop_sql_package(self, sql_package_name: str, scope: Scope, out_file: str = None):
129-
builder = DropLibraryBuilder(sql_package_name=sql_package_name, scope=scope)
138+
builder = DropLibraryBuilder(sql_package_name=sql_package_name, scope=scope, language_name=self._language_name)
130139
execute_query(builder, self._connection_info, out_file)
131140

132141
# TODO: Support not dependencies
@@ -146,7 +155,7 @@ def _install_from_pypi(self,
146155
target_package = target_package + "==" + version
147156

148157
with tempfile.TemporaryDirectory() as temporary_directory:
149-
pipdownloader = PipDownloader(self._connection_info, temporary_directory, target_package)
158+
pipdownloader = PipDownloader(self._connection_info, temporary_directory, target_package, language_name = self._language_name)
150159
target_package_file = pipdownloader.download_single()
151160
self._install_from_file(target_package_file, scope, upgrade, out_file=out_file)
152161

@@ -162,7 +171,7 @@ def _install_from_file(self, target_package_file: str, scope: Scope, upgrade: bo
162171

163172
# Download requirements from PyPI
164173
with tempfile.TemporaryDirectory() as temporary_directory:
165-
pipdownloader = PipDownloader(self._connection_info, temporary_directory, target_package_file)
174+
pipdownloader = PipDownloader(self._connection_info, temporary_directory, target_package_file, language_name = self._language_name)
166175

167176
# For now, we download all target package dependencies from PyPI.
168177
target_package_requirements, requirements_downloaded = pipdownloader.download()
@@ -189,8 +198,7 @@ def _install_many(self, target_package_file: str, dependency_files, scope: Scope
189198
sqlexecutor._cnxn.rollback()
190199
raise RuntimeError("Package installation failed, installed dependencies were rolled back.") from e
191200

192-
@staticmethod
193-
def _install_single(sqlexecutor: SQLQueryExecutor, package_file: str, scope: Scope, is_target=False, out_file: str=None):
201+
def _install_single(self, sqlexecutor: SQLQueryExecutor, package_file: str, scope: Scope, is_target=False, out_file: str=None):
194202
name = str(get_package_name_from_file(package_file))
195203
version = str(get_package_version_from_file(package_file))
196204
print("Installing {name} version: {version}".format(name=name, version=version))
@@ -200,9 +208,10 @@ def _install_single(sqlexecutor: SQLQueryExecutor, package_file: str, scope: Sco
200208
with zipfile.ZipFile(prezip, 'w') as zipf:
201209
zipf.write(package_file, os.path.basename(package_file))
202210

203-
builder = CreateLibraryBuilder(pkg_name=name, pkg_filename=prezip, scope=scope)
211+
builder = CreateLibraryBuilder(pkg_name=name, pkg_filename=prezip, scope=scope, language_name=self._language_name)
204212
sqlexecutor.execute(builder, out_file=out_file)
205-
builder = CheckLibraryBuilder(pkg_name=name, scope=scope)
213+
214+
builder = CheckLibraryBuilder(pkg_name=name, scope=scope, language_name=self._language_name)
206215
sqlexecutor.execute(builder, out_file=out_file)
207216

208217
@staticmethod

0 commit comments

Comments
 (0)