├── CONTRIBUTING ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py └── sqlany_django ├── __init__.py ├── base.py ├── client.py ├── compiler.py ├── creation.py ├── introspection.py ├── schema.py └── validation.py /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Stay tuned for a detailed description of how to contribute to this project 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 SAP AG or an SAP affiliate company. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of SAP nor the names of its contributors may be used 11 | to endorse or promote products derived from this software without 12 | specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY SAP ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 17 | NO EVENT SHALL SAP BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 19 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT INCLUDING 22 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. *************************************************************************** 2 | .. Copyright (c) 2015 SAP AG or an SAP affiliate company. All rights reserved. 3 | .. *************************************************************************** 4 | 5 | SQL Anywhere Django Driver 6 | ========================== 7 | This is a SQL Anywhere database backend for Django. The backend is 8 | distributed as a stand-alone python module. This backend has been 9 | tested with SQL Anywhere versions 12, 16, and 17 using Django versions 1.1.4, 10 | 1.2.7, 1.3.7, 1.4.10, 1.5.5, 1.6.1, 1.7.1, and 1.8.5. 11 | 12 | #. Install the required software 13 | 14 | (a) SQL Anywhere 12.0.0 (or higher) 15 | 16 | The SQL Anywhere Web Edition is a free, full-featured version for 17 | development and deployment of browser based applications. If you don't 18 | already have a license for SQL Anywhere, the Web Edition is a great 19 | place to start. Get the Web Edition at 20 | http://www.sybase.com/detail?id=1057560 21 | 22 | (b) Python (2.4 or greater) 23 | 24 | Install Python if you don't already have it installed. We recommend 25 | Python 2.7 but any version greater than 2.4 is supported. Python 3 is 26 | supported in Django 1.6 and later. You can download python from 27 | http://www.python.org/download/ 28 | 29 | If you are running on Linux you will most likely also be able to find 30 | python through your distribution's package management system. 31 | 32 | (c) Python setuptools 33 | 34 | The setuptools project for python acts as a package manager for Python 35 | code. Using setuptools will make it trivial to install the correct 36 | version of Django to use with SQL Anywhere. You can get setuptools for 37 | python from http://pypi.python.org/pypi/setuptools/ 38 | 39 | Again, if you are running on Linux you most likely be able to find 40 | setuptools through your distribution's package management 41 | system. This package is called "python-setuptools" on Ubuntu and 42 | "python-setuptools-devel" on Fedora. 43 | 44 | (d) Django 45 | 46 | Once you have installed setuptools, installing Django is a snap, simply run:: 47 | 48 | $ easy_install Django 49 | 50 | If you want a specific version of Django, you can give the version using 51 | the == syntax. For example, if you want 1.6.1, you can use:: 52 | 53 | $ easy_install Django==1.6.1 54 | 55 | (e) Python SQL Anywhere Database Interface 56 | 57 | If you are using pip to install the SQL Anywhere Django driver, you can 58 | skip this step since the SQL Anywhere Python driver will be installed 59 | as part of that step. 60 | 61 | The SQL Anywhere Database Interface for Python provides a Database API v2 62 | compliant driver (see Python PEP 249) for accessing SQL Anywhere 63 | databases from Python. The SQL Anywhere backend for Django is built on 64 | top of this interface so installing it is required. 65 | 66 | You can use pip to make this easy:: 67 | 68 | pip install sqlanydb 69 | 70 | Alternatively, you can obtain the Python SQL Anywhere Database Interface 71 | from https://github.com/sqlanywhere/sqlanydb. Install the driver by 72 | downloading the source and running the following command:: 73 | 74 | $ python setup.py install 75 | 76 | (f) SQL Anywhere Django Backend 77 | 78 | Again, use pip to install this easily:: 79 | 80 | pip install sqlany-django 81 | 82 | This will install the SQL Anywhere python driver if it was not already 83 | installed. 84 | 85 | Or you can obtain the SQL Anywhere Database backend for Django from 86 | https://github.com/sqlanywhere/sqlany-django/. Install the backend by 87 | downloading the source and running the following command:: 88 | 89 | $ python setup.py install 90 | 91 | #. Setup your environment 92 | 93 | (Linux/Unix/Mac OS X only) 94 | 95 | SQL Anywhere requires several environment variables to be set to run 96 | correctly -- the most important of which are PATH and 97 | LD_LIBRARY_PATH. The SQL Anywhere install creates a file named 98 | sa_config.sh to set all necessary environment variables automatically 99 | (Note the file is named sa_config.csh if you are using a csh 100 | derivative as your shell). 101 | 102 | This file is located in the "bin32" and/or the "bin64" directories of 103 | your install. Before trying to run the SQL Anywhere server or connect 104 | to a running server in a given shell you should make sure to source 105 | the file (with the "." command) corresponding to the bitness of the 106 | SQL Anywhere binaries you want to use. For example, if you are running 64-bit 107 | software and the product is installed in /opt/sqlanywhere16 you should run:: 108 | 109 | $ . /opt/sqlanywhere16/bin64/sa_config.sh 110 | 111 | #. Create a database 112 | 113 | Issue the following command to create a new database to use with 114 | Django. Note that we are specifying the UCA collation so that that CHAR 115 | columns in the database will support unicode strings. :: 116 | 117 | $ dbinit -z UCA django.db 118 | 119 | If all goes well SQL Anywhere will have created a new database file 120 | named 'django.db' in the directory where you ran the dbinit 121 | command. Feel free to move this database file to any location you 122 | want. You can even copy it to a machine running a different operating 123 | system if you wish. 124 | 125 | #. Start the Database Server 126 | 127 | SQL Anywhere includes two different database servers -- The personal 128 | server (dbeng12/dbeng16) and the network server (dbsrv12/dbsrv16). Both 129 | servers support the same complete set of features except that the 130 | personal server is limited to running on one CPU, allows a maximum of 131 | 10 concurrent connections and does not accept network connections from 132 | other machines. We will use the network server for our example. :: 133 | 134 | $ dbsrv16 django.db 135 | 136 | #. Configure Django 137 | 138 | Creating a new Django site and configuring it to use SQL Anywhere is 139 | very easy. First create the site in the normal fashion:: 140 | 141 | $ django-admin.py startproject mysite 142 | 143 | Then edit the file mysite/mysite/settings.py and change the DATABASES 144 | setting to match what is given below:: 145 | 146 | DATABASES = { 147 | 'default' : { 148 | 'ENGINE': 'sqlany_django', 149 | 'NAME': 'django', 150 | 'USER': 'dba', 151 | 'PASSWORD': 'sql', 152 | 'HOST': 'myhost', 153 | 'PORT': 'portnum' 154 | } 155 | } 156 | 157 | Here's how the parameters correspond to SQL Anywhere connection parameters: 158 | 159 | * NAME = DatabaseName (DBN) 160 | * USER = Userid (UID) 161 | * PASSWORD = Password (PWD) 162 | * HOST = Host 163 | * PORT = (port number in host, i.e. myhost:portnum) 164 | 165 | If you need to specify other connection parameters (eg. ENG), 166 | you can set a value with the key "OPTIONS", like this:: 167 | 168 | DATABASES = { 169 | 'default' : { 170 | 'ENGINE': 'sqlany_django', 171 | 'NAME': 'django', 172 | 'USER': 'dba', 173 | 'PASSWORD': 'sql', 174 | 'OPTIONS': {'eng': 'django'} 175 | } 176 | } 177 | 178 | HOST and PORT default to 'localhost' and '2638'. If you want to use shared 179 | memory, set the HOST and PORT values to None:: 180 | 181 | DATABASES = { 182 | 'default' : { 183 | 'ENGINE': 'sqlany_django', 184 | 'NAME': 'django', 185 | 'USER': 'dba', 186 | 'PASSWORD': 'sql', 187 | 'OPTIONS': {'eng': 'django'}, 188 | 'HOST': None, 189 | 'PORT': None 190 | } 191 | } 192 | 193 | Alternatively, you can set the parameters in an ODBC data source using the 194 | dbdsn utility and then specify the DSN connection parameter. The ENGINE 195 | parameter must still be specified. Any other parameters (eg. USER, HOST, etc.) 196 | that are specified will override the value in the DSN. For example:: 197 | 198 | DATABASES = { 199 | 'default' : { 200 | 'ENGINE': 'sqlany_django', 201 | 'OPTIONS': {'dsn': 'my_django_dsn'} 202 | } 203 | } 204 | 205 | Note: SQL Anywhere allows you to run several database servers on one 206 | machine. For this reason you should always specify the server you want 207 | to connect to as well as the database name. However if you want to connect to 208 | a server running in a SA OnDemand (cloud) environment, you should specify the 209 | NAME and HOST (and optionally PORT) options, and *not* specify the server name. 210 | 211 | #. Test to make sure everything is working 212 | 213 | The SQL Anywhere database backend for Django makes use of the Python 214 | SQL Anywhere Database interface. We first want to test that this 215 | interface is working correctly before testing Django connectivity 216 | itself. Create a file named test_sqlany.py with the following 217 | contents:: 218 | 219 | import sqlanydb 220 | conn = sqlanydb.connect(uid='dba', pwd='sql', eng='django', dbn='django') 221 | curs = conn.cursor() 222 | curs.execute("select 'Hello, world!'") 223 | print "SQL Anywhere says: %s" % curs.fetchone() 224 | curs.close() 225 | conn.close() 226 | 227 | Run the test script and ensure that you get the expected output:: 228 | 229 | $ python test_sqlany.py 230 | SQL Anywhere says: Hello, world! 231 | 232 | To test that Django can make use of the SQL Anywhere Database backend 233 | simply change to the "mysite" directory created in step 5 and ask 234 | Django to create the tables for the default applications. :: 235 | 236 | $ python manage.py syncdb 237 | 238 | If you don't receive any errors at this point then 239 | congratulations. Django is now correctly configured to use SQL 240 | Anywhere as a backend. 241 | 242 | #. What to do if you have problems? 243 | 244 | If you run into problems, don't worry. First try re-reading the 245 | instructions above and make sure you haven't missed a step. If you are 246 | still having issues here are a few resources to help you figure 247 | out what went wrong. You can consult the documentation, or post to a 248 | forum where many of the SQL Anywhere engineers hang out. 249 | 250 | | SQL Anywhere Online Documentation: http://dcx.sap.com/ 251 | | SQL Anywhere Development Forum: http://sqlanywhere-forum.sap.com/ 252 | 253 | #. Where to go from here? 254 | 255 | SQL Anywhere should now be successfully configured as a backend for 256 | your Django site. To learn more about creating web applications with 257 | Django try the excellent series of tutorials provided by the Django 258 | project: 259 | http://docs.djangoproject.com/en/dev/intro/tutorial01/#intro-tutorial01 260 | 261 | License 262 | ------- 263 | This package is licensed under the terms of the license described in 264 | the LICENSE file. 265 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # *************************************************************************** 3 | # Copyright (c) 2013 SAP AG or an SAP affiliate company. All rights reserved. 4 | # *************************************************************************** 5 | 6 | r"""sqlany-django - SQL Anywhere driver for Django. 7 | 8 | https://github.com/sqlanywhere/sqlany-django 9 | 10 | ----------------------------------------------------------------""" 11 | 12 | from setuptools import setup, find_packages 13 | import os,re 14 | 15 | with open( os.path.join( os.path.dirname(__file__), 'sqlany_django', 16 | '__init__.py' ) ) as v: 17 | VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1) 18 | 19 | setup(name='sqlany_django', 20 | version=VERSION, 21 | description='SQL Anywhere database backend for Django', 22 | long_description=open('README.rst').read(), 23 | author='Graeme Perrow', 24 | author_email='graeme.perrow@sap.com', 25 | install_requires=['sqlanydb >= 1.0.4'], 26 | url='https://github.com/sqlanywhere/sqlany-django', 27 | packages=find_packages(), 28 | license='New BSD', 29 | classifiers=[ 30 | 'Development Status :: 5 - Production/Stable', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Natural Language :: English', 34 | 'Operating System :: OS Independent', 35 | 'Framework :: Django', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.4', 38 | 'Programming Language :: Python :: 2.5', 39 | 'Programming Language :: Python :: 2.6', 40 | 'Programming Language :: Python :: 2.7', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.0', 43 | 'Programming Language :: Python :: 3.1', 44 | 'Programming Language :: Python :: 3.2', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Topic :: Database', 48 | 'Topic :: Software Development :: Libraries :: Python Modules' 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /sqlany_django/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.13' 2 | -------------------------------------------------------------------------------- /sqlany_django/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | SQL Anywhere database backend for Django. 3 | 4 | Requires sqlanydb 5 | """ 6 | 7 | import re,ctypes,sys 8 | 9 | try: 10 | import sqlanydb as Database 11 | except ImportError as e: 12 | from django.core.exceptions import ImproperlyConfigured 13 | raise ImproperlyConfigured("Error loading sqlanydb module: %s" % e) 14 | 15 | from django import VERSION as djangoVersion 16 | 17 | if djangoVersion[:2] >= (1, 4): 18 | from django.utils.timezone import is_aware, is_naive, utc, make_naive, make_aware, get_default_timezone 19 | import datetime 20 | 21 | from django.conf import settings 22 | if djangoVersion[:2] >= (1, 8): 23 | from django.db.backends.base.features import BaseDatabaseFeatures 24 | from django.db.backends.base.operations import BaseDatabaseOperations 25 | from django.db.backends.base.base import BaseDatabaseWrapper 26 | from django.db.backends import utils as util 27 | else: 28 | from django.db.backends import * 29 | if djangoVersion[:2] >= (1, 7): 30 | # renamed in 1.7 31 | util = utils 32 | from django.db.backends.signals import connection_created 33 | from sqlany_django.client import DatabaseClient 34 | from sqlany_django.creation import DatabaseCreation 35 | from sqlany_django.introspection import DatabaseIntrospection 36 | from sqlany_django.validation import DatabaseValidation 37 | if djangoVersion[:2] >= (1, 7): 38 | from sqlany_django.schema import DatabaseSchemaEditor 39 | if djangoVersion[:2] >= (1, 8): 40 | from sqlany_django.creation import global_data_types 41 | 42 | DatabaseError = Database.DatabaseError 43 | IntegrityError = Database.IntegrityError 44 | 45 | Database.register_converter(Database.DT_TIMESTAMP, util.typecast_timestamp) 46 | Database.register_converter(Database.DT_DATE, util.typecast_date) 47 | Database.register_converter(Database.DT_TIME, util.typecast_time) 48 | Database.register_converter(Database.DT_DECIMAL, util.typecast_decimal) 49 | Database.register_converter(Database.DT_BIT, lambda x: x if x is None else bool(x)) 50 | 51 | def trace(x): 52 | # print( x ) 53 | return x 54 | 55 | def _datetimes_in(args): 56 | def fix(arg): 57 | if isinstance(arg, datetime.datetime): 58 | if is_naive(arg): 59 | warnings.warn("Received a naive datetime (%s) while timezone support is active." % arg, RuntimeWarning) 60 | arg = make_aware(arg, timezone.get_default_timezone()) 61 | arg = arg.astimezone(utc).replace(tzinfo=None) 62 | return arg 63 | 64 | return tuple(fix(arg) for arg in args) 65 | 66 | class CursorWrapper(object): 67 | """ 68 | A thin wrapper around sqlanydb's normal cursor class so that we can catch 69 | particular exception instances and reraise them with the right types. 70 | 71 | Implemented as a wrapper, rather than a subclass, so that we aren't stuck 72 | to the particular underlying representation returned by Connection.cursor(). 73 | """ 74 | codes_for_integrityerror = (1048,) 75 | 76 | def __init__(self, cursor): 77 | self.cursor = cursor 78 | 79 | def __del__(self): 80 | if self.cursor: 81 | self.cursor.close() 82 | self.cursor = None 83 | 84 | def convert_query(self, query, num_params): 85 | """ 86 | Django uses "format" style placeholders, but SQL Anywhere uses "qmark" style. 87 | This fixes it -- but note that if you want to use a literal "%s" in a query, 88 | you'll need to use "%%s". 89 | """ 90 | return query if num_params == 0 else query % tuple("?" * num_params) 91 | 92 | def execute(self, query, args=()): 93 | if djangoVersion[:2] >= (1, 4) and settings.USE_TZ: 94 | args = _datetimes_in(args) 95 | try: 96 | if args != None: 97 | query = self.convert_query(query, len(args)) 98 | ret = self.cursor.execute(trace(query), trace(args)) 99 | return ret 100 | except Database.OperationalError as e: 101 | if e.message == 'Connection was terminated': 102 | from django import db 103 | try: 104 | db.close_old_connections() 105 | except AttributeError: 106 | db.close_connection() 107 | # Map some error codes to IntegrityError, since they seem to be 108 | # misclassified and Django would prefer the more logical place. 109 | if e.errorcode in self.codes_for_integrityerror: 110 | raise Database.IntegrityError(e) 111 | raise 112 | 113 | def executemany(self, query, args): 114 | if djangoVersion[:2] >= (1, 4) and settings.USE_TZ: 115 | args = tuple(_datetimes_in(arg) for arg in args) 116 | try: 117 | try: 118 | len(args) 119 | except TypeError: 120 | args = tuple(args) 121 | if len(args) > 0: 122 | query = self.convert_query(query, len(args[0])) 123 | ret = self.cursor.executemany(trace(query), trace(args)) 124 | return trace(ret) 125 | else: 126 | return None 127 | except Database.OperationalError as e: 128 | # Map some error codes to IntegrityError, since they seem to be 129 | # misclassified and Django would prefer the more logical place. 130 | if e.errorcode in self.codes_for_integrityerror: 131 | raise Database.IntegrityError(e) 132 | raise 133 | 134 | def fetchone(self): 135 | if djangoVersion[:2] < (1, 4) or not settings.USE_TZ: 136 | return trace(self.cursor.fetchone()) 137 | return self._datetimes_out(self.cursor.fetchone()) 138 | 139 | def fetchmany(self, size=0): 140 | if djangoVersion[:2] < (1, 4) or not settings.USE_TZ: 141 | return trace(self.cursor.fetchmany(size)) 142 | rows = self.cursor.fetchmany(size) 143 | return list(self._datetimes_out(row) for row in rows) 144 | 145 | def fetchall(self): 146 | if djangoVersion[:2] < (1, 4) or not settings.USE_TZ: 147 | return trace(self.cursor.fetchall()) 148 | return list(self._datetimes_out(row) for row in self.cursor.fetchall()) 149 | 150 | def _datetimes_out(self, row): 151 | def fix(item): 152 | value, desc = item 153 | if desc[1] == Database.DATETIME: 154 | if value is not None and is_naive(value): 155 | value = value.replace(tzinfo=utc) 156 | return value 157 | 158 | if row is None: 159 | return row 160 | 161 | return trace(tuple(fix(item) for item in zip(row, self.cursor.description))) 162 | 163 | def __getattr__(self, attr): 164 | if attr in self.__dict__: 165 | return self.__dict__[attr] 166 | else: 167 | return getattr(self.cursor, attr) 168 | 169 | def __iter__(self): 170 | return iter(self.fetchall()) 171 | 172 | class DatabaseFeatures(BaseDatabaseFeatures): 173 | allows_group_by_pk = False 174 | empty_fetchmany_value = [] 175 | has_bulk_insert = True 176 | has_select_for_update = True 177 | has_zoneinfo_database = False 178 | related_fields_match_type = True 179 | supports_regex_backreferencing = False 180 | supports_sequence_reset = False 181 | update_can_self_select = False 182 | uses_custom_query_class = False 183 | 184 | class DatabaseOperations(BaseDatabaseOperations): 185 | compiler_module = "sqlany_django.compiler" 186 | 187 | def bulk_insert_sql(self, fields, num_values): 188 | items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) 189 | return "VALUES " + ", ".join([items_sql] * num_values) 190 | 191 | def date_extract_sql(self, lookup_type, field_name): 192 | """ 193 | Given a lookup_type of 'year', 'month' or 'day', returns the SQL that 194 | extracts a value from the given date field field_name. 195 | """ 196 | if lookup_type == 'week_day': 197 | # Returns an integer, 1-7, Sunday=1 198 | return "DATEFORMAT(%s, 'd')" % field_name 199 | else: 200 | # YEAR(), MONTH(), DAY() functions 201 | return "%s(%s)" % (lookup_type.upper(), field_name) 202 | 203 | if djangoVersion[:2] >= (1, 8): 204 | # SQL Anywhere does not support the INTERVAL syntax 205 | pass 206 | #def date_interval_sql(self, timedelta): 207 | else: 208 | def date_interval_sql(self, sql, connector, timedelta): 209 | """ 210 | Implements the date interval functionality for expressions 211 | """ 212 | return 'DATEADD(day, %s(%d), DATEADD(second, %s(%d), DATEADD(microsecond, %s(%d), %s)))' % (connector, timedelta.days, connector, timedelta.seconds, connector, timedelta.microseconds, sql) 213 | 214 | def date_trunc_sql(self, lookup_type, field_name): 215 | """ 216 | Given a lookup_type of 'year', 'month' or 'day', returns the SQL that 217 | truncates the given date field field_name to a DATE object with only 218 | the given specificity. 219 | """ 220 | fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] 221 | format = ('YYYY-', 'MM', '-DD', 'HH:', 'NN', ':SS') # Use double percents to escape. 222 | format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') 223 | try: 224 | i = fields.index(lookup_type) + 1 225 | except ValueError: 226 | sql = field_name 227 | else: 228 | format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) 229 | sql = "CAST(DATEFORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) 230 | return sql 231 | 232 | def datetime_extract_sql(self, lookup_type, field_name, tzname): 233 | """ 234 | Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or 235 | 'second', returns the SQL that extracts a value from the given 236 | datetime field field_name, and a tuple of parameters. 237 | """ 238 | if lookup_type == 'week_day': 239 | # Returns an integer, 1-7, Sunday=1 240 | sql = "DATEFORMAT(%s, 'd')" % field_name 241 | else: 242 | # YEAR(), MONTH(), DAY(), HOUR(), MINUTE(), SECOND() functions 243 | sql = "%s(%s)" % (lookup_type.upper(), field_name) 244 | return sql,[] 245 | 246 | def datetime_trunc_sql(self, lookup_type, field_name, tzname): 247 | """ 248 | Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or 249 | 'second', returns the SQL that truncates the given datetime field 250 | field_name to a datetime object with only the given specificity, and 251 | a tuple of parameters. 252 | """ 253 | fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] 254 | format = ('YYYY-', 'MM', '-DD', 'HH:', 'NN', ':SS') # Use double percents to escape. 255 | format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') 256 | try: 257 | i = fields.index(lookup_type) + 1 258 | except ValueError: 259 | sql = field_name 260 | else: 261 | format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) 262 | sql = "CAST(DATEFORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) 263 | return sql,[] 264 | 265 | def deferrable_sql(self): 266 | return "" 267 | 268 | def drop_foreignkey_sql(self): 269 | """ 270 | Returns the SQL command that drops a foreign key. 271 | """ 272 | # This will work provided it is inserted in an ALTER TABLE statement 273 | return "DROP FOREIGN KEY" 274 | 275 | def force_no_ordering(self): 276 | """ 277 | "ORDER BY NULL" prevents SQL Anywhere from implicitly ordering by grouped 278 | columns. If no ordering would otherwise be applied, we don't want any 279 | implicit sorting going on. 280 | """ 281 | return ["NULL"] 282 | 283 | def fulltext_search_sql(self, field_name): 284 | """ 285 | Returns the SQL WHERE clause to use in order to perform a full-text 286 | search of the given field_name. Note that the resulting string should 287 | contain a '%s' placeholder for the value being searched against. 288 | """ 289 | return 'CONTAINS(%s, %%s)' % field_name 290 | 291 | def last_insert_id(self, cursor, table_name, pk_name): 292 | cursor.execute('SELECT @@identity') 293 | return cursor.fetchone()[0] 294 | 295 | def max_name_length(self): 296 | """ 297 | Returns the maximum length of table and column names, or None if there 298 | is no limit. 299 | """ 300 | # SQL Anywhere 11 has a maximum of 128 for table and column names 301 | return 128 302 | 303 | def no_limit_value(self): 304 | """ 305 | Returns the value to use for the LIMIT when we are wanting "LIMIT 306 | infinity". Returns None if the limit clause can be omitted in this case. 307 | """ 308 | return None 309 | 310 | def prep_for_iexact_query(self, x): 311 | return x 312 | 313 | def query_class(self, DefaultQueryClass): 314 | """ 315 | Given the default Query class, returns a custom Query class 316 | to use for this backend. Returns None if a custom Query isn't used. 317 | See also BaseDatabaseFeatures.uses_custom_query_class, which regulates 318 | whether this method is called at all. 319 | """ 320 | return query.query_class(DefaultQueryClass) 321 | 322 | def quote_name(self, name): 323 | """ 324 | Returns a quoted version of the given table, index or column name. Does 325 | not quote the given name if it's already been quoted. 326 | """ 327 | if name.startswith('"') and name.endswith('"'): 328 | return name # Quoting once is enough. 329 | return '"%s"' % name 330 | 331 | def regex_lookup(self, lookup_type): 332 | """ 333 | Returns the string to use in a query when performing regular expression 334 | lookups (using "regex" or "iregex"). The resulting string should 335 | contain a '%s' placeholder for the column being searched against. 336 | """ 337 | if lookup_type == 'iregex': 338 | raise NotImplementedError("SQL Anywhere does not support case insensitive regular expressions") 339 | return "%s REGEXP ('.*'||%s||'.*')" 340 | 341 | def random_function_sql(self): 342 | """ 343 | Returns a SQL expression that returns a random value. 344 | """ 345 | return 'RAND()' 346 | 347 | def savepoint_create_sql(self, sid): 348 | """ 349 | Returns the SQL for starting a new savepoint. Only required if the 350 | "uses_savepoints" feature is True. The "sid" parameter is a string 351 | for the savepoint id. 352 | """ 353 | return 'SAVEPOINT ' + self.quote_name(sid) 354 | 355 | def savepoint_commit_sql(self, sid): 356 | """ 357 | Returns the SQL for committing the given savepoint. 358 | """ 359 | return 'COMMIT' 360 | 361 | def savepoint_rollback_sql(self, sid): 362 | """ 363 | Returns the SQL for rolling back the given savepoint. 364 | """ 365 | return 'ROLLBACK TO SAVEPOINT ' + self.quote_name(sid) 366 | 367 | def sql_flush(self, style, tables, sequences): 368 | """ 369 | Returns a list of SQL statements required to remove all data from 370 | the given database tables (without actually removing the tables 371 | themselves). 372 | """ 373 | if tables: 374 | sql = ['SET TEMPORARY OPTION wait_for_commit = \'On\';'] 375 | # TODO: We should truncate tables here, but there may cause an error; 376 | # for now, delete (all) from each table 377 | for table in tables: 378 | sql.append('DELETE FROM %s;' % self.quote_name(table)) 379 | 380 | # TODO: This requires DBA authority, but once the truncate bug is fixed 381 | # it won't be necessary 382 | for sequence in sequences: 383 | sql.append('call sa_reset_identity(\'%s\', NULL, 0);' % sequence['table']) 384 | 385 | sql.append('SET TEMPORARY OPTION wait_for_commit = \'Off\';') 386 | sql.append('COMMIT;') 387 | 388 | return sql 389 | 390 | def value_to_db_datetime(self, value): 391 | if value is None: 392 | return None 393 | 394 | if djangoVersion[:2] <= (1, 3): 395 | # SQL Anywhere doesn't support tz-aware datetimes 396 | if value.tzinfo is not None: 397 | raise ValueError("SQL Anywhere backend does not support timezone-aware datetimes.") 398 | else: 399 | if is_aware(value): 400 | if settings.USE_TZ: 401 | value = value.astimezone(utc).replace(tzinfo=None) 402 | else: 403 | make_naive(value, get_default_timezone()) 404 | 405 | return str(value) 406 | 407 | def value_to_db_time(self, value): 408 | if value is None: 409 | return None 410 | 411 | if djangoVersion[:2] <= (1, 3): 412 | # SQL Anywhere doesn't support tz-aware datetimes 413 | if value.tzinfo is not None: 414 | raise ValueError("SQL Anywhere backend does not support timezone-aware datetimes.") 415 | else: 416 | if is_aware(value): 417 | make_naive(value, get_default_timezone()) 418 | 419 | return str(value) 420 | 421 | class DatabaseWrapper(BaseDatabaseWrapper): 422 | vendor = 'sqlanywhere' 423 | operators = { 424 | 'exact': '= %s', 425 | 'iexact': '= %s', 426 | 'contains': "LIKE %s ESCAPE '\\'", 427 | 'icontains': "LIKE %s ESCAPE '\\'", 428 | 'regex': "REGEXP ('.*'||%s||'.*')", 429 | # 'iregex': "REGEXP ('.*'||%s||'.*')", 430 | 'gt': '> %s', 431 | 'gte': '>= %s', 432 | 'lt': '< %s', 433 | 'lte': '<= %s', 434 | 'startswith': "LIKE %s ESCAPE '\\'", 435 | 'istartswith': "LIKE %s ESCAPE '\\'", 436 | 'endswith': "LIKE %s ESCAPE '\\'", 437 | 'iendswith': "LIKE %s ESCAPE '\\'" 438 | } 439 | if djangoVersion[:2] >= (1, 8): 440 | # Moved from DatabaseCreation in 1.8 441 | data_types = global_data_types 442 | 443 | Database = Database 444 | 445 | def __init__(self, *args, **kwargs): 446 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 447 | 448 | self.server_version = None 449 | if djangoVersion[:2] >= (1, 3): 450 | self.features = DatabaseFeatures(self) 451 | else: 452 | self.features = DatabaseFeatures() 453 | if djangoVersion[:2] >= (1, 4): 454 | self.ops = DatabaseOperations(self) 455 | else: 456 | self.ops = DatabaseOperations() 457 | self.client = DatabaseClient(self) 458 | self.creation = DatabaseCreation(self) 459 | self.introspection = DatabaseIntrospection(self) 460 | if djangoVersion[:2] >= (1, 2): 461 | self.validation = DatabaseValidation(self) 462 | else: 463 | self.validation = DatabaseValidation() 464 | 465 | def _valid_connection(self): 466 | if self.connection is not None: 467 | try: 468 | self.connection.con() 469 | return True 470 | except InterfaceError: 471 | self.connection.close() 472 | self.connection = None 473 | return False 474 | 475 | def check_constraints(self, table_names=None): 476 | self.cursor().execute('PREPARE TO COMMIT') 477 | 478 | def _cursor(self): 479 | return self.create_cursor() 480 | 481 | def _rollback(self): 482 | try: 483 | BaseDatabaseWrapper._rollback(self) 484 | except Database.NotSupportedError: 485 | pass 486 | 487 | # New methods for Django 1.6 488 | def get_connection_params(self): 489 | kwargs = {} 490 | links = {} 491 | 492 | settings_dict = self.settings_dict 493 | 494 | def setting( key ): 495 | if key in settings_dict: 496 | return settings_dict[key] 497 | dbkey = 'DATABASE_%s' % key 498 | if dbkey in settings_dict: 499 | return settings_dict[dbkey] 500 | return None 501 | # 502 | 503 | def empty( s ): 504 | return True if ( s is None or s == '' ) else False 505 | # 506 | 507 | uid = setting( 'USER' ) 508 | if not empty( uid ): 509 | kwargs['uid'] = uid 510 | dbn = setting( 'NAME' ) 511 | if not empty( dbn ): 512 | kwargs['dbn'] = dbn 513 | pwd = setting( 'PASSWORD' ) 514 | if not empty( pwd ): 515 | kwargs['pwd'] = pwd 516 | 517 | root = Database.Root('PYTHON') 518 | 519 | try: 520 | vers = root.api.sqlany_client_version() 521 | ret = True 522 | except: 523 | length = 1000 524 | buffer = ctypes.create_string_buffer(length) 525 | ret = root.api.sqlany_client_version(ctypes.byref(buffer), length) 526 | vers = buffer.value 527 | if ret: 528 | if sys.version_info[0] >= 3: 529 | # Python 3: convert bytes to str 530 | vers = str(vers, 'utf-8') 531 | vers = int(vers.split('.')[0]) 532 | else: 533 | vers = 11 # assume old 534 | host = setting( 'HOST' ) 535 | if host == '': 536 | host = 'localhost' # "Set to empty string for localhost" 537 | if not empty( host ) and vers > 11: 538 | kwargs['host'] = host 539 | port = setting( 'PORT' ) 540 | if not empty( port ): 541 | kwargs['host'] += ':%s' % port 542 | else: 543 | if not empty( host ): 544 | links['host'] = host 545 | port = setting( 'PORT' ) 546 | if not empty( port ): 547 | links['port'] = str( port ) 548 | if len(links) > 0: 549 | kwargs['links'] = 'tcpip(' + ','.join(k+'='+v for k, v in list(links.items())) + ')' 550 | kwargs.update(setting( 'OPTIONS' )) 551 | return kwargs 552 | 553 | def get_new_connection( self, conn_params ): 554 | conn = Database.connect(**conn_params) 555 | if conn is not None and djangoVersion[:2] >= (1, 6): 556 | # Autocommit is the default for 1.6+ 557 | curs = conn.cursor() 558 | curs.execute( "SET TEMPORARY OPTION chained='Off'" ) 559 | curs.close() 560 | return conn 561 | 562 | def init_connection_state( self ): 563 | if 'AUTOCOMMIT' in self.settings_dict and \ 564 | not self.settings_dict['AUTOCOMMIT']: 565 | self.set_autocommit( False ) 566 | 567 | def create_cursor( self ): 568 | cursor = None 569 | if not self._valid_connection(): 570 | kwargs = self.get_connection_params() 571 | self.connection = self.get_new_connection(kwargs) 572 | cursor = CursorWrapper(self.connection.cursor()) 573 | if djangoVersion[:2] < (1, 2): 574 | cursor.execute("SET TEMPORARY OPTION PUBLIC.reserved_keywords='LIMIT'") 575 | cursor.execute("SET TEMPORARY OPTION TIMESTAMP_FORMAT='YYYY-MM-DD HH:NN:SS.SSSSSS'") 576 | connection_created.send(sender=self.__class__, connection=self) 577 | if not cursor: 578 | cursor = CursorWrapper(self.connection.cursor()) 579 | 580 | return cursor 581 | 582 | def _set_autocommit( self, autocommit ): 583 | """ 584 | Backend-specific implementation to enable or disable autocommit. 585 | """ 586 | curs = self.create_cursor() 587 | curs.execute( "SET TEMPORARY OPTION chained='%s'" % 588 | ('Off' if autocommit else 'On') ) 589 | curs.close() 590 | 591 | def is_usable(self): 592 | """ 593 | Tests if the database connection is usable. 594 | This function may assume that self.connection is not None. 595 | """ 596 | return self._valid_connection() 597 | 598 | # New methods for Django 1.7 599 | if djangoVersion[:2] >= (1, 7): 600 | def schema_editor(self, *args, **kwargs): 601 | "Returns a new instance of this backend's SchemaEditor" 602 | return DatabaseSchemaEditor( self, *args, **kwargs ) 603 | 604 | # 605 | -------------------------------------------------------------------------------- /sqlany_django/client.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as djangoVersion 2 | 3 | if djangoVersion[:2] >= (1, 8): 4 | from django.db.backends.base.client import BaseDatabaseClient 5 | else: 6 | from django.db.backends import BaseDatabaseClient 7 | from django.conf import settings 8 | import os 9 | 10 | class DatabaseClient(BaseDatabaseClient): 11 | executable_name = 'dbisqlc' 12 | 13 | def runshell(self): 14 | conn_str = [] 15 | 16 | if settings.DATABASE_NAME: 17 | conn_str.append("dbn=%s" % settings.DATABASE_NAME) 18 | if settings.DATABASE_USER: 19 | conn_str.append("uid=%s" % settings.DATABASE_USER) 20 | if settings.DATABASE_PASSWORD: 21 | conn_str.append("pwd=%s" % settings.DATABASE_PASSWORD) 22 | if settings.DATABASE_HOST: 23 | tmp = "links=tcpip(host=%s" % settings.DATABASE_HOST 24 | if settings.DATABASE_PORT: 25 | tmp += ";port=%s" % settings.DATABASE_PORT 26 | tmp += ")" 27 | conn_str.append(tmp) 28 | for k,v in settings.DATABASE_OPTIONS: 29 | conn_str.append("%s=%s" % (k,v)) 30 | 31 | args = [self.executable_name] 32 | if len(conn_str): 33 | args.append( '-c' ) 34 | args.append( ';'.join(conn_str) ) 35 | 36 | os.execvp(self.executable_name,args) 37 | -------------------------------------------------------------------------------- /sqlany_django/compiler.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django import VERSION as djangoVersion 3 | from django.db.models.sql import compiler 4 | 5 | # Cache classes that have already been built 6 | _classes = {} 7 | select_re = re.compile('^SELECT[ ]+(DISTINCT\s)?') 8 | 9 | class SQLCompiler(compiler.SQLCompiler): 10 | def as_sql(self, with_limits=True, with_col_aliases=True, subquery=True): 11 | if djangoVersion[:2] >= (1, 8): 12 | query, params = super(SQLCompiler, self).as_sql(with_limits=False, 13 | with_col_aliases=with_col_aliases, 14 | subquery=subquery) 15 | else: 16 | query, params = super(SQLCompiler, self).as_sql(with_limits=False, 17 | with_col_aliases=with_col_aliases) 18 | m = select_re.match(query) 19 | if with_limits and m != None: 20 | num = None 21 | insert = None 22 | if self.query.high_mark is not None: 23 | num = self.query.high_mark - self.query.low_mark 24 | if num > 0: 25 | insert = 'TOP %d' % num 26 | if self.query.low_mark: 27 | if insert is None: 28 | insert = 'TOP ALL' 29 | insert = '%s START AT %d' % (insert, self.query.low_mark + 1) 30 | if insert is not None: 31 | if m.groups()[0] != None: 32 | query = select_re.sub('SELECT DISTINCT %s ' % insert, query) 33 | else: 34 | query = select_re.sub('SELECT %s ' % insert, query) 35 | return query, params 36 | 37 | class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): 38 | pass 39 | 40 | class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): 41 | pass 42 | 43 | class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): 44 | pass 45 | 46 | class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): 47 | pass 48 | 49 | if djangoVersion[:2] < (1, 8): 50 | # Removed in 1.8 51 | class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): 52 | pass 53 | -------------------------------------------------------------------------------- /sqlany_django/creation.py: -------------------------------------------------------------------------------- 1 | import sys, traceback, time, re 2 | from django.conf import settings 3 | 4 | from django import VERSION as djangoVersion 5 | 6 | if djangoVersion[:2] >= (1, 8): 7 | from django.db.backends.base.creation import BaseDatabaseCreation, TEST_DATABASE_PREFIX 8 | else: 9 | from django.db.backends.creation import BaseDatabaseCreation, TEST_DATABASE_PREFIX 10 | 11 | 12 | try: 13 | import sqlanydb as Database 14 | except ImportError as e: 15 | from django.core.exceptions import ImproperlyConfigured 16 | raise ImproperlyConfigured("Error loading sqlanydb module: %s" % e) 17 | 18 | global_data_types = { 19 | 'AutoField': 'integer DEFAULT AUTOINCREMENT', 20 | 'BooleanField': 'bit', 21 | 'NullBooleanField': 'bit null', 22 | 'CharField': 'varchar(%(max_length)s)', 23 | 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', 24 | 'DateField': 'date', 25 | 'DateTimeField': 'datetime', 26 | 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', 27 | 'FileField': 'varchar(%(max_length)s)', 28 | 'FilePathField': 'varchar(%(max_length)s)', 29 | 'FloatField': 'double precision', 30 | 'IntegerField': 'integer', 31 | 'BigIntegerField': 'bigint', 32 | 'IPAddressField': 'char(15)', 33 | 'GenericIPAddressField': 'char(39)', 34 | 'OneToOneField': 'integer', 35 | 'PositiveIntegerField': 'UNSIGNED integer', 36 | 'PositiveSmallIntegerField': 'UNSIGNED smallint', 37 | 'SlugField': 'varchar(%(max_length)s)', 38 | 'SmallIntegerField': 'smallint', 39 | 'TextField': 'text', 40 | 'TimeField': 'time', 41 | } 42 | 43 | class DatabaseCreation(BaseDatabaseCreation): 44 | # This dictionary maps Field objects to their associated SQL Anywhere column 45 | # types, as strings. Column-type strings can contain format strings; they'll 46 | # be interpolated against the values of Field.__dict__ before being output. 47 | # If a column type is set to None, it won't be included in the output. 48 | 49 | if djangoVersion[:2] < (1, 8): 50 | # Moved to DatabaseWrapper in 1.8 51 | data_types = global_data_types 52 | 53 | def sql_table_creation_suffix(self): 54 | suffix = [] 55 | if settings.TEST_DATABASE_COLLATION: 56 | suffix.append('COLLATION %s' % settings.TEST_DATABASE_COLLATION) 57 | if settings.TEST_DATABASE_CHARSET: 58 | suffix.append('ENCODING %s' % settings.TEST_DATABASE_CHARSET) 59 | return ' '.join(suffix) 60 | 61 | def sql_db_start_suffix(self): 62 | return 'AUTOSTOP OFF' 63 | 64 | if djangoVersion[:2] >= (1, 6): 65 | def sql_for_inline_foreign_key_references(self, model, field, known_models, style): 66 | """Don't use inline references for SQL Anywhere. This makes it 67 | easier to deal with conditionally creating UNIQUE constraints 68 | and UNIQUE indexes""" 69 | return [], True 70 | else: 71 | def sql_for_inline_foreign_key_references(self, field, known_models, style): 72 | """Don't use inline references for SQL Anywhere. This makes it 73 | easier to deal with conditionally creating UNIQUE constraints 74 | and UNIQUE indexes""" 75 | return [], True 76 | 77 | 78 | def sql_for_inline_many_to_many_references(self, model, field, style): 79 | from django.db import models 80 | opts = model._meta 81 | qn = self.connection.ops.quote_name 82 | 83 | table_output = [ 84 | ' %s %s %s,' % 85 | (style.SQL_FIELD(qn(field.m2m_column_name())), 86 | style.SQL_COLTYPE(models.ForeignKey(model).db_type()), 87 | style.SQL_KEYWORD('NOT NULL')), 88 | ' %s %s %s,' % 89 | (style.SQL_FIELD(qn(field.m2m_reverse_name())), 90 | style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type()), 91 | style.SQL_KEYWORD('NOT NULL')) 92 | ] 93 | deferred = [ 94 | (field.m2m_db_table(), field.m2m_column_name(), opts.db_table, 95 | opts.pk.column), 96 | (field.m2m_db_table(), field.m2m_reverse_name(), 97 | field.rel.to._meta.db_table, field.rel.to._meta.pk.column) 98 | ] 99 | return table_output, deferred 100 | 101 | def _connect_to_utility_db(self): 102 | # Note: We don't use our standard double-quotes to "quote name" 103 | # a database name when creating a new database 104 | kwargs = {} 105 | links = {} 106 | settings_dict = self.connection.settings_dict 107 | if settings_dict['USER']: 108 | kwargs['uid'] = settings_dict['USER'] 109 | kwargs['dbn'] = 'utility_db' 110 | if settings_dict['PASSWORD']: 111 | kwargs['pwd'] = settings_dict['PASSWORD'] 112 | if settings_dict['HOST']: 113 | links['host'] = settings_dict['HOST'] 114 | if settings_dict['PORT']: 115 | links['port'] = str(settings_dict['PORT']) 116 | kwargs.update(settings_dict['OPTIONS']) 117 | if len(links) > 0: 118 | kwargs['links'] = 'tcpip(' + ','.join(k+'='+v for k, v in list(links.items())) + ')' 119 | return Database.connect(**kwargs) 120 | 121 | def _create_test_db(self, verbosity, autoclobber): 122 | "Internal implementation - creates the test db tables." 123 | suffix = self.sql_table_creation_suffix() 124 | suffix_start = self.sql_db_start_suffix() 125 | 126 | test_database_name = self.connection.settings_dict['TEST_NAME'] 127 | 128 | connection = self._connect_to_utility_db() 129 | cursor = connection.cursor() 130 | try: 131 | cursor.execute("CREATE DATABASE '%s' %s COLLATION 'UCA'" % (test_database_name, suffix)) 132 | cursor.execute("START DATABASE '%s' %s" % (test_database_name, suffix_start)) 133 | except Exception as e: 134 | traceback.print_exc() 135 | sys.stderr.write("Got an error creating the test database: %s\n" % e) 136 | if not autoclobber: 137 | confirm = eval(input("Type 'yes' if you would like to try deleting the test database '%s', or 'no' to cancel: " % test_database_name)) 138 | if autoclobber or confirm == 'yes': 139 | try: 140 | if verbosity >= 1: 141 | print( "Destroying old test database..." ) 142 | cursor.execute("STOP DATABASE %s" % test_database_name) 143 | cursor.execute("DROP DATABASE '%s'" % test_database_name) 144 | if verbosity >= 1: 145 | print( "Creating test database..." ) 146 | cursor.execute("CREATE DATABASE '%s' %s COLLATION 'UCA'" % (test_database_name, suffix)) 147 | cursor.execute("START DATABASE '%s' %s" % (test_database_name, suffix)) 148 | except Exception as e: 149 | sys.stderr.write("Got an error recreating the test database: %s\n" % e) 150 | sys.exit(2) 151 | else: 152 | print( "Tests cancelled." ) 153 | sys.exit(1) 154 | finally: 155 | cursor.close() 156 | connection.close() 157 | 158 | return test_database_name 159 | 160 | def _destroy_test_db(self, test_database_name, verbosity): 161 | "Internal implementation - remove the test db tables." 162 | # Remove the test database to clean up after 163 | # ourselves. Connect to the previous database (not the test database) 164 | # to do so, because it's not allowed to delete a database while being 165 | # connected to it. 166 | connection = self._connect_to_utility_db() 167 | cursor = connection.cursor() 168 | try: 169 | # Note: We don't use our standard double-quotes to "quote name" 170 | # a database name when droping a database 171 | cursor.execute("STOP DATABASE %s" % test_database_name) 172 | cursor.execute("DROP DATABASE '%s'" % test_database_name) 173 | except Exception as e: 174 | traceback.print_exc() 175 | sys.stderr.write("Got an error dropping test database: %s\n" % e) 176 | finally: 177 | cursor.close() 178 | connection.close() 179 | 180 | def _unique_swap(self, query, fields, model, style, table=None): 181 | """ 182 | Fix unique constraints on multiple fields 183 | Build unique indexes instead of unique constraints 184 | 185 | Follows SQL generation from 186 | django.db.creation.BaseDatabaseCreation.sql_create_model 187 | """ 188 | opts = model._meta 189 | qn = self.connection.ops.quote_name 190 | 191 | if table == None: 192 | table = opts.db_table 193 | 194 | fields_str = ", ".join([style.SQL_FIELD(qn(f)) for f in fields]) 195 | multi_name = style.SQL_FIELD(qn("_".join(f for f in fields))) 196 | unique_str = 'UNIQUE (%s)' % fields_str 197 | unique_re_str = re.escape(unique_str) + '[,]?' 198 | query = re.sub(unique_re_str, '', query) 199 | 200 | idx_query = 'CREATE UNIQUE INDEX %s ON %s (%s);' % \ 201 | (multi_name, style.SQL_FIELD(qn(table)), fields_str) 202 | return [query, idx_query] 203 | 204 | def _unique_swap_many(self, queries, fields, model, style, table=None): 205 | for i, query in enumerate(queries): 206 | changes = self._unique_swap(query, fields, model, style, table=table) 207 | if changes[0] != query: 208 | queries[i] = changes[0] 209 | queries.append(changes[1]) 210 | 211 | return queries 212 | 213 | def sql_create_model(self, model, style, known_models=set()): 214 | """ 215 | Returns the SQL required to create a single model, as a tuple of: 216 | (list_of_sql, pending_references_dict) 217 | """ 218 | # Let BaseDatabaseCreation do most of the work 219 | opts = model._meta 220 | 221 | unique_nullable_fields = [] 222 | 223 | for f in opts.local_fields: 224 | if f.unique and f.null: 225 | unique_nullable_fields.append(f) 226 | f._unique = False 227 | 228 | outputs, pending = super(DatabaseCreation,self).sql_create_model(model,style,known_models) 229 | qn = self.connection.ops.quote_name 230 | 231 | for f in unique_nullable_fields: 232 | f._unique = True 233 | outputs.append("CREATE UNIQUE INDEX %s on %s(%s);" % ("%s_%s_UNIQUE" % (opts.db_table, f.column), qn(opts.db_table), qn(f.column))) 234 | 235 | for field_constraints in opts.unique_together: 236 | fields = [opts.get_field(f).column for f in field_constraints] 237 | outputs = self._unique_swap_many(outputs, fields, model, style) 238 | 239 | return outputs, pending 240 | 241 | def sql_for_many_to_many_field(self, model, f, style): 242 | "Return the CREATE TABLE + CREATE UNIQUE INDEX statements for a single m2m field" 243 | # Let BaseDatabaseCreation do most of the work 244 | outputs = super(DatabaseCreation, self).sql_for_many_to_many_field(model, f, style) 245 | 246 | from django.db import models 247 | from django.db.backends.util import truncate_name 248 | 249 | if f.creates_table: 250 | opts = model._meta 251 | qn = self.connection.ops.quote_name 252 | fields = [f.m2m_column_name(), f.m2m_reverse_name()] 253 | outputs = self._unique_swap_many(outputs, 254 | fields, 255 | model, 256 | style, 257 | table=f.m2m_db_table()) 258 | 259 | return outputs 260 | -------------------------------------------------------------------------------- /sqlany_django/introspection.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as djangoVersion 2 | if djangoVersion[:2] >= (1, 8): 3 | from django.db.backends.base.introspection import BaseDatabaseIntrospection, TableInfo 4 | else: 5 | from django.db.backends import BaseDatabaseIntrospection 6 | from sqlanydb import ProgrammingError, OperationalError 7 | import re 8 | import sqlanydb 9 | 10 | 11 | class DatabaseIntrospection(BaseDatabaseIntrospection): 12 | data_types_reverse = { sqlanydb.DT_DATE : 'DateField', 13 | sqlanydb.DT_TIME : 'DateTimeField', 14 | sqlanydb.DT_TIMESTAMP : 'DateTimeField', 15 | sqlanydb.DT_VARCHAR : 'CharField', 16 | sqlanydb.DT_FIXCHAR : 'CharField', 17 | sqlanydb.DT_LONGVARCHAR : 'CharField', 18 | sqlanydb.DT_STRING : 'CharField', 19 | sqlanydb.DT_DOUBLE : 'FloatField', 20 | sqlanydb.DT_FLOAT : 'FloatField', 21 | sqlanydb.DT_DECIMAL : 'IntegerField', 22 | sqlanydb.DT_INT : 'IntegerField', 23 | sqlanydb.DT_SMALLINT : 'IntegerField', 24 | sqlanydb.DT_BINARY : 'BlobField', 25 | sqlanydb.DT_LONGBINARY : 'BlobField', 26 | sqlanydb.DT_TINYINT : 'IntegerField', 27 | sqlanydb.DT_BIGINT : 'BigIntegerField', 28 | sqlanydb.DT_UNSINT : 'IntegerField', 29 | sqlanydb.DT_UNSSMALLINT : 'IntegerField', 30 | sqlanydb.DT_UNSBIGINT : 'BigIntegerField', 31 | sqlanydb.DT_BIT : 'IntegerField', 32 | sqlanydb.DT_LONGNVARCHAR : 'CharField' 33 | } 34 | 35 | def get_table_list(self, cursor): 36 | "Returns a list of table names in the current database." 37 | cursor.execute( "SELECT table_name,table_type FROM sys.SYSTAB WHERE " 38 | "creator = USER_ID() and table_type in (1,2,3,4,21)" ) 39 | if djangoVersion[:2] < (1, 8): 40 | return [row[0] for row in cursor.fetchall()] 41 | return [TableInfo( row[0], 'v' if row[1] in ['2','21'] else 't' ) 42 | for row in cursor.fetchall()] 43 | 44 | def get_table_description(self, cursor, table_name): 45 | "Returns a description of the table, with the DB-API cursor.description interface." 46 | cursor.execute("SELECT FIRST * FROM %s" % 47 | self.connection.ops.quote_name(table_name)) 48 | return tuple((c[0], t, None, c[3], c[4], c[5], int(c[6]) == 1) for c, t in cursor.columns()) 49 | 50 | def _name_to_index(self, cursor, table_name): 51 | """ 52 | Returns a dictionary of {field_name: field_index} for the given table. 53 | Indexes are 0-based. 54 | """ 55 | return dict([(d[0], i) for i, d in enumerate(self.get_table_description(cursor, table_name))]) 56 | 57 | def get_relations(self, cursor, table_name): 58 | """ 59 | Returns a dictionary of {field_index: (field_index_other_table, other_table)} 60 | representing all relationships to the given table. Indexes are 0-based. 61 | """ 62 | my_field_dict = self._name_to_index(cursor, table_name) 63 | constraints = [] 64 | relations = {} 65 | cursor.execute(""" 66 | SELECT (fidx.column_id - 1), t2.table_name, (pidx.column_id - 1) FROM SYSTAB t1 67 | INNER JOIN SYSFKEY f ON f.foreign_table_id = t1.table_id 68 | INNER JOIN SYSTAB t2 ON t2.table_id = f.primary_table_id 69 | INNER JOIN SYSIDXCOL fidx ON fidx.table_id = f.foreign_table_id AND fidx.index_id = f.foreign_index_id 70 | INNER JOIN SYSIDXCOL pidx ON pidx.table_id = f.primary_table_id AND pidx.index_id = f.primary_index_id 71 | WHERE t1.table_name = %s""", [table_name]) 72 | constraints.extend(cursor.fetchall()) 73 | 74 | for my_field_index, other_table, other_field_index in constraints: 75 | relations[my_field_index] = (other_field_index, other_table) 76 | 77 | return relations 78 | 79 | def get_indexes(self, cursor, table_name): 80 | """ 81 | Returns a dictionary of fieldname -> infodict for the given table, 82 | where each infodict is in the format: 83 | {'primary_key': boolean representing whether it's the primary key, 84 | 'unique': boolean representing whether it's a unique index} 85 | """ 86 | # We need to skip multi-column indexes. 87 | cursor.execute(""" 88 | select max(c.column_name), 89 | max(ix.index_category), 90 | max(ix."unique") 91 | from SYSIDX ix, SYSTABLE t, SYSIDXCOL ixc, SYSCOLUMN c 92 | where ix.table_id = t.table_id 93 | and ixc.table_id = t.table_id 94 | and ixc.index_id = ix.index_id 95 | and ixc.table_id = c.table_id 96 | and ixc.column_id = c.column_id 97 | and t.table_name = %s 98 | group by ix.index_id 99 | having count(*) = 1 100 | order by ix.index_id 101 | """, [table_name]) 102 | 103 | indexes = {} 104 | for col_name, cat, unique in cursor.fetchall(): 105 | indexes[col_name] = { 106 | 'primary_key': (cat == 1), 107 | 'unique': (unique == 1 or unique == 2) } 108 | 109 | return indexes 110 | -------------------------------------------------------------------------------- /sqlany_django/schema.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as djangoVersion 2 | 3 | if djangoVersion[:2] >= (1, 8): 4 | from django.db.backends.base.schema import BaseDatabaseSchemaEditor 5 | else: 6 | from django.db.backends.schema import BaseDatabaseSchemaEditor 7 | 8 | class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 9 | 10 | # Overrideable SQL templates 11 | sql_rename_table = "ALTER TABLE %(old_table)s RENAME %(new_table)s" 12 | sql_retablespace_table = None 13 | sql_create_column = "ALTER TABLE %(table)s ADD %(column)s %(definition)s" 14 | sql_alter_column_type = "ALTER %(column)s %(type)s" 15 | sql_alter_column_null = "ALTER %(column)s NULL" 16 | sql_alter_column_not_null = "ALTER %(column)s NOT NULL" 17 | sql_alter_column_default = "ALTER %(column)s DEFAULT %(default)s" 18 | sql_alter_column_no_default = "ALTER %(column)s DROP DEFAULT" 19 | sql_delete_column = "ALTER TABLE %(table)s DROP %(column)s" 20 | sql_rename_column = "ALTER TABLE %(table)s RENAME %(old_column)s TO %(new_column)s" 21 | sql_update_with_default = "UPDATE %(table)s SET %(column)s = %(default)s WHERE %(column)s IS NULL" 22 | 23 | sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s)" 24 | sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" 25 | 26 | def alter_db_tablespace(self, model, old_db_tablespace, new_db_tablespace): 27 | """ 28 | Moves a model's table between tablespaces 29 | - not applicable to SQL Anywhere 30 | """ 31 | pass 32 | # 33 | -------------------------------------------------------------------------------- /sqlany_django/validation.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as djangoVersion 2 | 3 | if djangoVersion[:2] >= (1, 8): 4 | from django.db.backends.base.validation import BaseDatabaseValidation 5 | else: 6 | from django.db.backends import BaseDatabaseValidation 7 | 8 | class DatabaseValidation(BaseDatabaseValidation): 9 | def validate_field(self, errors, opts, f): 10 | from django.db import models 11 | from django.db import connection 12 | varchar_fields = (models.CharField, models.CommaSeparatedIntegerField, 13 | models.SlugField) 14 | # TODO: We should check for UTF8 15 | # For varchar maximum, specs say single-byte:32767, double-byte:16383, utf8:8191 16 | if isinstance(f, varchar_fields) and f.max_length > 8191: 17 | msg = '"%(name)s": %(cls)s cannot have a "max_length" greater than 8191' 18 | if msg: 19 | errors.add(opts, msg % {'name': f.name, 'cls': f.__class__.__name__}) 20 | 21 | --------------------------------------------------------------------------------