2006-05-02 09:31:56 +08:00
|
|
|
import datetime
|
2010-05-04 22:00:30 +08:00
|
|
|
import decimal
|
2017-09-22 00:13:09 +08:00
|
|
|
import functools
|
2011-03-28 10:11:19 +08:00
|
|
|
import hashlib
|
2012-09-21 03:03:24 +08:00
|
|
|
import logging
|
2019-05-09 00:34:22 +08:00
|
|
|
import time
|
2019-04-26 00:07:28 +08:00
|
|
|
from contextlib import contextmanager
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2019-08-20 15:54:41 +08:00
|
|
|
from django.db import NotSupportedError
|
2008-08-02 13:56:57 +08:00
|
|
|
|
2012-09-21 03:03:24 +08:00
|
|
|
logger = logging.getLogger('django.db.backends')
|
2010-10-04 23:12:39 +08:00
|
|
|
|
2011-02-12 21:03:34 +08:00
|
|
|
|
2017-01-19 15:39:46 +08:00
|
|
|
class CursorWrapper:
|
2011-02-14 08:06:52 +08:00
|
|
|
def __init__(self, cursor, db):
|
2006-05-02 09:31:56 +08:00
|
|
|
self.cursor = cursor
|
2011-02-14 08:06:52 +08:00
|
|
|
self.db = db
|
2011-02-12 21:03:34 +08:00
|
|
|
|
2013-09-23 04:14:17 +08:00
|
|
|
WRAP_ERROR_ATTRS = frozenset(['fetchone', 'fetchmany', 'fetchall', 'nextset'])
|
2013-09-15 15:12:16 +08:00
|
|
|
|
2012-01-13 05:55:19 +08:00
|
|
|
def __getattr__(self, attr):
|
Refactored database exceptions wrapping.
Squashed commit of the following:
commit 2181d833ed1a2e422494738dcef311164c4e097e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Wed Feb 27 14:28:39 2013 +0100
Fixed #15901 -- Wrapped all PEP-249 exceptions.
commit 5476a5d93c19aa2f928c497d39ce6e33f52694e2
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 17:26:52 2013 +0100
Added PEP 3134 exception chaining.
Thanks Jacob Kaplan-Moss for the suggestion.
commit 9365fad0a650328002fb424457d675a273c95802
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 17:13:49 2013 +0100
Improved API for wrapping database errors.
Thanks Alex Gaynor for the proposal.
commit 1b463b765f2826f73a8d9266795cd5da4f8d5e9e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 15:00:39 2013 +0100
Removed redundant exception wrapping.
This is now taken care of by the cursor wrapper.
commit 524bc7345a724bf526bdd2dd1bcf5ede67d6bb5c
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 14:55:10 2013 +0100
Wrapped database exceptions in the base backend.
This covers the most common PEP-249 APIs:
- Connection APIs: close(), commit(), rollback(), cursor()
- Cursor APIs: callproc(), close(), execute(), executemany(),
fetchone(), fetchmany(), fetchall(), nextset().
Fixed #19920.
commit a66746bb5f0839f35543222787fce3b6a0d0a3ea
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 14:53:34 2013 +0100
Added a wrap_database_exception context manager and decorator.
It re-throws backend-specific exceptions using Django's common wrappers.
2013-02-26 21:53:34 +08:00
|
|
|
cursor_attr = getattr(self.cursor, attr)
|
2013-09-15 15:12:16 +08:00
|
|
|
if attr in CursorWrapper.WRAP_ERROR_ATTRS:
|
|
|
|
return self.db.wrap_database_errors(cursor_attr)
|
Refactored database exceptions wrapping.
Squashed commit of the following:
commit 2181d833ed1a2e422494738dcef311164c4e097e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Wed Feb 27 14:28:39 2013 +0100
Fixed #15901 -- Wrapped all PEP-249 exceptions.
commit 5476a5d93c19aa2f928c497d39ce6e33f52694e2
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 17:26:52 2013 +0100
Added PEP 3134 exception chaining.
Thanks Jacob Kaplan-Moss for the suggestion.
commit 9365fad0a650328002fb424457d675a273c95802
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 17:13:49 2013 +0100
Improved API for wrapping database errors.
Thanks Alex Gaynor for the proposal.
commit 1b463b765f2826f73a8d9266795cd5da4f8d5e9e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 15:00:39 2013 +0100
Removed redundant exception wrapping.
This is now taken care of by the cursor wrapper.
commit 524bc7345a724bf526bdd2dd1bcf5ede67d6bb5c
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 14:55:10 2013 +0100
Wrapped database exceptions in the base backend.
This covers the most common PEP-249 APIs:
- Connection APIs: close(), commit(), rollback(), cursor()
- Cursor APIs: callproc(), close(), execute(), executemany(),
fetchone(), fetchmany(), fetchall(), nextset().
Fixed #19920.
commit a66746bb5f0839f35543222787fce3b6a0d0a3ea
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date: Tue Feb 26 14:53:34 2013 +0100
Added a wrap_database_exception context manager and decorator.
It re-throws backend-specific exceptions using Django's common wrappers.
2013-02-26 21:53:34 +08:00
|
|
|
else:
|
|
|
|
return cursor_attr
|
2011-02-12 21:03:34 +08:00
|
|
|
|
|
|
|
def __iter__(self):
|
2014-10-03 09:58:36 +08:00
|
|
|
with self.db.wrap_database_errors:
|
2017-02-24 09:06:01 +08:00
|
|
|
yield from self.cursor
|
2011-02-12 21:03:34 +08:00
|
|
|
|
2013-09-24 08:17:59 +08:00
|
|
|
def __enter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
2017-01-19 18:00:41 +08:00
|
|
|
# Close instead of passing through to avoid backend-specific behavior
|
|
|
|
# (#17671). Catch errors liberally because errors in cleanup code
|
|
|
|
# aren't useful.
|
2014-04-24 14:41:37 +08:00
|
|
|
try:
|
|
|
|
self.close()
|
|
|
|
except self.db.Database.Error:
|
|
|
|
pass
|
2013-09-24 08:17:59 +08:00
|
|
|
|
2013-09-23 04:14:17 +08:00
|
|
|
# The following methods cannot be implemented in __getattr__, because the
|
|
|
|
# code must run when the method is invoked, not just when it is accessed.
|
2011-02-12 21:03:34 +08:00
|
|
|
|
2017-08-13 03:06:49 +08:00
|
|
|
def callproc(self, procname, params=None, kparams=None):
|
|
|
|
# Keyword parameters for callproc aren't supported in PEP 249, but the
|
|
|
|
# database driver may support them (e.g. cx_Oracle).
|
|
|
|
if kparams is not None and not self.db.features.supports_callproc_kwargs:
|
|
|
|
raise NotSupportedError(
|
|
|
|
'Keyword parameters for callproc are not supported on this '
|
|
|
|
'database backend.'
|
|
|
|
)
|
2013-09-23 04:14:17 +08:00
|
|
|
self.db.validate_no_broken_transaction()
|
|
|
|
with self.db.wrap_database_errors:
|
2017-08-13 03:06:49 +08:00
|
|
|
if params is None and kparams is None:
|
2013-09-23 04:14:17 +08:00
|
|
|
return self.cursor.callproc(procname)
|
2017-08-13 03:06:49 +08:00
|
|
|
elif kparams is None:
|
2013-09-23 04:14:17 +08:00
|
|
|
return self.cursor.callproc(procname, params)
|
2017-08-13 03:06:49 +08:00
|
|
|
else:
|
|
|
|
params = params or ()
|
|
|
|
return self.cursor.callproc(procname, params, kparams)
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2013-03-23 23:09:56 +08:00
|
|
|
def execute(self, sql, params=None):
|
2017-09-22 00:13:09 +08:00
|
|
|
return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
|
|
|
|
|
|
|
|
def executemany(self, sql, param_list):
|
|
|
|
return self._execute_with_wrappers(sql, param_list, many=True, executor=self._executemany)
|
|
|
|
|
|
|
|
def _execute_with_wrappers(self, sql, params, many, executor):
|
|
|
|
context = {'connection': self.db, 'cursor': self}
|
|
|
|
for wrapper in reversed(self.db.execute_wrappers):
|
|
|
|
executor = functools.partial(wrapper, executor)
|
|
|
|
return executor(sql, params, many, context)
|
|
|
|
|
|
|
|
def _execute(self, sql, params, *ignored_wrapper_args):
|
2013-09-23 04:14:17 +08:00
|
|
|
self.db.validate_no_broken_transaction()
|
|
|
|
with self.db.wrap_database_errors:
|
|
|
|
if params is None:
|
2019-05-05 13:49:43 +08:00
|
|
|
# params default might be backend specific.
|
2013-09-23 04:14:17 +08:00
|
|
|
return self.cursor.execute(sql)
|
|
|
|
else:
|
|
|
|
return self.cursor.execute(sql, params)
|
|
|
|
|
2017-09-22 00:13:09 +08:00
|
|
|
def _executemany(self, sql, param_list, *ignored_wrapper_args):
|
2013-09-23 04:14:17 +08:00
|
|
|
self.db.validate_no_broken_transaction()
|
|
|
|
with self.db.wrap_database_errors:
|
|
|
|
return self.cursor.executemany(sql, param_list)
|
|
|
|
|
|
|
|
|
|
|
|
class CursorDebugWrapper(CursorWrapper):
|
|
|
|
|
|
|
|
# XXX callproc isn't instrumented at this time.
|
|
|
|
|
|
|
|
def execute(self, sql, params=None):
|
2019-04-26 00:07:28 +08:00
|
|
|
with self.debug_sql(sql, params, use_last_executed_query=True):
|
2017-01-21 21:13:44 +08:00
|
|
|
return super().execute(sql, params)
|
2006-05-02 09:31:56 +08:00
|
|
|
|
|
|
|
def executemany(self, sql, param_list):
|
2019-04-26 00:07:28 +08:00
|
|
|
with self.debug_sql(sql, param_list, many=True):
|
|
|
|
return super().executemany(sql, param_list)
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def debug_sql(self, sql=None, params=None, use_last_executed_query=False, many=False):
|
2019-05-09 00:34:22 +08:00
|
|
|
start = time.monotonic()
|
2006-05-02 09:31:56 +08:00
|
|
|
try:
|
2019-04-26 00:07:28 +08:00
|
|
|
yield
|
2006-05-02 09:31:56 +08:00
|
|
|
finally:
|
2019-05-09 00:34:22 +08:00
|
|
|
stop = time.monotonic()
|
2010-10-04 23:12:39 +08:00
|
|
|
duration = stop - start
|
2019-04-26 00:07:28 +08:00
|
|
|
if use_last_executed_query:
|
|
|
|
sql = self.db.ops.last_executed_query(self.cursor, sql, params)
|
2012-01-23 00:41:20 +08:00
|
|
|
try:
|
2019-04-26 00:07:28 +08:00
|
|
|
times = len(params) if many else ''
|
|
|
|
except TypeError:
|
|
|
|
# params could be an iterator.
|
2012-01-23 00:41:20 +08:00
|
|
|
times = '?'
|
2014-06-07 20:09:27 +08:00
|
|
|
self.db.queries_log.append({
|
2019-04-26 00:07:28 +08:00
|
|
|
'sql': '%s times: %s' % (times, sql) if many else sql,
|
|
|
|
'time': '%.3f' % duration,
|
2006-05-02 09:31:56 +08:00
|
|
|
})
|
2016-03-29 06:33:29 +08:00
|
|
|
logger.debug(
|
2019-04-26 00:07:28 +08:00
|
|
|
'(%.3f) %s; args=%s',
|
|
|
|
duration,
|
|
|
|
sql,
|
|
|
|
params,
|
|
|
|
extra={'duration': duration, 'sql': sql, 'params': params},
|
2010-10-04 23:12:39 +08:00
|
|
|
)
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2011-02-12 21:03:34 +08:00
|
|
|
|
2006-05-02 09:31:56 +08:00
|
|
|
###############################################
|
|
|
|
# Converters from database (string) to Python #
|
|
|
|
###############################################
|
|
|
|
|
|
|
|
def typecast_date(s):
|
2017-01-25 07:04:12 +08:00
|
|
|
return datetime.date(*map(int, s.split('-'))) if s else None # return None if s is null
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2013-07-08 08:39:54 +08:00
|
|
|
|
2013-11-03 05:02:56 +08:00
|
|
|
def typecast_time(s): # does NOT store time zone information
|
2013-07-08 08:39:54 +08:00
|
|
|
if not s:
|
|
|
|
return None
|
2006-05-02 09:31:56 +08:00
|
|
|
hour, minutes, seconds = s.split(':')
|
2013-11-03 05:02:56 +08:00
|
|
|
if '.' in seconds: # check whether seconds have a fractional part
|
2006-05-02 09:31:56 +08:00
|
|
|
seconds, microseconds = seconds.split('.')
|
|
|
|
else:
|
|
|
|
microseconds = '0'
|
2016-04-14 00:31:31 +08:00
|
|
|
return datetime.time(int(hour), int(minutes), int(seconds), int((microseconds + '000000')[:6]))
|
2013-07-08 08:39:54 +08:00
|
|
|
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2013-11-03 05:02:56 +08:00
|
|
|
def typecast_timestamp(s): # does NOT store time zone information
|
2006-05-02 09:31:56 +08:00
|
|
|
# "2005-07-29 15:48:00.590358-05"
|
|
|
|
# "2005-07-29 09:56:00-05"
|
2013-07-08 08:39:54 +08:00
|
|
|
if not s:
|
|
|
|
return None
|
2014-03-31 03:11:05 +08:00
|
|
|
if ' ' not in s:
|
2013-07-08 08:39:54 +08:00
|
|
|
return typecast_date(s)
|
2006-05-02 09:31:56 +08:00
|
|
|
d, t = s.split()
|
2018-11-09 21:27:07 +08:00
|
|
|
# Remove timezone information.
|
2006-05-02 09:31:56 +08:00
|
|
|
if '-' in t:
|
2018-11-09 21:27:07 +08:00
|
|
|
t, _ = t.split('-', 1)
|
2006-05-02 09:31:56 +08:00
|
|
|
elif '+' in t:
|
2018-11-09 21:27:07 +08:00
|
|
|
t, _ = t.split('+', 1)
|
2006-05-02 09:31:56 +08:00
|
|
|
dates = d.split('-')
|
|
|
|
times = t.split(':')
|
|
|
|
seconds = times[2]
|
2013-11-03 05:02:56 +08:00
|
|
|
if '.' in seconds: # check whether seconds have a fractional part
|
2006-05-02 09:31:56 +08:00
|
|
|
seconds, microseconds = seconds.split('.')
|
|
|
|
else:
|
|
|
|
microseconds = '0'
|
2016-03-29 06:33:29 +08:00
|
|
|
return datetime.datetime(
|
|
|
|
int(dates[0]), int(dates[1]), int(dates[2]),
|
2011-11-18 21:01:06 +08:00
|
|
|
int(times[0]), int(times[1]), int(seconds),
|
2019-12-11 20:59:49 +08:00
|
|
|
int((microseconds + '000000')[:6])
|
2016-03-29 06:33:29 +08:00
|
|
|
)
|
2006-05-02 09:31:56 +08:00
|
|
|
|
2013-07-08 08:39:54 +08:00
|
|
|
|
2006-05-02 09:31:56 +08:00
|
|
|
###############################################
|
|
|
|
# Converters from Python to database (string) #
|
|
|
|
###############################################
|
|
|
|
|
2017-11-12 08:17:20 +08:00
|
|
|
def split_identifier(identifier):
|
2007-06-23 22:16:00 +08:00
|
|
|
"""
|
2020-05-06 12:35:26 +08:00
|
|
|
Split an SQL identifier into a two element tuple of (namespace, name).
|
2017-11-12 08:17:20 +08:00
|
|
|
|
|
|
|
The identifier could be a table, column, or sequence name might be prefixed
|
|
|
|
by a namespace.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
namespace, name = identifier.split('"."')
|
|
|
|
except ValueError:
|
|
|
|
namespace, name = '', identifier
|
|
|
|
return namespace.strip('"'), name.strip('"')
|
|
|
|
|
|
|
|
|
|
|
|
def truncate_name(identifier, length=None, hash_len=4):
|
|
|
|
"""
|
2020-05-06 12:35:26 +08:00
|
|
|
Shorten an SQL identifier to a repeatable mangled version with the given
|
2017-11-12 08:17:20 +08:00
|
|
|
length.
|
|
|
|
|
|
|
|
If a quote stripped name contains a namespace, e.g. USERNAME"."TABLE,
|
2017-02-15 19:22:34 +08:00
|
|
|
truncate the table portion only.
|
|
|
|
"""
|
2017-11-12 08:17:20 +08:00
|
|
|
namespace, name = split_identifier(identifier)
|
2017-02-15 19:22:34 +08:00
|
|
|
|
2017-11-12 08:17:20 +08:00
|
|
|
if length is None or len(name) <= length:
|
|
|
|
return identifier
|
2007-06-23 22:16:00 +08:00
|
|
|
|
2018-10-02 22:41:43 +08:00
|
|
|
digest = names_digest(name, length=hash_len)
|
2017-11-12 08:17:20 +08:00
|
|
|
return '%s%s%s' % ('%s"."' % namespace if namespace else '', name[:length - hash_len], digest)
|
2013-07-08 08:39:54 +08:00
|
|
|
|
2008-07-29 13:09:29 +08:00
|
|
|
|
2018-10-02 22:41:43 +08:00
|
|
|
def names_digest(*args, length):
|
|
|
|
"""
|
|
|
|
Generate a 32-bit digest of a set of arguments that can be used to shorten
|
|
|
|
identifying names.
|
|
|
|
"""
|
|
|
|
h = hashlib.md5()
|
|
|
|
for arg in args:
|
|
|
|
h.update(arg.encode())
|
|
|
|
return h.hexdigest()[:length]
|
|
|
|
|
|
|
|
|
2008-07-29 13:09:29 +08:00
|
|
|
def format_number(value, max_digits, decimal_places):
|
|
|
|
"""
|
2017-01-25 07:04:12 +08:00
|
|
|
Format a number into a string with the requisite number of digits and
|
2008-07-29 13:09:29 +08:00
|
|
|
decimal places.
|
|
|
|
"""
|
2014-12-01 14:11:23 +08:00
|
|
|
if value is None:
|
|
|
|
return None
|
2018-07-03 04:10:35 +08:00
|
|
|
context = decimal.getcontext().copy()
|
|
|
|
if max_digits is not None:
|
|
|
|
context.prec = max_digits
|
2014-12-01 14:11:23 +08:00
|
|
|
if decimal_places is not None:
|
2018-07-03 04:10:35 +08:00
|
|
|
value = value.quantize(decimal.Decimal(1).scaleb(-decimal_places), context=context)
|
|
|
|
else:
|
|
|
|
context.traps[decimal.Rounded] = 1
|
|
|
|
value = context.create_decimal(value)
|
2014-12-01 14:11:23 +08:00
|
|
|
return "{:f}".format(value)
|
2016-12-31 06:11:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
def strip_quotes(table_name):
|
|
|
|
"""
|
|
|
|
Strip quotes off of quoted table names to make them safe for use in index
|
|
|
|
names, sequence names, etc. For example '"USER"."TABLE"' (an Oracle naming
|
|
|
|
scheme) becomes 'USER"."TABLE'.
|
|
|
|
"""
|
|
|
|
has_quotes = table_name.startswith('"') and table_name.endswith('"')
|
|
|
|
return table_name[1:-1] if has_quotes else table_name
|