1 | # sybase.py |
---|
2 | # Copyright (C) 2007 Fisch Asset Management AG http://www.fam.ch |
---|
3 | # Coding: Alexander Houben alexander.houben@thor-solutions.ch |
---|
4 | # |
---|
5 | # This module is part of SQLAlchemy and is released under |
---|
6 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
7 | |
---|
8 | """ |
---|
9 | Sybase database backend. |
---|
10 | |
---|
11 | Known issues / TODO: |
---|
12 | |
---|
13 | * Uses the mx.ODBC driver from egenix (version 2.1.0) |
---|
14 | * The current version of sqlalchemy.databases.sybase only supports |
---|
15 | mx.ODBC.Windows (other platforms such as mx.ODBC.unixODBC still need |
---|
16 | some development) |
---|
17 | * Support for pyodbc has been built in but is not yet complete (needs |
---|
18 | further development) |
---|
19 | * Results of running tests/alltests.py: |
---|
20 | Ran 934 tests in 287.032s |
---|
21 | FAILED (failures=3, errors=1) |
---|
22 | * Tested on 'Adaptive Server Anywhere 9' (version 9.0.1.1751) |
---|
23 | """ |
---|
24 | |
---|
25 | import datetime, operator |
---|
26 | |
---|
27 | from sqlalchemy import util, sql, schema, exc |
---|
28 | from sqlalchemy.sql import compiler, expression |
---|
29 | from sqlalchemy.engine import default, base |
---|
30 | from sqlalchemy import types as sqltypes |
---|
31 | from sqlalchemy.sql import operators as sql_operators |
---|
32 | from sqlalchemy import MetaData, Table, Column |
---|
33 | from sqlalchemy import String, Integer, SMALLINT, CHAR, ForeignKey |
---|
34 | |
---|
35 | |
---|
36 | __all__ = [ |
---|
37 | 'SybaseTypeError' |
---|
38 | 'SybaseNumeric', 'SybaseFloat', 'SybaseInteger', 'SybaseBigInteger', |
---|
39 | 'SybaseTinyInteger', 'SybaseSmallInteger', |
---|
40 | 'SybaseDateTime_mxodbc', 'SybaseDateTime_pyodbc', |
---|
41 | 'SybaseDate_mxodbc', 'SybaseDate_pyodbc', |
---|
42 | 'SybaseTime_mxodbc', 'SybaseTime_pyodbc', |
---|
43 | 'SybaseText', 'SybaseString', 'SybaseChar', 'SybaseBinary', |
---|
44 | 'SybaseBoolean', 'SybaseTimeStamp', 'SybaseMoney', 'SybaseSmallMoney', |
---|
45 | 'SybaseUniqueIdentifier', |
---|
46 | ] |
---|
47 | |
---|
48 | |
---|
49 | RESERVED_WORDS = set([ |
---|
50 | "add", "all", "alter", "and", |
---|
51 | "any", "as", "asc", "backup", |
---|
52 | "begin", "between", "bigint", "binary", |
---|
53 | "bit", "bottom", "break", "by", |
---|
54 | "call", "capability", "cascade", "case", |
---|
55 | "cast", "char", "char_convert", "character", |
---|
56 | "check", "checkpoint", "close", "comment", |
---|
57 | "commit", "connect", "constraint", "contains", |
---|
58 | "continue", "convert", "create", "cross", |
---|
59 | "cube", "current", "current_timestamp", "current_user", |
---|
60 | "cursor", "date", "dbspace", "deallocate", |
---|
61 | "dec", "decimal", "declare", "default", |
---|
62 | "delete", "deleting", "desc", "distinct", |
---|
63 | "do", "double", "drop", "dynamic", |
---|
64 | "else", "elseif", "encrypted", "end", |
---|
65 | "endif", "escape", "except", "exception", |
---|
66 | "exec", "execute", "existing", "exists", |
---|
67 | "externlogin", "fetch", "first", "float", |
---|
68 | "for", "force", "foreign", "forward", |
---|
69 | "from", "full", "goto", "grant", |
---|
70 | "group", "having", "holdlock", "identified", |
---|
71 | "if", "in", "index", "index_lparen", |
---|
72 | "inner", "inout", "insensitive", "insert", |
---|
73 | "inserting", "install", "instead", "int", |
---|
74 | "integer", "integrated", "intersect", "into", |
---|
75 | "iq", "is", "isolation", "join", |
---|
76 | "key", "lateral", "left", "like", |
---|
77 | "lock", "login", "long", "match", |
---|
78 | "membership", "message", "mode", "modify", |
---|
79 | "natural", "new", "no", "noholdlock", |
---|
80 | "not", "notify", "null", "numeric", |
---|
81 | "of", "off", "on", "open", |
---|
82 | "option", "options", "or", "order", |
---|
83 | "others", "out", "outer", "over", |
---|
84 | "passthrough", "precision", "prepare", "primary", |
---|
85 | "print", "privileges", "proc", "procedure", |
---|
86 | "publication", "raiserror", "readtext", "real", |
---|
87 | "reference", "references", "release", "remote", |
---|
88 | "remove", "rename", "reorganize", "resource", |
---|
89 | "restore", "restrict", "return", "revoke", |
---|
90 | "right", "rollback", "rollup", "save", |
---|
91 | "savepoint", "scroll", "select", "sensitive", |
---|
92 | "session", "set", "setuser", "share", |
---|
93 | "smallint", "some", "sqlcode", "sqlstate", |
---|
94 | "start", "stop", "subtrans", "subtransaction", |
---|
95 | "synchronize", "syntax_error", "table", "temporary", |
---|
96 | "then", "time", "timestamp", "tinyint", |
---|
97 | "to", "top", "tran", "trigger", |
---|
98 | "truncate", "tsequal", "unbounded", "union", |
---|
99 | "unique", "unknown", "unsigned", "update", |
---|
100 | "updating", "user", "using", "validate", |
---|
101 | "values", "varbinary", "varchar", "variable", |
---|
102 | "varying", "view", "wait", "waitfor", |
---|
103 | "when", "where", "while", "window", |
---|
104 | "with", "with_cube", "with_lparen", "with_rollup", |
---|
105 | "within", "work", "writetext", |
---|
106 | ]) |
---|
107 | |
---|
108 | ischema = MetaData() |
---|
109 | |
---|
110 | tables = Table("SYSTABLE", ischema, |
---|
111 | Column("table_id", Integer, primary_key=True), |
---|
112 | Column("file_id", SMALLINT), |
---|
113 | Column("table_name", CHAR(128)), |
---|
114 | Column("table_type", CHAR(10)), |
---|
115 | Column("creator", Integer), |
---|
116 | #schema="information_schema" |
---|
117 | ) |
---|
118 | |
---|
119 | domains = Table("SYSDOMAIN", ischema, |
---|
120 | Column("domain_id", Integer, primary_key=True), |
---|
121 | Column("domain_name", CHAR(128)), |
---|
122 | Column("type_id", SMALLINT), |
---|
123 | Column("precision", SMALLINT, quote=True), |
---|
124 | #schema="information_schema" |
---|
125 | ) |
---|
126 | |
---|
127 | columns = Table("SYSCOLUMN", ischema, |
---|
128 | Column("column_id", Integer, primary_key=True), |
---|
129 | Column("table_id", Integer, ForeignKey(tables.c.table_id)), |
---|
130 | Column("pkey", CHAR(1)), |
---|
131 | Column("column_name", CHAR(128)), |
---|
132 | Column("nulls", CHAR(1)), |
---|
133 | Column("width", SMALLINT), |
---|
134 | Column("domain_id", SMALLINT, ForeignKey(domains.c.domain_id)), |
---|
135 | # FIXME: should be mx.BIGINT |
---|
136 | Column("max_identity", Integer), |
---|
137 | # FIXME: should be mx.ODBC.Windows.LONGVARCHAR |
---|
138 | Column("default", String), |
---|
139 | Column("scale", Integer), |
---|
140 | #schema="information_schema" |
---|
141 | ) |
---|
142 | |
---|
143 | foreignkeys = Table("SYSFOREIGNKEY", ischema, |
---|
144 | Column("foreign_table_id", Integer, ForeignKey(tables.c.table_id), primary_key=True), |
---|
145 | Column("foreign_key_id", SMALLINT, primary_key=True), |
---|
146 | Column("primary_table_id", Integer, ForeignKey(tables.c.table_id)), |
---|
147 | #schema="information_schema" |
---|
148 | ) |
---|
149 | fkcols = Table("SYSFKCOL", ischema, |
---|
150 | Column("foreign_table_id", Integer, ForeignKey(columns.c.table_id), primary_key=True), |
---|
151 | Column("foreign_key_id", SMALLINT, ForeignKey(foreignkeys.c.foreign_key_id), primary_key=True), |
---|
152 | Column("foreign_column_id", Integer, ForeignKey(columns.c.column_id), primary_key=True), |
---|
153 | Column("primary_column_id", Integer), |
---|
154 | #schema="information_schema" |
---|
155 | ) |
---|
156 | |
---|
157 | class SybaseTypeError(sqltypes.TypeEngine): |
---|
158 | def result_processor(self, dialect): |
---|
159 | return None |
---|
160 | |
---|
161 | def bind_processor(self, dialect): |
---|
162 | def process(value): |
---|
163 | raise exc.InvalidRequestError("Data type not supported", [value]) |
---|
164 | return process |
---|
165 | |
---|
166 | def get_col_spec(self): |
---|
167 | raise exc.CompileError("Data type not supported") |
---|
168 | |
---|
169 | class SybaseNumeric(sqltypes.Numeric): |
---|
170 | def get_col_spec(self): |
---|
171 | if self.scale is None: |
---|
172 | if self.precision is None: |
---|
173 | return "NUMERIC" |
---|
174 | else: |
---|
175 | return "NUMERIC(%(precision)s)" % {'precision' : self.precision} |
---|
176 | else: |
---|
177 | return "NUMERIC(%(precision)s, %(scale)s)" % {'precision': self.precision, 'scale' : self.scale} |
---|
178 | |
---|
179 | class SybaseFloat(sqltypes.FLOAT, SybaseNumeric): |
---|
180 | def __init__(self, precision = 10, asdecimal = False, scale = 2, **kwargs): |
---|
181 | super(sqltypes.FLOAT, self).__init__(precision, asdecimal, **kwargs) |
---|
182 | self.scale = scale |
---|
183 | |
---|
184 | def get_col_spec(self): |
---|
185 | # if asdecimal is True, handle same way as SybaseNumeric |
---|
186 | if self.asdecimal: |
---|
187 | return SybaseNumeric.get_col_spec(self) |
---|
188 | if self.precision is None: |
---|
189 | return "FLOAT" |
---|
190 | else: |
---|
191 | return "FLOAT(%(precision)s)" % {'precision': self.precision} |
---|
192 | |
---|
193 | def result_processor(self, dialect): |
---|
194 | def process(value): |
---|
195 | if value is None: |
---|
196 | return None |
---|
197 | return float(value) |
---|
198 | if self.asdecimal: |
---|
199 | return SybaseNumeric.result_processor(self, dialect) |
---|
200 | return process |
---|
201 | |
---|
202 | class SybaseInteger(sqltypes.Integer): |
---|
203 | def get_col_spec(self): |
---|
204 | return "INTEGER" |
---|
205 | |
---|
206 | class SybaseBigInteger(SybaseInteger): |
---|
207 | def get_col_spec(self): |
---|
208 | return "BIGINT" |
---|
209 | |
---|
210 | class SybaseTinyInteger(SybaseInteger): |
---|
211 | def get_col_spec(self): |
---|
212 | return "TINYINT" |
---|
213 | |
---|
214 | class SybaseSmallInteger(SybaseInteger): |
---|
215 | def get_col_spec(self): |
---|
216 | return "SMALLINT" |
---|
217 | |
---|
218 | class SybaseDateTime_mxodbc(sqltypes.DateTime): |
---|
219 | def __init__(self, *a, **kw): |
---|
220 | super(SybaseDateTime_mxodbc, self).__init__(False) |
---|
221 | |
---|
222 | def get_col_spec(self): |
---|
223 | return "DATETIME" |
---|
224 | |
---|
225 | class SybaseDateTime_pyodbc(sqltypes.DateTime): |
---|
226 | def __init__(self, *a, **kw): |
---|
227 | super(SybaseDateTime_pyodbc, self).__init__(False) |
---|
228 | |
---|
229 | def get_col_spec(self): |
---|
230 | return "DATETIME" |
---|
231 | |
---|
232 | def result_processor(self, dialect): |
---|
233 | def process(value): |
---|
234 | if value is None: |
---|
235 | return None |
---|
236 | # Convert the datetime.datetime back to datetime.time |
---|
237 | return value |
---|
238 | return process |
---|
239 | |
---|
240 | def bind_processor(self, dialect): |
---|
241 | def process(value): |
---|
242 | if value is None: |
---|
243 | return None |
---|
244 | return value |
---|
245 | return process |
---|
246 | |
---|
247 | class SybaseDate_mxodbc(sqltypes.Date): |
---|
248 | def __init__(self, *a, **kw): |
---|
249 | super(SybaseDate_mxodbc, self).__init__(False) |
---|
250 | |
---|
251 | def get_col_spec(self): |
---|
252 | return "DATE" |
---|
253 | |
---|
254 | class SybaseDate_pyodbc(sqltypes.Date): |
---|
255 | def __init__(self, *a, **kw): |
---|
256 | super(SybaseDate_pyodbc, self).__init__(False) |
---|
257 | |
---|
258 | def get_col_spec(self): |
---|
259 | return "DATE" |
---|
260 | |
---|
261 | class SybaseTime_mxodbc(sqltypes.Time): |
---|
262 | def __init__(self, *a, **kw): |
---|
263 | super(SybaseTime_mxodbc, self).__init__(False) |
---|
264 | |
---|
265 | def get_col_spec(self): |
---|
266 | return "DATETIME" |
---|
267 | |
---|
268 | def result_processor(self, dialect): |
---|
269 | def process(value): |
---|
270 | if value is None: |
---|
271 | return None |
---|
272 | # Convert the datetime.datetime back to datetime.time |
---|
273 | return datetime.time(value.hour, value.minute, value.second, value.microsecond) |
---|
274 | return process |
---|
275 | |
---|
276 | class SybaseTime_pyodbc(sqltypes.Time): |
---|
277 | def __init__(self, *a, **kw): |
---|
278 | super(SybaseTime_pyodbc, self).__init__(False) |
---|
279 | |
---|
280 | def get_col_spec(self): |
---|
281 | return "DATETIME" |
---|
282 | |
---|
283 | def result_processor(self, dialect): |
---|
284 | def process(value): |
---|
285 | if value is None: |
---|
286 | return None |
---|
287 | # Convert the datetime.datetime back to datetime.time |
---|
288 | return datetime.time(value.hour, value.minute, value.second, value.microsecond) |
---|
289 | return process |
---|
290 | |
---|
291 | def bind_processor(self, dialect): |
---|
292 | def process(value): |
---|
293 | if value is None: |
---|
294 | return None |
---|
295 | return datetime.datetime(1970, 1, 1, value.hour, value.minute, value.second, value.microsecond) |
---|
296 | return process |
---|
297 | |
---|
298 | class SybaseText(sqltypes.Text): |
---|
299 | def get_col_spec(self): |
---|
300 | return "TEXT" |
---|
301 | |
---|
302 | class SybaseString(sqltypes.String): |
---|
303 | def get_col_spec(self): |
---|
304 | return "VARCHAR(%(length)s)" % {'length' : self.length} |
---|
305 | |
---|
306 | class SybaseChar(sqltypes.CHAR): |
---|
307 | def get_col_spec(self): |
---|
308 | return "CHAR(%(length)s)" % {'length' : self.length} |
---|
309 | |
---|
310 | class SybaseBinary(sqltypes.Binary): |
---|
311 | def get_col_spec(self): |
---|
312 | return "IMAGE" |
---|
313 | |
---|
314 | class SybaseBoolean(sqltypes.Boolean): |
---|
315 | def get_col_spec(self): |
---|
316 | return "BIT" |
---|
317 | |
---|
318 | def result_processor(self, dialect): |
---|
319 | def process(value): |
---|
320 | if value is None: |
---|
321 | return None |
---|
322 | return value and True or False |
---|
323 | return process |
---|
324 | |
---|
325 | def bind_processor(self, dialect): |
---|
326 | def process(value): |
---|
327 | if value is True: |
---|
328 | return 1 |
---|
329 | elif value is False: |
---|
330 | return 0 |
---|
331 | elif value is None: |
---|
332 | return None |
---|
333 | else: |
---|
334 | return value and True or False |
---|
335 | return process |
---|
336 | |
---|
337 | class SybaseTimeStamp(sqltypes.TIMESTAMP): |
---|
338 | def get_col_spec(self): |
---|
339 | return "TIMESTAMP" |
---|
340 | |
---|
341 | class SybaseMoney(sqltypes.TypeEngine): |
---|
342 | def get_col_spec(self): |
---|
343 | return "MONEY" |
---|
344 | |
---|
345 | class SybaseSmallMoney(SybaseMoney): |
---|
346 | def get_col_spec(self): |
---|
347 | return "SMALLMONEY" |
---|
348 | |
---|
349 | class SybaseUniqueIdentifier(sqltypes.TypeEngine): |
---|
350 | def get_col_spec(self): |
---|
351 | return "UNIQUEIDENTIFIER" |
---|
352 | |
---|
353 | class SybaseSQLExecutionContext(default.DefaultExecutionContext): |
---|
354 | pass |
---|
355 | |
---|
356 | class SybaseSQLExecutionContext_mxodbc(SybaseSQLExecutionContext): |
---|
357 | |
---|
358 | def __init__(self, dialect, connection, compiled=None, statement=None, parameters=None): |
---|
359 | super(SybaseSQLExecutionContext_mxodbc, self).__init__(dialect, connection, compiled, statement, parameters) |
---|
360 | |
---|
361 | def pre_exec(self): |
---|
362 | super(SybaseSQLExecutionContext_mxodbc, self).pre_exec() |
---|
363 | |
---|
364 | def post_exec(self): |
---|
365 | if self.compiled.isinsert: |
---|
366 | table = self.compiled.statement.table |
---|
367 | # get the inserted values of the primary key |
---|
368 | |
---|
369 | # get any sequence IDs first (using @@identity) |
---|
370 | self.cursor.execute("SELECT @@identity AS lastrowid") |
---|
371 | row = self.cursor.fetchone() |
---|
372 | lastrowid = int(row[0]) |
---|
373 | if lastrowid > 0: |
---|
374 | # an IDENTITY was inserted, fetch it |
---|
375 | # FIXME: always insert in front ? This only works if the IDENTITY is the first column, no ?! |
---|
376 | if not hasattr(self, '_last_inserted_ids') or self._last_inserted_ids is None: |
---|
377 | self._last_inserted_ids = [lastrowid] |
---|
378 | else: |
---|
379 | self._last_inserted_ids = [lastrowid] + self._last_inserted_ids[1:] |
---|
380 | super(SybaseSQLExecutionContext_mxodbc, self).post_exec() |
---|
381 | |
---|
382 | class SybaseSQLExecutionContext_pyodbc(SybaseSQLExecutionContext): |
---|
383 | def __init__(self, dialect, connection, compiled=None, statement=None, parameters=None): |
---|
384 | super(SybaseSQLExecutionContext_pyodbc, self).__init__(dialect, connection, compiled, statement, parameters) |
---|
385 | |
---|
386 | def pre_exec(self): |
---|
387 | super(SybaseSQLExecutionContext_pyodbc, self).pre_exec() |
---|
388 | |
---|
389 | def post_exec(self): |
---|
390 | if self.compiled.isinsert: |
---|
391 | table = self.compiled.statement.table |
---|
392 | # get the inserted values of the primary key |
---|
393 | |
---|
394 | # get any sequence IDs first (using @@identity) |
---|
395 | self.cursor.execute("SELECT @@identity AS lastrowid") |
---|
396 | row = self.cursor.fetchone() |
---|
397 | lastrowid = int(row[0]) |
---|
398 | if lastrowid > 0: |
---|
399 | # an IDENTITY was inserted, fetch it |
---|
400 | # FIXME: always insert in front ? This only works if the IDENTITY is the first column, no ?! |
---|
401 | if not hasattr(self, '_last_inserted_ids') or self._last_inserted_ids is None: |
---|
402 | self._last_inserted_ids = [lastrowid] |
---|
403 | else: |
---|
404 | self._last_inserted_ids = [lastrowid] + self._last_inserted_ids[1:] |
---|
405 | super(SybaseSQLExecutionContext_pyodbc, self).post_exec() |
---|
406 | |
---|
407 | class SybaseSQLDialect(default.DefaultDialect): |
---|
408 | colspecs = { |
---|
409 | # FIXME: unicode support |
---|
410 | #sqltypes.Unicode : SybaseUnicode, |
---|
411 | sqltypes.Integer : SybaseInteger, |
---|
412 | sqltypes.SmallInteger : SybaseSmallInteger, |
---|
413 | sqltypes.Numeric : SybaseNumeric, |
---|
414 | sqltypes.Float : SybaseFloat, |
---|
415 | sqltypes.String : SybaseString, |
---|
416 | sqltypes.Binary : SybaseBinary, |
---|
417 | sqltypes.Boolean : SybaseBoolean, |
---|
418 | sqltypes.Text : SybaseText, |
---|
419 | sqltypes.CHAR : SybaseChar, |
---|
420 | sqltypes.TIMESTAMP : SybaseTimeStamp, |
---|
421 | sqltypes.FLOAT : SybaseFloat, |
---|
422 | } |
---|
423 | |
---|
424 | ischema_names = { |
---|
425 | 'integer' : SybaseInteger, |
---|
426 | 'unsigned int' : SybaseInteger, |
---|
427 | 'unsigned smallint' : SybaseInteger, |
---|
428 | 'unsigned bigint' : SybaseInteger, |
---|
429 | 'bigint': SybaseBigInteger, |
---|
430 | 'smallint' : SybaseSmallInteger, |
---|
431 | 'tinyint' : SybaseTinyInteger, |
---|
432 | 'varchar' : SybaseString, |
---|
433 | 'long varchar' : SybaseText, |
---|
434 | 'char' : SybaseChar, |
---|
435 | 'decimal' : SybaseNumeric, |
---|
436 | 'numeric' : SybaseNumeric, |
---|
437 | 'float' : SybaseFloat, |
---|
438 | 'double' : SybaseFloat, |
---|
439 | 'binary' : SybaseBinary, |
---|
440 | 'long binary' : SybaseBinary, |
---|
441 | 'varbinary' : SybaseBinary, |
---|
442 | 'bit': SybaseBoolean, |
---|
443 | 'image' : SybaseBinary, |
---|
444 | 'timestamp': SybaseTimeStamp, |
---|
445 | 'money': SybaseMoney, |
---|
446 | 'smallmoney': SybaseSmallMoney, |
---|
447 | 'uniqueidentifier': SybaseUniqueIdentifier, |
---|
448 | |
---|
449 | 'java.lang.Object' : SybaseTypeError, |
---|
450 | 'java serialization' : SybaseTypeError, |
---|
451 | } |
---|
452 | |
---|
453 | name = 'sybase' |
---|
454 | # Sybase backend peculiarities |
---|
455 | supports_unicode_statements = False |
---|
456 | supports_sane_rowcount = False |
---|
457 | supports_sane_multi_rowcount = False |
---|
458 | execution_ctx_cls = SybaseSQLExecutionContext |
---|
459 | |
---|
460 | def __new__(cls, dbapi=None, *args, **kwargs): |
---|
461 | if cls != SybaseSQLDialect: |
---|
462 | return super(SybaseSQLDialect, cls).__new__(cls, *args, **kwargs) |
---|
463 | if dbapi: |
---|
464 | print dbapi.__name__ |
---|
465 | dialect = dialect_mapping.get(dbapi.__name__) |
---|
466 | return dialect(*args, **kwargs) |
---|
467 | else: |
---|
468 | return object.__new__(cls, *args, **kwargs) |
---|
469 | |
---|
470 | def __init__(self, **params): |
---|
471 | super(SybaseSQLDialect, self).__init__(**params) |
---|
472 | self.text_as_varchar = False |
---|
473 | # FIXME: what is the default schema for sybase connections (DBA?) ? |
---|
474 | self.set_default_schema_name("dba") |
---|
475 | |
---|
476 | def dbapi(cls, module_name=None): |
---|
477 | if module_name: |
---|
478 | try: |
---|
479 | dialect_cls = dialect_mapping[module_name] |
---|
480 | return dialect_cls.import_dbapi() |
---|
481 | except KeyError: |
---|
482 | raise exc.InvalidRequestError("Unsupported SybaseSQL module '%s' requested (must be " + " or ".join([x for x in dialect_mapping.keys()]) + ")" % module_name) |
---|
483 | else: |
---|
484 | for dialect_cls in dialect_mapping.values(): |
---|
485 | try: |
---|
486 | return dialect_cls.import_dbapi() |
---|
487 | except ImportError, e: |
---|
488 | pass |
---|
489 | else: |
---|
490 | raise ImportError('No DBAPI module detected for SybaseSQL - please install mxodbc') |
---|
491 | dbapi = classmethod(dbapi) |
---|
492 | |
---|
493 | def type_descriptor(self, typeobj): |
---|
494 | newobj = sqltypes.adapt_type(typeobj, self.colspecs) |
---|
495 | return newobj |
---|
496 | |
---|
497 | def last_inserted_ids(self): |
---|
498 | return self.context.last_inserted_ids |
---|
499 | |
---|
500 | def get_default_schema_name(self, connection): |
---|
501 | return self.schema_name |
---|
502 | |
---|
503 | def set_default_schema_name(self, schema_name): |
---|
504 | self.schema_name = schema_name |
---|
505 | |
---|
506 | def do_execute(self, cursor, statement, params, **kwargs): |
---|
507 | params = tuple(params) |
---|
508 | super(SybaseSQLDialect, self).do_execute(cursor, statement, params, **kwargs) |
---|
509 | |
---|
510 | # FIXME: remove ? |
---|
511 | def _execute(self, c, statement, parameters): |
---|
512 | try: |
---|
513 | if parameters == {}: |
---|
514 | parameters = () |
---|
515 | c.execute(statement, parameters) |
---|
516 | self.context.rowcount = c.rowcount |
---|
517 | c.DBPROP_COMMITPRESERVE = "Y" |
---|
518 | except Exception, e: |
---|
519 | raise exc.DBAPIError.instance(statement, parameters, e) |
---|
520 | |
---|
521 | def table_names(self, connection, schema): |
---|
522 | """Ignore the schema and the charset for now.""" |
---|
523 | s = sql.select([tables.c.table_name], |
---|
524 | sql.not_(tables.c.table_name.like("SYS%")) and |
---|
525 | tables.c.creator >= 100 |
---|
526 | ) |
---|
527 | rp = connection.execute(s) |
---|
528 | return [row[0] for row in rp.fetchall()] |
---|
529 | |
---|
530 | def has_table(self, connection, tablename, schema=None): |
---|
531 | # FIXME: ignore schemas for sybase |
---|
532 | s = sql.select([tables.c.table_name], tables.c.table_name == tablename) |
---|
533 | |
---|
534 | c = connection.execute(s) |
---|
535 | row = c.fetchone() |
---|
536 | print "has_table: " + tablename + ": " + str(bool(row is not None)) |
---|
537 | return row is not None |
---|
538 | |
---|
539 | def reflecttable(self, connection, table, include_columns): |
---|
540 | # Get base columns |
---|
541 | if table.schema is not None: |
---|
542 | current_schema = table.schema |
---|
543 | else: |
---|
544 | current_schema = self.get_default_schema_name(connection) |
---|
545 | |
---|
546 | s = sql.select([columns, domains], tables.c.table_name==table.name, from_obj=[columns.join(tables).join(domains)], order_by=[columns.c.column_id]) |
---|
547 | |
---|
548 | c = connection.execute(s) |
---|
549 | found_table = False |
---|
550 | # makes sure we append the columns in the correct order |
---|
551 | while True: |
---|
552 | row = c.fetchone() |
---|
553 | if row is None: |
---|
554 | break |
---|
555 | found_table = True |
---|
556 | (name, type, nullable, charlen, numericprec, numericscale, default, primary_key, max_identity, table_id, column_id) = ( |
---|
557 | row[columns.c.column_name], |
---|
558 | row[domains.c.domain_name], |
---|
559 | row[columns.c.nulls] == 'Y', |
---|
560 | row[columns.c.width], |
---|
561 | row[domains.c.precision], |
---|
562 | row[columns.c.scale], |
---|
563 | row[columns.c.default], |
---|
564 | row[columns.c.pkey] == 'Y', |
---|
565 | row[columns.c.max_identity], |
---|
566 | row[tables.c.table_id], |
---|
567 | row[columns.c.column_id], |
---|
568 | ) |
---|
569 | if include_columns and name not in include_columns: |
---|
570 | continue |
---|
571 | |
---|
572 | # FIXME: else problems with SybaseBinary(size) |
---|
573 | if numericscale == 0: |
---|
574 | numericscale = None |
---|
575 | |
---|
576 | args = [] |
---|
577 | for a in (charlen, numericprec, numericscale): |
---|
578 | if a is not None: |
---|
579 | args.append(a) |
---|
580 | coltype = self.ischema_names.get(type, None) |
---|
581 | if coltype == SybaseString and charlen == -1: |
---|
582 | coltype = SybaseText() |
---|
583 | else: |
---|
584 | if coltype is None: |
---|
585 | util.warn("Did not recognize type '%s' of column '%s'" % |
---|
586 | (type, name)) |
---|
587 | coltype = sqltypes.NULLTYPE |
---|
588 | coltype = coltype(*args) |
---|
589 | colargs = [] |
---|
590 | if default is not None: |
---|
591 | colargs.append(schema.DefaultClause(sql.text(default))) |
---|
592 | |
---|
593 | # any sequences ? |
---|
594 | col = schema.Column(name, coltype, nullable=nullable, primary_key=primary_key, *colargs) |
---|
595 | if int(max_identity) > 0: |
---|
596 | col.sequence = schema.Sequence(name + '_identity') |
---|
597 | col.sequence.start = int(max_identity) |
---|
598 | col.sequence.increment = 1 |
---|
599 | |
---|
600 | # append the column |
---|
601 | table.append_column(col) |
---|
602 | |
---|
603 | # any foreign key constraint for this table ? |
---|
604 | # note: no multi-column foreign keys are considered |
---|
605 | s = "select st1.table_name, sc1.column_name, st2.table_name, sc2.column_name from systable as st1 join sysfkcol on st1.table_id=sysfkcol.foreign_table_id join sysforeignkey join systable as st2 on sysforeignkey.primary_table_id = st2.table_id join syscolumn as sc1 on sysfkcol.foreign_column_id=sc1.column_id and sc1.table_id=st1.table_id join syscolumn as sc2 on sysfkcol.primary_column_id=sc2.column_id and sc2.table_id=st2.table_id where st1.table_name='%(table_name)s';" % { 'table_name' : table.name } |
---|
606 | c = connection.execute(s) |
---|
607 | foreignKeys = {} |
---|
608 | while True: |
---|
609 | row = c.fetchone() |
---|
610 | if row is None: |
---|
611 | break |
---|
612 | (foreign_table, foreign_column, primary_table, primary_column) = ( |
---|
613 | row[0], row[1], row[2], row[3], |
---|
614 | ) |
---|
615 | if not primary_table in foreignKeys.keys(): |
---|
616 | foreignKeys[primary_table] = [['%s' % (foreign_column)], ['%s.%s'%(primary_table, primary_column)]] |
---|
617 | else: |
---|
618 | foreignKeys[primary_table][0].append('%s'%(foreign_column)) |
---|
619 | foreignKeys[primary_table][1].append('%s.%s'%(primary_table, primary_column)) |
---|
620 | for primary_table in foreignKeys.keys(): |
---|
621 | #table.append_constraint(schema.ForeignKeyConstraint(['%s.%s'%(foreign_table, foreign_column)], ['%s.%s'%(primary_table,primary_column)])) |
---|
622 | table.append_constraint(schema.ForeignKeyConstraint(foreignKeys[primary_table][0], foreignKeys[primary_table][1], link_to_name=True)) |
---|
623 | |
---|
624 | if not found_table: |
---|
625 | raise exc.NoSuchTableError(table.name) |
---|
626 | |
---|
627 | |
---|
628 | class SybaseSQLDialect_mxodbc(SybaseSQLDialect): |
---|
629 | execution_ctx_cls = SybaseSQLExecutionContext_mxodbc |
---|
630 | |
---|
631 | def __init__(self, **params): |
---|
632 | super(SybaseSQLDialect_mxodbc, self).__init__(**params) |
---|
633 | |
---|
634 | self.dbapi_type_map = {'getdate' : SybaseDate_mxodbc()} |
---|
635 | |
---|
636 | def import_dbapi(cls): |
---|
637 | #import mx.ODBC.Windows as module |
---|
638 | import mxODBC as module |
---|
639 | return module |
---|
640 | import_dbapi = classmethod(import_dbapi) |
---|
641 | |
---|
642 | colspecs = SybaseSQLDialect.colspecs.copy() |
---|
643 | colspecs[sqltypes.Time] = SybaseTime_mxodbc |
---|
644 | colspecs[sqltypes.Date] = SybaseDate_mxodbc |
---|
645 | colspecs[sqltypes.DateTime] = SybaseDateTime_mxodbc |
---|
646 | |
---|
647 | ischema_names = SybaseSQLDialect.ischema_names.copy() |
---|
648 | ischema_names['time'] = SybaseTime_mxodbc |
---|
649 | ischema_names['date'] = SybaseDate_mxodbc |
---|
650 | ischema_names['datetime'] = SybaseDateTime_mxodbc |
---|
651 | ischema_names['smalldatetime'] = SybaseDateTime_mxodbc |
---|
652 | |
---|
653 | def is_disconnect(self, e): |
---|
654 | # FIXME: optimize |
---|
655 | #return isinstance(e, self.dbapi.Error) and '[08S01]' in str(e) |
---|
656 | #return True |
---|
657 | return False |
---|
658 | |
---|
659 | def do_execute(self, cursor, statement, parameters, context=None, **kwargs): |
---|
660 | super(SybaseSQLDialect_mxodbc, self).do_execute(cursor, statement, parameters, context=context, **kwargs) |
---|
661 | |
---|
662 | def create_connect_args(self, url): |
---|
663 | '''Return a tuple of *args,**kwargs''' |
---|
664 | # FIXME: handle mx.odbc.Windows proprietary args |
---|
665 | opts = url.translate_connect_args(username='user') |
---|
666 | opts.update(url.query) |
---|
667 | argsDict = {} |
---|
668 | argsDict['user'] = opts['user'] |
---|
669 | argsDict['password'] = opts['password'] |
---|
670 | connArgs = [[opts['dsn']], argsDict] |
---|
671 | return connArgs |
---|
672 | |
---|
673 | |
---|
674 | class SybaseSQLDialect_pyodbc(SybaseSQLDialect): |
---|
675 | execution_ctx_cls = SybaseSQLExecutionContext_pyodbc |
---|
676 | |
---|
677 | def __init__(self, **params): |
---|
678 | super(SybaseSQLDialect_pyodbc, self).__init__(**params) |
---|
679 | self.dbapi_type_map = {'getdate' : SybaseDate_pyodbc()} |
---|
680 | |
---|
681 | def import_dbapi(cls): |
---|
682 | import mypyodbc as module |
---|
683 | return module |
---|
684 | import_dbapi = classmethod(import_dbapi) |
---|
685 | |
---|
686 | colspecs = SybaseSQLDialect.colspecs.copy() |
---|
687 | colspecs[sqltypes.Time] = SybaseTime_pyodbc |
---|
688 | colspecs[sqltypes.Date] = SybaseDate_pyodbc |
---|
689 | colspecs[sqltypes.DateTime] = SybaseDateTime_pyodbc |
---|
690 | |
---|
691 | ischema_names = SybaseSQLDialect.ischema_names.copy() |
---|
692 | ischema_names['time'] = SybaseTime_pyodbc |
---|
693 | ischema_names['date'] = SybaseDate_pyodbc |
---|
694 | ischema_names['datetime'] = SybaseDateTime_pyodbc |
---|
695 | ischema_names['smalldatetime'] = SybaseDateTime_pyodbc |
---|
696 | |
---|
697 | def is_disconnect(self, e): |
---|
698 | # FIXME: optimize |
---|
699 | #return isinstance(e, self.dbapi.Error) and '[08S01]' in str(e) |
---|
700 | #return True |
---|
701 | return False |
---|
702 | |
---|
703 | def do_execute(self, cursor, statement, parameters, context=None, **kwargs): |
---|
704 | super(SybaseSQLDialect_pyodbc, self).do_execute(cursor, statement, parameters, context=context, **kwargs) |
---|
705 | |
---|
706 | def create_connect_args(self, url): |
---|
707 | '''Return a tuple of *args,**kwargs''' |
---|
708 | # FIXME: handle pyodbc proprietary args |
---|
709 | opts = url.translate_connect_args(username='user') |
---|
710 | opts.update(url.query) |
---|
711 | |
---|
712 | self.autocommit = False |
---|
713 | if 'autocommit' in opts: |
---|
714 | self.autocommit = bool(int(opts.pop('autocommit'))) |
---|
715 | |
---|
716 | argsDict = {} |
---|
717 | argsDict['UID'] = opts['user'] |
---|
718 | argsDict['PWD'] = opts['password'] |
---|
719 | argsDict['DSN'] = opts['dsn'] |
---|
720 | connArgs = [[';'.join(["%s=%s"%(key, argsDict[key]) for key in argsDict])], {'autocommit' : self.autocommit}] |
---|
721 | return connArgs |
---|
722 | |
---|
723 | |
---|
724 | dialect_mapping = { |
---|
725 | 'sqlalchemy.databases.mxODBC' : SybaseSQLDialect_mxodbc, |
---|
726 | # 'pyodbc' : SybaseSQLDialect_pyodbc, |
---|
727 | } |
---|
728 | |
---|
729 | |
---|
730 | class SybaseSQLCompiler(compiler.DefaultCompiler): |
---|
731 | operators = compiler.DefaultCompiler.operators.copy() |
---|
732 | operators.update({ |
---|
733 | sql_operators.mod: lambda x, y: "MOD(%s, %s)" % (x, y), |
---|
734 | }) |
---|
735 | |
---|
736 | extract_map = compiler.DefaultCompiler.extract_map.copy() |
---|
737 | extract_map.update ({ |
---|
738 | 'doy': 'dayofyear', |
---|
739 | 'dow': 'weekday', |
---|
740 | 'milliseconds': 'millisecond' |
---|
741 | }) |
---|
742 | |
---|
743 | |
---|
744 | def bindparam_string(self, name): |
---|
745 | res = super(SybaseSQLCompiler, self).bindparam_string(name) |
---|
746 | if name.lower().startswith('literal'): |
---|
747 | res = 'STRING(%s)' % res |
---|
748 | return res |
---|
749 | |
---|
750 | def get_select_precolumns(self, select): |
---|
751 | s = select._distinct and "DISTINCT " or "" |
---|
752 | if select._limit: |
---|
753 | #if select._limit == 1: |
---|
754 | #s += "FIRST " |
---|
755 | #else: |
---|
756 | #s += "TOP %s " % (select._limit,) |
---|
757 | s += "TOP %s " % (select._limit,) |
---|
758 | if select._offset: |
---|
759 | if not select._limit: |
---|
760 | # FIXME: sybase doesn't allow an offset without a limit |
---|
761 | # so use a huge value for TOP here |
---|
762 | s += "TOP 1000000 " |
---|
763 | s += "START AT %s " % (select._offset+1,) |
---|
764 | return s |
---|
765 | |
---|
766 | def limit_clause(self, select): |
---|
767 | # Limit in sybase is after the select keyword |
---|
768 | return "" |
---|
769 | |
---|
770 | def visit_binary(self, binary): |
---|
771 | """Move bind parameters to the right-hand side of an operator, where possible.""" |
---|
772 | if isinstance(binary.left, expression._BindParamClause) and binary.operator == operator.eq: |
---|
773 | return self.process(expression._BinaryExpression(binary.right, binary.left, binary.operator)) |
---|
774 | else: |
---|
775 | return super(SybaseSQLCompiler, self).visit_binary(binary) |
---|
776 | |
---|
777 | def label_select_column(self, select, column, asfrom): |
---|
778 | if isinstance(column, expression.Function): |
---|
779 | return column.label(None) |
---|
780 | else: |
---|
781 | return super(SybaseSQLCompiler, self).label_select_column(select, column, asfrom) |
---|
782 | |
---|
783 | function_rewrites = {'current_date': 'getdate', |
---|
784 | } |
---|
785 | def visit_function(self, func): |
---|
786 | func.name = self.function_rewrites.get(func.name, func.name) |
---|
787 | res = super(SybaseSQLCompiler, self).visit_function(func) |
---|
788 | if func.name.lower() == 'getdate': |
---|
789 | # apply CAST operator |
---|
790 | # FIXME: what about _pyodbc ? |
---|
791 | cast = expression._Cast(func, SybaseDate_mxodbc) |
---|
792 | # infinite recursion |
---|
793 | # res = self.visit_cast(cast) |
---|
794 | res = "CAST(%s AS %s)" % (res, self.process(cast.typeclause)) |
---|
795 | return res |
---|
796 | |
---|
797 | def visit_extract(self, extract): |
---|
798 | field = self.extract_map.get(extract.field, extract.field) |
---|
799 | return 'DATEPART("%s", %s)' % (field, self.process(extract.expr)) |
---|
800 | |
---|
801 | def for_update_clause(self, select): |
---|
802 | # "FOR UPDATE" is only allowed on "DECLARE CURSOR" which SQLAlchemy doesn't use |
---|
803 | return '' |
---|
804 | |
---|
805 | def order_by_clause(self, select): |
---|
806 | order_by = self.process(select._order_by_clause) |
---|
807 | |
---|
808 | # SybaseSQL only allows ORDER BY in subqueries if there is a LIMIT |
---|
809 | if order_by and (not self.is_subquery() or select._limit): |
---|
810 | return " ORDER BY " + order_by |
---|
811 | else: |
---|
812 | return "" |
---|
813 | |
---|
814 | |
---|
815 | class SybaseSQLSchemaGenerator(compiler.SchemaGenerator): |
---|
816 | def get_column_specification(self, column, **kwargs): |
---|
817 | |
---|
818 | colspec = self.preparer.format_column(column) |
---|
819 | |
---|
820 | if (not getattr(column.table, 'has_sequence', False)) and column.primary_key and \ |
---|
821 | column.autoincrement and isinstance(column.type, sqltypes.Integer): |
---|
822 | if column.default is None or (isinstance(column.default, schema.Sequence) and column.default.optional): |
---|
823 | column.sequence = schema.Sequence(column.name + '_seq') |
---|
824 | |
---|
825 | if hasattr(column, 'sequence'): |
---|
826 | column.table.has_sequence = column |
---|
827 | #colspec += " numeric(30,0) IDENTITY" |
---|
828 | colspec += " Integer IDENTITY" |
---|
829 | else: |
---|
830 | colspec += " " + column.type.dialect_impl(self.dialect).get_col_spec() |
---|
831 | |
---|
832 | if not column.nullable: |
---|
833 | colspec += " NOT NULL" |
---|
834 | |
---|
835 | default = self.get_column_default_string(column) |
---|
836 | if default is not None: |
---|
837 | colspec += " DEFAULT " + default |
---|
838 | |
---|
839 | return colspec |
---|
840 | |
---|
841 | |
---|
842 | class SybaseSQLSchemaDropper(compiler.SchemaDropper): |
---|
843 | def visit_index(self, index): |
---|
844 | self.append("\nDROP INDEX %s.%s" % ( |
---|
845 | self.preparer.quote_identifier(index.table.name), |
---|
846 | self.preparer.quote(self._validate_identifier(index.name, False), index.quote) |
---|
847 | )) |
---|
848 | self.execute() |
---|
849 | |
---|
850 | |
---|
851 | class SybaseSQLDefaultRunner(base.DefaultRunner): |
---|
852 | pass |
---|
853 | |
---|
854 | |
---|
855 | class SybaseSQLIdentifierPreparer(compiler.IdentifierPreparer): |
---|
856 | reserved_words = RESERVED_WORDS |
---|
857 | |
---|
858 | def __init__(self, dialect): |
---|
859 | super(SybaseSQLIdentifierPreparer, self).__init__(dialect) |
---|
860 | |
---|
861 | def _escape_identifier(self, value): |
---|
862 | #TODO: determin SybaseSQL's escapeing rules |
---|
863 | return value |
---|
864 | |
---|
865 | def _fold_identifier_case(self, value): |
---|
866 | #TODO: determin SybaseSQL's case folding rules |
---|
867 | return value |
---|
868 | |
---|
869 | |
---|
870 | dialect = SybaseSQLDialect |
---|
871 | dialect.statement_compiler = SybaseSQLCompiler |
---|
872 | dialect.schemagenerator = SybaseSQLSchemaGenerator |
---|
873 | dialect.schemadropper = SybaseSQLSchemaDropper |
---|
874 | dialect.preparer = SybaseSQLIdentifierPreparer |
---|
875 | dialect.defaultrunner = SybaseSQLDefaultRunner |
---|