mirror of
https://github.com/hanzoai/pymssql.git
synced 2026-10-30 14:54:29 +00:00
688 lines
20 KiB
Cython
688 lines
20 KiB
Cython
"""DB-SIG compliant module for communicating with MS SQL servers"""
|
|
# pymssql.pyx
|
|
#
|
|
# Copyright (C) 2003 Joon-cheol Park <[email protected]>
|
|
# 2008 Andrzej Kukula <[email protected]>
|
|
# 2009 Damien Churchill <[email protected]>
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
# MA 02110-1301 USA
|
|
|
|
import datetime
|
|
import time
|
|
|
|
import _mssql
|
|
cimport _mssql
|
|
from cpython cimport bool, PY_MAJOR_VERSION
|
|
|
|
cdef extern from "pymssql_version.h":
|
|
const char *PYMSSQL_VERSION
|
|
|
|
__author__ = 'Damien Churchill <[email protected]>'
|
|
__full_version__ = PYMSSQL_VERSION.decode('ascii')
|
|
__version__ = '.'.join(__full_version__.split('.')[:3]) # drop '.dev' from 'X.Y.Z.dev'
|
|
|
|
# Strives for compliance with DB-API 2.0 (PEP 249)
|
|
# http://www.python.org/dev/peps/pep-0249/
|
|
apilevel = '2.0'
|
|
|
|
# module may be shared, but not connections
|
|
threadsafety = 1
|
|
|
|
# this module uses extended python format codes
|
|
paramstyle = 'pyformat'
|
|
|
|
from _mssql import set_wait_callback
|
|
|
|
# store a tuple of programming error codes
|
|
cdef object prog_errors = (
|
|
102, # syntax error
|
|
207, # invalid column name
|
|
208, # invalid object name
|
|
2812, # unknown procedure
|
|
4104 # multi-part identifier could not be bound
|
|
)
|
|
|
|
# store a tuple of integrity error codes
|
|
cdef object integrity_errors = (
|
|
515, # NULL insert
|
|
547, # FK related
|
|
2601, # violate unique index
|
|
2627, # violate UNIQUE KEY constraint
|
|
)
|
|
|
|
class DBAPIType:
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def __cmp__(self, other):
|
|
if other == self.value:
|
|
return 0
|
|
if other < self.value:
|
|
return 1
|
|
else:
|
|
return -1
|
|
|
|
def __eq__(self, other):
|
|
return (other == self.value)
|
|
|
|
def __repr__(self):
|
|
return '<DBAPIType %s>' % self.value
|
|
|
|
STRING = DBAPIType(_mssql.STRING)
|
|
BINARY = DBAPIType(_mssql.BINARY)
|
|
NUMBER = DBAPIType(_mssql.NUMBER)
|
|
DATETIME = DBAPIType(_mssql.DATETIME)
|
|
DECIMAL = DBAPIType(_mssql.DECIMAL)
|
|
|
|
Date = datetime.date
|
|
Time = datetime.time
|
|
Timestamp = datetime.datetime
|
|
DateFromTicks = lambda ticks: Date(*time.localtime(ticks)[:3])
|
|
TimeFromTicks = lambda ticks: Time(*time.localtime(ticks)[3:6])
|
|
TimestampFromTicks = lambda ticks: Timestamp(*time.localtime(ticks)[:6])
|
|
Binary = bytes
|
|
|
|
cdef dict DBTYPES = {
|
|
'bool': _mssql.SQLBITN,
|
|
'str': _mssql.SQLVARCHAR,
|
|
'unicode': _mssql.SQLVARCHAR,
|
|
'Decimal': _mssql.SQLDECIMAL,
|
|
'datetime': _mssql.SQLDATETIME,
|
|
'date': _mssql.SQLDATETIME,
|
|
#Dump type for work vith None
|
|
'NoneType': _mssql.SQLVARCHAR,
|
|
}
|
|
|
|
cdef int py2db_type(py_type, value):
|
|
if PY_MAJOR_VERSION == 3:
|
|
if py_type == 'int':
|
|
if value is not None and value >= -2147483648 and value <= 2147483647: # -2^31 - 2^31-1
|
|
return _mssql.SQLINTN
|
|
else:
|
|
return _mssql.SQLINT8
|
|
else:
|
|
if py_type == 'int':
|
|
return _mssql.SQLINTN
|
|
if py_type == 'long':
|
|
return _mssql.SQLINT8
|
|
return DBTYPES[py_type]
|
|
|
|
try:
|
|
StandardError
|
|
except NameError:
|
|
StandardError = Exception
|
|
|
|
# exception hierarchy
|
|
class Warning(StandardError):
|
|
pass
|
|
|
|
class Error(StandardError):
|
|
pass
|
|
|
|
class InterfaceError(Error):
|
|
pass
|
|
|
|
class DatabaseError(Error):
|
|
pass
|
|
|
|
class DataError(Error):
|
|
pass
|
|
|
|
class OperationalError(DatabaseError):
|
|
pass
|
|
|
|
class IntegrityError(DatabaseError):
|
|
pass
|
|
|
|
class InternalError(DatabaseError):
|
|
pass
|
|
|
|
class ProgrammingError(DatabaseError):
|
|
pass
|
|
|
|
class NotSupportedError(DatabaseError):
|
|
pass
|
|
|
|
class ColumnsWithoutNamesError(InterfaceError):
|
|
def __init__(self, columns_without_names):
|
|
self.columns_without_names = columns_without_names
|
|
|
|
def __str__(self):
|
|
return 'Specified as_dict=True and ' \
|
|
'there are columns with no names: %r' \
|
|
% (self.columns_without_names,)
|
|
|
|
def row2dict(row):
|
|
"""Filter dict so it only has string keys; used when as_dict == True"""
|
|
return dict([(k, v) for k, v in row.items() if hasattr(k, 'startswith')])
|
|
|
|
# stored procedure output parameter
|
|
cdef class output:
|
|
|
|
cdef object _type
|
|
cdef object _value
|
|
|
|
property type:
|
|
"""
|
|
This is the type of the parameter.
|
|
"""
|
|
def __get__(self):
|
|
return self._type
|
|
|
|
property value:
|
|
"""
|
|
This is the value of the parameter.
|
|
"""
|
|
def __get__(self):
|
|
return self._value
|
|
|
|
|
|
def __init__(self, param_type, value=None):
|
|
self._type = param_type
|
|
self._value = value
|
|
|
|
######################
|
|
## Connection class ##
|
|
######################
|
|
cdef class Connection:
|
|
"""
|
|
This class represents an MS-SQL database connection.
|
|
"""
|
|
|
|
cdef bool _as_dict
|
|
cdef bool _autocommit
|
|
cdef _mssql.MSSQLConnection conn
|
|
|
|
property as_dict:
|
|
"""
|
|
Instructs all cursors this connection creates to return results
|
|
as a dictionary rather than a tuple.
|
|
"""
|
|
def __get__(self):
|
|
return self._as_dict
|
|
def __set__(self, value):
|
|
self._as_dict = value
|
|
|
|
property autocommit_state:
|
|
"""
|
|
The current state of autocommit on the connection.
|
|
"""
|
|
def __get__(self):
|
|
return self._autocommit
|
|
|
|
property _conn:
|
|
"""
|
|
INTERNAL PROPERTY. Returns the _mssql.MSSQLConnection object, and
|
|
raise exception if it's set to None. It's easier than adding the
|
|
necessary checks to every other method.
|
|
"""
|
|
def __get__(self):
|
|
if self.conn == None:
|
|
raise InterfaceError('Connection is closed.')
|
|
return self.conn
|
|
|
|
def __init__(self, conn, as_dict):
|
|
self.conn = conn
|
|
self._autocommit = False
|
|
self.as_dict = as_dict
|
|
try:
|
|
self._conn.execute_non_query('BEGIN TRAN')
|
|
except Exception, e:
|
|
raise OperationalError('Cannot start transaction: ' + str(e.args[0]))
|
|
|
|
def __dealloc__(self):
|
|
if self.conn:
|
|
self.close()
|
|
|
|
def autocommit(self, status):
|
|
"""
|
|
Turn autocommit ON or OFF.
|
|
"""
|
|
|
|
if status == self._autocommit:
|
|
return
|
|
|
|
tran_type = 'ROLLBACK' if status else 'BEGIN'
|
|
self._conn.execute_non_query('%s TRAN' % tran_type)
|
|
self._autocommit = status
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
self.close()
|
|
|
|
def close(self):
|
|
"""
|
|
Close the connection to the database. Implicitly rolls back all
|
|
uncommitted transactions.
|
|
"""
|
|
if self.conn:
|
|
self.conn.close()
|
|
self.conn = None
|
|
|
|
def commit(self):
|
|
"""
|
|
Commit transaction which is currently in progress.
|
|
"""
|
|
|
|
if self._autocommit == True:
|
|
return
|
|
|
|
try:
|
|
self._conn.execute_non_query('COMMIT TRAN')
|
|
self._conn.execute_non_query('BEGIN TRAN')
|
|
except Exception, e:
|
|
raise OperationalError('Cannot commit transaction: ' + str(e.args[0]))
|
|
|
|
def cursor(self, as_dict=None):
|
|
"""
|
|
Return cursor object that can be used to make queries and fetch
|
|
results from the database.
|
|
"""
|
|
if as_dict is None:
|
|
as_dict = self.as_dict
|
|
return Cursor(self, as_dict)
|
|
|
|
def rollback(self):
|
|
"""
|
|
Roll back transaction which is currently in progress.
|
|
"""
|
|
if self._autocommit == True:
|
|
return
|
|
|
|
try:
|
|
self._conn.execute_non_query('ROLLBACK TRAN')
|
|
except _mssql.MSSQLException, e:
|
|
# PEP 249 indicates that we have contract with the user that we will
|
|
# always have a transaction in place if autocommit is False.
|
|
# Therefore, it seems logical to ignore this exception since it
|
|
# indicates a situation we shouldn't ever encounter anyway. However,
|
|
# it can happen when an error is severe enough to cause a
|
|
# "batch-abort". In that case, SQL Server *implicitly* rolls back
|
|
# the transaction for us (how helpful!). But there doesn't seem
|
|
# to be any way for us to know if an error is severe enough to cause
|
|
# a batch abort:
|
|
# http://stackoverflow.com/questions/5877162/why-does-microsoft-sql-server-implicitly-rollback-when-a-create-statement-fails
|
|
#
|
|
# the alternative is to do 'select @@trancount' before each rollback
|
|
# but that is slower and doesn't seem to offer any benefit.
|
|
if 'The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION' not in str(e):
|
|
raise
|
|
try:
|
|
self._conn.execute_non_query('BEGIN TRAN')
|
|
except Exception, e:
|
|
raise OperationalError('Cannot begin transaction: ' + str(e.args[0]))
|
|
|
|
##################
|
|
## Cursor class ##
|
|
##################
|
|
cdef class Cursor:
|
|
"""
|
|
This class represents a database cursor, which is used to issue queries
|
|
and fetch results from a database connection.
|
|
"""
|
|
|
|
cdef Connection conn
|
|
cdef public tuple description
|
|
cdef int batchsize
|
|
cdef int _batchsize
|
|
cdef int _rownumber
|
|
cdef bool as_dict
|
|
cdef object _returnvalue
|
|
|
|
property connection:
|
|
def __get__(self):
|
|
return self.conn
|
|
|
|
property lastrowid:
|
|
def __get__(self):
|
|
return self.conn.conn.identity
|
|
|
|
property rowcount:
|
|
def __get__(self):
|
|
return self._rownumber
|
|
|
|
property returnvalue:
|
|
def __get__(self):
|
|
return self._returnvalue
|
|
|
|
property rownumber:
|
|
def __get__(self):
|
|
return self._rownumber
|
|
|
|
property _source:
|
|
def __get__(self):
|
|
if self.conn == None:
|
|
raise InterfaceError('Cursor is closed.')
|
|
return self.conn
|
|
|
|
def __init__(self, conn, as_dict):
|
|
self.conn = conn
|
|
self.description = None
|
|
self._batchsize = 1
|
|
self._rownumber = 0
|
|
self._returnvalue = None
|
|
self.as_dict = as_dict
|
|
|
|
def __iter__(self):
|
|
"""
|
|
Return self to make cursors compatibile with Python iteration
|
|
protocol.
|
|
"""
|
|
return self
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
self.close()
|
|
|
|
def callproc(self, str procname, parameters=()):
|
|
"""
|
|
Call a stored procedure with the given name.
|
|
|
|
:param procname: The name of the procedure to call
|
|
:type procname: str
|
|
:keyword parameters: The optional parameters for the procedure
|
|
:type parameters: sequence
|
|
"""
|
|
self._returnvalue = None
|
|
proc = self._source._conn.init_procedure(procname)
|
|
for parameter in parameters:
|
|
if type(parameter) is output:
|
|
param_type = parameter.type
|
|
param_value = parameter.value
|
|
param_output = True
|
|
else:
|
|
param_type = type(parameter)
|
|
param_value = parameter
|
|
param_output = False
|
|
|
|
try:
|
|
type_name = param_type.__name__
|
|
db_type = py2db_type(type_name, param_value)
|
|
except (AttributeError, KeyError):
|
|
raise NotSupportedError('Unable to determine database type from python %s type' % type_name)
|
|
|
|
proc.bind(param_value, db_type, output=param_output)
|
|
try:
|
|
self._returnvalue = proc.execute()
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise DatabaseError, e.args[0]
|
|
return tuple([proc.parameters[p] for p in proc.parameters])
|
|
|
|
def close(self):
|
|
"""
|
|
Closes the cursor. The cursor is unusable from this point.
|
|
"""
|
|
self.conn = None
|
|
self.description = None
|
|
|
|
def execute(self, operation, params=()):
|
|
self.description = None
|
|
self._rownumber = 0
|
|
|
|
try:
|
|
if not params:
|
|
self._source._conn.execute_query(operation)
|
|
else:
|
|
self._source._conn.execute_query(operation, params)
|
|
self.description = self._source._conn.get_header()
|
|
self._rownumber = self._source._conn.rows_affected
|
|
|
|
if self.as_dict and self.description:
|
|
columns_without_names = [
|
|
idx
|
|
for idx, column_descriptor in enumerate(self.description)
|
|
if len(column_descriptor[0]) == 0
|
|
]
|
|
if columns_without_names:
|
|
raise ColumnsWithoutNamesError(columns_without_names)
|
|
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
if e.number in prog_errors:
|
|
raise ProgrammingError, e.args[0]
|
|
if e.number in integrity_errors:
|
|
raise IntegrityError, e.args[0]
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
def executemany(self, operation, params_seq):
|
|
self.description = None
|
|
rownumber = 0
|
|
for params in params_seq:
|
|
self.execute(operation, params)
|
|
# support correct rowcount across multiple executes
|
|
rownumber += self._rownumber
|
|
self._rownumber = rownumber
|
|
|
|
def nextset(self):
|
|
try:
|
|
if not self._source._conn.nextresult():
|
|
return None
|
|
self._rownumber = 0
|
|
self.description = self._source._conn.get_header()
|
|
return 1
|
|
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
cdef getrow(self):
|
|
"""
|
|
Helper method used by fetchone and fetchmany to fetch and handle
|
|
converting the row if as_dict = True.
|
|
"""
|
|
row_format = _mssql.ROW_FORMAT_DICT if self.as_dict else _mssql.ROW_FORMAT_TUPLE
|
|
row = next(self._source._conn.get_iterator(row_format))
|
|
if not self.as_dict:
|
|
return row
|
|
return row2dict(row)
|
|
|
|
def fetchone(self):
|
|
if self.description is None:
|
|
raise OperationalError('Statement not executed or executed statement has no resultset')
|
|
|
|
try:
|
|
return self.getrow()
|
|
|
|
except StopIteration:
|
|
return None
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
def fetchmany(self, size=None):
|
|
if self.description is None:
|
|
raise OperationalError('Statement not executed or executed statement has no resultset')
|
|
|
|
if size == None:
|
|
size = self._batchsize
|
|
self.batchsize = size
|
|
|
|
try:
|
|
rows = []
|
|
for i in xrange(size):
|
|
try:
|
|
rows.append(self.getrow())
|
|
except StopIteration:
|
|
break
|
|
return rows
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
def fetchall(self):
|
|
if self.description is None:
|
|
raise OperationalError('Statement not executed or executed statement has no resultset')
|
|
|
|
try:
|
|
rows = []
|
|
while True:
|
|
try:
|
|
rows.append(self.getrow())
|
|
except StopIteration:
|
|
break
|
|
self._rownumber = self._source._conn.rows_affected
|
|
return rows
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
def __next__(self):
|
|
try:
|
|
row = self.getrow()
|
|
self._rownumber += 1
|
|
return row
|
|
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError, e.args[0]
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError, e.args[0]
|
|
|
|
def setinputsizes(self, sizes=None):
|
|
"""
|
|
This method does nothing, as permitted by DB-API specification.
|
|
"""
|
|
pass
|
|
|
|
def setoutputsize(self, size=None, column=0):
|
|
"""
|
|
This method does nothing, as permitted by DB-API specification.
|
|
"""
|
|
pass
|
|
|
|
def connect(server='.', user='', password='', database='', timeout=0,
|
|
login_timeout=60, charset='UTF-8', as_dict=False,
|
|
host='', appname=None, port='1433', conn_properties=None):
|
|
"""
|
|
Constructor for creating a connection to the database. Returns a
|
|
Connection object.
|
|
|
|
:param server: database host
|
|
:type server: string
|
|
:param user: database user to connect as
|
|
:type user: string
|
|
:param password: user's password
|
|
:type password: string
|
|
:param database: the database to initially connect to
|
|
:type database: string
|
|
:param timeout: query timeout in seconds, default 0 (no timeout)
|
|
:type timeout: int
|
|
:param login_timeout: timeout for connection and login in seconds, default 60
|
|
:type login_timeout: int
|
|
:param charset: character set with which to connect to the database
|
|
:type charset: string
|
|
:keyword as_dict: whether rows should be returned as dictionaries instead of tuples.
|
|
:type as_dict: boolean
|
|
:keyword appname: Set the application name to use for the connection
|
|
:type appname: string
|
|
:keyword port: the TCP port to use to connect to the server
|
|
:type port: string
|
|
:keyword conn_properties: SQL queries to send to the server upon connection
|
|
establishment. Can be a string or another kind
|
|
of iterable of strings
|
|
"""
|
|
|
|
_mssql.login_timeout = login_timeout
|
|
|
|
# set the login timeout
|
|
try:
|
|
login_timeout = int(login_timeout)
|
|
except ValueError:
|
|
login_timeout = 0
|
|
|
|
# default query timeout
|
|
try:
|
|
timeout = int(timeout)
|
|
except ValueError:
|
|
timeout = 0
|
|
|
|
if host:
|
|
server = host
|
|
|
|
try:
|
|
conn = _mssql.connect(server=server, user=user, password=password,
|
|
charset=charset, database=database,
|
|
appname=appname, port=port,
|
|
conn_properties=conn_properties)
|
|
|
|
except _mssql.MSSQLDatabaseException, e:
|
|
raise OperationalError(e.args[0])
|
|
|
|
except _mssql.MSSQLDriverException, e:
|
|
raise InterfaceError(e.args[0])
|
|
|
|
|
|
if timeout != 0:
|
|
conn.query_timeout = timeout
|
|
|
|
return Connection(conn, as_dict)
|
|
|
|
def get_max_connections():
|
|
"""
|
|
Get the maximum number of simulatenous connections pymssql will open
|
|
to the server.
|
|
"""
|
|
return _mssql.get_max_connections()
|
|
|
|
def set_max_connections(int limit):
|
|
"""
|
|
Set maximum simultaneous connections db-lib will open to the server.
|
|
|
|
:param limit: the connection limit
|
|
:type limit: int
|
|
"""
|
|
_mssql.set_max_connections(limit)
|
|
|
|
cdef extern from "sybdb.h":
|
|
char *dbversion()
|
|
|
|
# Only recent versions of FreeTDS have the ct_config function
|
|
# so this can break builds
|
|
# Maybe later we can enable this or make it conditional
|
|
DEF HAS_CT_CONFIG = False
|
|
IF HAS_CT_CONFIG:
|
|
cdef extern from "ctpublic.h":
|
|
ctypedef int CS_INT
|
|
ctypedef void CS_VOID
|
|
struct _CS_CONTEXT
|
|
ctypedef _CS_CONTEXT CS_CONTEXT
|
|
ctypedef CS_INT CS_RETCODE
|
|
|
|
int CS_GET, CS_VERSION
|
|
|
|
CS_RETCODE ct_config(CS_CONTEXT * ctx, CS_INT action, CS_INT property,
|
|
CS_VOID * buffer, CS_INT buflen, CS_INT * outlen)
|
|
|
|
def get_freetds_version():
|
|
cdef CS_CONTEXT *ctx = NULL
|
|
cdef char buf[256]
|
|
cdef int outlen
|
|
|
|
ret = ct_config(ctx, CS_GET, CS_VERSION, buf, 256, &outlen)
|
|
return buf
|
|
|
|
def get_dbversion():
|
|
return dbversion()
|