diff --git a/README.rst b/README.rst index 2deab87c..625b1d15 100644 --- a/README.rst +++ b/README.rst @@ -39,6 +39,13 @@ Installation 'ENGINE': 'sql_server.pyodbc' +Regex Support +------------- + +django-mssql-backend supports regex using a CLR .dll file. To install it, run :: + + python manage.py install_regex_clr {database_name} + Configuration ------------- diff --git a/sql_server/pyodbc/creation.py b/sql_server/pyodbc/creation.py index 1c533716..61745b57 100644 --- a/sql_server/pyodbc/creation.py +++ b/sql_server/pyodbc/creation.py @@ -1,3 +1,6 @@ +import binascii +import os + from django.db.backends.base.creation import BaseDatabaseCreation @@ -25,3 +28,68 @@ def sql_table_creation_suffix(self): if collation: suffix.append('COLLATE %s' % collation) return ' '.join(suffix) + + # The following code to add regex support in SQLServer is taken from django-mssql + # see https://bitbucket.org/Manfre/django-mssql + def enable_clr(self): + """ Enables clr for server if not already enabled + This function will not fail if current user doesn't have + permissions to enable clr, and clr is already enabled + """ + with self._nodb_connection.cursor() as cursor: + # check whether clr is enabled + cursor.execute(''' + SELECT value FROM sys.configurations + WHERE name = 'clr enabled' + ''') + res = None + try: + res = cursor.fetchone() + except Exception: + pass + + if not res or not res[0]: + # if not enabled enable clr + cursor.execute("sp_configure 'clr enabled', 1") + cursor.execute("RECONFIGURE") + + cursor.execute("sp_configure 'show advanced options', 1") + cursor.execute("RECONFIGURE") + + cursor.execute("sp_configure 'clr strict security', 0") + cursor.execute("RECONFIGURE") + + def install_regex_clr(self, database_name): + sql = ''' +USE {database_name}; +-- Drop and recreate the function if it already exists +IF OBJECT_ID('REGEXP_LIKE') IS NOT NULL +DROP FUNCTION [dbo].[REGEXP_LIKE] +IF EXISTS(select * from sys.assemblies where name like 'regex_clr') +DROP ASSEMBLY regex_clr +; +CREATE ASSEMBLY regex_clr +FROM 0x{assembly_hex} +WITH PERMISSION_SET = SAFE; +create function [dbo].[REGEXP_LIKE] +( +@input nvarchar(max), +@pattern nvarchar(max), +@caseSensitive int +) +RETURNS INT AS +EXTERNAL NAME regex_clr.UserDefinedFunctions.REGEXP_LIKE + '''.format( + database_name=self.connection.ops.quote_name(database_name), + assembly_hex=self.get_regex_clr_assembly_hex(), + ).split(';') + + self.enable_clr() + + with self._nodb_connection.cursor() as cursor: + for s in sql: + cursor.execute(s) + + def get_regex_clr_assembly_hex(self): + with open(os.path.join(os.path.dirname(__file__), 'regex_clr.dll'), 'rb') as f: + return binascii.hexlify(f.read()).decode('ascii') diff --git a/sql_server/pyodbc/features.py b/sql_server/pyodbc/features.py index 18936d34..6563b9d9 100644 --- a/sql_server/pyodbc/features.py +++ b/sql_server/pyodbc/features.py @@ -25,7 +25,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_ignore_conflicts = False supports_index_on_text_field = False supports_paramstyle_pyformat = False - supports_regex_backreferencing = False + supports_regex_backreferencing = True supports_sequence_reset = False supports_subqueries_in_group_by = False supports_tablespaces = True diff --git a/sql_server/pyodbc/management/__init__.py b/sql_server/pyodbc/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sql_server/pyodbc/management/commands/__init__.py b/sql_server/pyodbc/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sql_server/pyodbc/management/commands/install_regex_clr.py b/sql_server/pyodbc/management/commands/install_regex_clr.py new file mode 100644 index 00000000..6aef0508 --- /dev/null +++ b/sql_server/pyodbc/management/commands/install_regex_clr.py @@ -0,0 +1,25 @@ +# Add regex support in SQLServer +# Code taken from django-mssql (see https://bitbucket.org/Manfre/django-mssql) + +from django.core.management.base import BaseCommand +from django.db import connection + + +class Command(BaseCommand): + help = "Installs the regex_clr.dll assembly with the database" + + requires_model_validation = False + + args = 'database_name' + + def add_arguments(self, parser): + parser.add_argument('database_name') + + def handle(self, *args, **options): + database_name = options['database_name'] + if not database_name: + self.print_help('manage.py', 'install_regex_clr') + return + + connection.creation.install_regex_clr(database_name) + print('Installed regex_clr to database %s' % database_name) diff --git a/sql_server/pyodbc/operations.py b/sql_server/pyodbc/operations.py index 0474e819..74b1c009 100644 --- a/sql_server/pyodbc/operations.py +++ b/sql_server/pyodbc/operations.py @@ -273,7 +273,8 @@ def regex_lookup(self, lookup_type): If the feature is not supported (or part of it is not supported), a NotImplementedError exception can be raised. """ - raise NotImplementedError('SQL Server has no built-in regular expression support.') + match_option = {'iregex': 0, 'regex': 1}[lookup_type] + return "dbo.REGEXP_LIKE(%%s, %%s, %s)=1" % (match_option,) def limit_offset_sql(self, low_mark, high_mark): """Return LIMIT/OFFSET SQL clause.""" diff --git a/sql_server/pyodbc/regex_clr.dll b/sql_server/pyodbc/regex_clr.dll new file mode 100644 index 00000000..8a7efd51 Binary files /dev/null and b/sql_server/pyodbc/regex_clr.dll differ diff --git a/test.sh b/test.sh index 8b9fcd7b..644249f8 100755 --- a/test.sh +++ b/test.sh @@ -12,7 +12,7 @@ git fetch --depth=1 origin +refs/tags/*:refs/tags/* git checkout $DJANGO_VERSION pip install -r tests/requirements/py3.txt -python tests/runtests.py --settings=testapp.settings --noinput \ +python tests/runtests.py --settings=testapp.settings --noinput --keepdb \ aggregation \ aggregation_regress \ annotations \ diff --git a/testapp/settings.py b/testapp/settings.py index 1893cd3c..07106562 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -9,6 +9,7 @@ 'django.contrib.contenttypes', 'django.contrib.staticfiles', 'django.contrib.auth', + 'sql_server.pyodbc', 'testapp', ) diff --git a/tox.ini b/tox.ini index b12664b6..1b56e027 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,8 @@ whitelist_externals = /bin/bash commands = - python manage.py test + python manage.py test --keepdb + python manage.py install_regex_clr test_default bash test.sh deps =