620 lines
24 KiB
Python
620 lines
24 KiB
Python
|
# firebird.py
|
||
|
# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com
|
||
|
#
|
||
|
# This module is part of SQLAlchemy and is released under
|
||
|
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||
|
|
||
|
"""
|
||
|
Support for the Firebird database.
|
||
|
|
||
|
Connectivity is usually supplied via the kinterbasdb_ DBAPI module.
|
||
|
|
||
|
Dialects
|
||
|
~~~~~~~~
|
||
|
|
||
|
Firebird offers two distinct dialects_ (not to be confused with a
|
||
|
SQLAlchemy ``Dialect``):
|
||
|
|
||
|
dialect 1
|
||
|
This is the old syntax and behaviour, inherited from Interbase pre-6.0.
|
||
|
|
||
|
dialect 3
|
||
|
This is the newer and supported syntax, introduced in Interbase 6.0.
|
||
|
|
||
|
The SQLAlchemy Firebird dialect detects these versions and
|
||
|
adjusts its representation of SQL accordingly. However,
|
||
|
support for dialect 1 is not well tested and probably has
|
||
|
incompatibilities.
|
||
|
|
||
|
Locking Behavior
|
||
|
~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Firebird locks tables aggressively. For this reason, a DROP TABLE may
|
||
|
hang until other transactions are released. SQLAlchemy does its best
|
||
|
to release transactions as quickly as possible. The most common cause
|
||
|
of hanging transactions is a non-fully consumed result set, i.e.::
|
||
|
|
||
|
result = engine.execute("select * from table")
|
||
|
row = result.fetchone()
|
||
|
return
|
||
|
|
||
|
Where above, the ``ResultProxy`` has not been fully consumed. The
|
||
|
connection will be returned to the pool and the transactional state
|
||
|
rolled back once the Python garbage collector reclaims the objects
|
||
|
which hold onto the connection, which often occurs asynchronously.
|
||
|
The above use case can be alleviated by calling ``first()`` on the
|
||
|
``ResultProxy`` which will fetch the first row and immediately close
|
||
|
all remaining cursor/connection resources.
|
||
|
|
||
|
RETURNING support
|
||
|
~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Firebird 2.0 supports returning a result set from inserts, and 2.1
|
||
|
extends that to deletes and updates. This is generically exposed by
|
||
|
the SQLAlchemy ``returning()`` method, such as::
|
||
|
|
||
|
# INSERT..RETURNING
|
||
|
result = table.insert().returning(table.c.col1, table.c.col2).\\
|
||
|
values(name='foo')
|
||
|
print result.fetchall()
|
||
|
|
||
|
# UPDATE..RETURNING
|
||
|
raises = empl.update().returning(empl.c.id, empl.c.salary).\\
|
||
|
where(empl.c.sales>100).\\
|
||
|
values(dict(salary=empl.c.salary * 1.1))
|
||
|
print raises.fetchall()
|
||
|
|
||
|
|
||
|
.. _dialects: http://mc-computing.com/Databases/Firebird/SQL_Dialect.html
|
||
|
|
||
|
"""
|
||
|
|
||
|
import datetime, re
|
||
|
|
||
|
from sqlalchemy import schema as sa_schema
|
||
|
from sqlalchemy import exc, types as sqltypes, sql, util
|
||
|
from sqlalchemy.sql import expression
|
||
|
from sqlalchemy.engine import base, default, reflection
|
||
|
from sqlalchemy.sql import compiler
|
||
|
|
||
|
|
||
|
from sqlalchemy.types import (BIGINT, BLOB, BOOLEAN, CHAR, DATE,
|
||
|
FLOAT, INTEGER, NUMERIC, SMALLINT,
|
||
|
TEXT, TIME, TIMESTAMP, VARCHAR)
|
||
|
|
||
|
|
||
|
RESERVED_WORDS = set([
|
||
|
"active", "add", "admin", "after", "all", "alter", "and", "any", "as",
|
||
|
"asc", "ascending", "at", "auto", "avg", "before", "begin", "between",
|
||
|
"bigint", "bit_length", "blob", "both", "by", "case", "cast", "char",
|
||
|
"character", "character_length", "char_length", "check", "close",
|
||
|
"collate", "column", "commit", "committed", "computed", "conditional",
|
||
|
"connect", "constraint", "containing", "count", "create", "cross",
|
||
|
"cstring", "current", "current_connection", "current_date",
|
||
|
"current_role", "current_time", "current_timestamp",
|
||
|
"current_transaction", "current_user", "cursor", "database", "date",
|
||
|
"day", "dec", "decimal", "declare", "default", "delete", "desc",
|
||
|
"descending", "disconnect", "distinct", "do", "domain", "double",
|
||
|
"drop", "else", "end", "entry_point", "escape", "exception",
|
||
|
"execute", "exists", "exit", "external", "extract", "fetch", "file",
|
||
|
"filter", "float", "for", "foreign", "from", "full", "function",
|
||
|
"gdscode", "generator", "gen_id", "global", "grant", "group",
|
||
|
"having", "hour", "if", "in", "inactive", "index", "inner",
|
||
|
"input_type", "insensitive", "insert", "int", "integer", "into", "is",
|
||
|
"isolation", "join", "key", "leading", "left", "length", "level",
|
||
|
"like", "long", "lower", "manual", "max", "maximum_segment", "merge",
|
||
|
"min", "minute", "module_name", "month", "names", "national",
|
||
|
"natural", "nchar", "no", "not", "null", "numeric", "octet_length",
|
||
|
"of", "on", "only", "open", "option", "or", "order", "outer",
|
||
|
"output_type", "overflow", "page", "pages", "page_size", "parameter",
|
||
|
"password", "plan", "position", "post_event", "precision", "primary",
|
||
|
"privileges", "procedure", "protected", "rdb$db_key", "read", "real",
|
||
|
"record_version", "recreate", "recursive", "references", "release",
|
||
|
"reserv", "reserving", "retain", "returning_values", "returns",
|
||
|
"revoke", "right", "rollback", "rows", "row_count", "savepoint",
|
||
|
"schema", "second", "segment", "select", "sensitive", "set", "shadow",
|
||
|
"shared", "singular", "size", "smallint", "snapshot", "some", "sort",
|
||
|
"sqlcode", "stability", "start", "starting", "starts", "statistics",
|
||
|
"sub_type", "sum", "suspend", "table", "then", "time", "timestamp",
|
||
|
"to", "trailing", "transaction", "trigger", "trim", "uncommitted",
|
||
|
"union", "unique", "update", "upper", "user", "using", "value",
|
||
|
"values", "varchar", "variable", "varying", "view", "wait", "when",
|
||
|
"where", "while", "with", "work", "write", "year",
|
||
|
])
|
||
|
|
||
|
|
||
|
colspecs = {
|
||
|
}
|
||
|
|
||
|
ischema_names = {
|
||
|
'SHORT': SMALLINT,
|
||
|
'LONG': BIGINT,
|
||
|
'QUAD': FLOAT,
|
||
|
'FLOAT': FLOAT,
|
||
|
'DATE': DATE,
|
||
|
'TIME': TIME,
|
||
|
'TEXT': TEXT,
|
||
|
'INT64': NUMERIC,
|
||
|
'DOUBLE': FLOAT,
|
||
|
'TIMESTAMP': TIMESTAMP,
|
||
|
'VARYING': VARCHAR,
|
||
|
'CSTRING': CHAR,
|
||
|
'BLOB': BLOB,
|
||
|
}
|
||
|
|
||
|
|
||
|
# TODO: date conversion types (should be implemented as _FBDateTime, _FBDate, etc.
|
||
|
# as bind/result functionality is required)
|
||
|
|
||
|
class FBTypeCompiler(compiler.GenericTypeCompiler):
|
||
|
def visit_boolean(self, type_):
|
||
|
return self.visit_SMALLINT(type_)
|
||
|
|
||
|
def visit_datetime(self, type_):
|
||
|
return self.visit_TIMESTAMP(type_)
|
||
|
|
||
|
def visit_TEXT(self, type_):
|
||
|
return "BLOB SUB_TYPE 1"
|
||
|
|
||
|
def visit_BLOB(self, type_):
|
||
|
return "BLOB SUB_TYPE 0"
|
||
|
|
||
|
|
||
|
class FBCompiler(sql.compiler.SQLCompiler):
|
||
|
"""Firebird specific idiosincrasies"""
|
||
|
|
||
|
def visit_mod(self, binary, **kw):
|
||
|
# Firebird lacks a builtin modulo operator, but there is
|
||
|
# an equivalent function in the ib_udf library.
|
||
|
return "mod(%s, %s)" % (self.process(binary.left), self.process(binary.right))
|
||
|
|
||
|
def visit_alias(self, alias, asfrom=False, **kwargs):
|
||
|
if self.dialect._version_two:
|
||
|
return super(FBCompiler, self).visit_alias(alias, asfrom=asfrom, **kwargs)
|
||
|
else:
|
||
|
# Override to not use the AS keyword which FB 1.5 does not like
|
||
|
if asfrom:
|
||
|
alias_name = isinstance(alias.name, expression._generated_label) and \
|
||
|
self._truncated_identifier("alias", alias.name) or alias.name
|
||
|
|
||
|
return self.process(alias.original, asfrom=asfrom, **kwargs) + " " + \
|
||
|
self.preparer.format_alias(alias, alias_name)
|
||
|
else:
|
||
|
return self.process(alias.original, **kwargs)
|
||
|
|
||
|
def visit_substring_func(self, func, **kw):
|
||
|
s = self.process(func.clauses.clauses[0])
|
||
|
start = self.process(func.clauses.clauses[1])
|
||
|
if len(func.clauses.clauses) > 2:
|
||
|
length = self.process(func.clauses.clauses[2])
|
||
|
return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length)
|
||
|
else:
|
||
|
return "SUBSTRING(%s FROM %s)" % (s, start)
|
||
|
|
||
|
def visit_length_func(self, function, **kw):
|
||
|
if self.dialect._version_two:
|
||
|
return "char_length" + self.function_argspec(function)
|
||
|
else:
|
||
|
return "strlen" + self.function_argspec(function)
|
||
|
|
||
|
visit_char_length_func = visit_length_func
|
||
|
|
||
|
def function_argspec(self, func, **kw):
|
||
|
if func.clauses is not None and len(func.clauses):
|
||
|
return self.process(func.clause_expr)
|
||
|
else:
|
||
|
return ""
|
||
|
|
||
|
def default_from(self):
|
||
|
return " FROM rdb$database"
|
||
|
|
||
|
def visit_sequence(self, seq):
|
||
|
return "gen_id(%s, 1)" % self.preparer.format_sequence(seq)
|
||
|
|
||
|
def get_select_precolumns(self, select):
|
||
|
"""Called when building a ``SELECT`` statement, position is just
|
||
|
before column list Firebird puts the limit and offset right
|
||
|
after the ``SELECT``...
|
||
|
"""
|
||
|
|
||
|
result = ""
|
||
|
if select._limit:
|
||
|
result += "FIRST %d " % select._limit
|
||
|
if select._offset:
|
||
|
result +="SKIP %d " % select._offset
|
||
|
if select._distinct:
|
||
|
result += "DISTINCT "
|
||
|
return result
|
||
|
|
||
|
def limit_clause(self, select):
|
||
|
"""Already taken care of in the `get_select_precolumns` method."""
|
||
|
|
||
|
return ""
|
||
|
|
||
|
def returning_clause(self, stmt, returning_cols):
|
||
|
|
||
|
columns = [
|
||
|
self.process(
|
||
|
self.label_select_column(None, c, asfrom=False),
|
||
|
within_columns_clause=True,
|
||
|
result_map=self.result_map
|
||
|
)
|
||
|
for c in expression._select_iterables(returning_cols)
|
||
|
]
|
||
|
return 'RETURNING ' + ', '.join(columns)
|
||
|
|
||
|
|
||
|
class FBDDLCompiler(sql.compiler.DDLCompiler):
|
||
|
"""Firebird syntactic idiosincrasies"""
|
||
|
|
||
|
def visit_create_sequence(self, create):
|
||
|
"""Generate a ``CREATE GENERATOR`` statement for the sequence."""
|
||
|
|
||
|
# no syntax for these
|
||
|
# http://www.firebirdsql.org/manual/generatorguide-sqlsyntax.html
|
||
|
if create.element.start is not None:
|
||
|
raise NotImplemented("Firebird SEQUENCE doesn't support START WITH")
|
||
|
if create.element.increment is not None:
|
||
|
raise NotImplemented("Firebird SEQUENCE doesn't support INCREMENT BY")
|
||
|
|
||
|
if self.dialect._version_two:
|
||
|
return "CREATE SEQUENCE %s" % self.preparer.format_sequence(create.element)
|
||
|
else:
|
||
|
return "CREATE GENERATOR %s" % self.preparer.format_sequence(create.element)
|
||
|
|
||
|
def visit_drop_sequence(self, drop):
|
||
|
"""Generate a ``DROP GENERATOR`` statement for the sequence."""
|
||
|
|
||
|
if self.dialect._version_two:
|
||
|
return "DROP SEQUENCE %s" % self.preparer.format_sequence(drop.element)
|
||
|
else:
|
||
|
return "DROP GENERATOR %s" % self.preparer.format_sequence(drop.element)
|
||
|
|
||
|
|
||
|
class FBIdentifierPreparer(sql.compiler.IdentifierPreparer):
|
||
|
"""Install Firebird specific reserved words."""
|
||
|
|
||
|
reserved_words = RESERVED_WORDS
|
||
|
|
||
|
def __init__(self, dialect):
|
||
|
super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True)
|
||
|
|
||
|
|
||
|
class FBExecutionContext(default.DefaultExecutionContext):
|
||
|
def fire_sequence(self, seq):
|
||
|
"""Get the next value from the sequence using ``gen_id()``."""
|
||
|
|
||
|
return self._execute_scalar("SELECT gen_id(%s, 1) FROM rdb$database" % \
|
||
|
self.dialect.identifier_preparer.format_sequence(seq))
|
||
|
|
||
|
|
||
|
class FBDialect(default.DefaultDialect):
|
||
|
"""Firebird dialect"""
|
||
|
|
||
|
name = 'firebird'
|
||
|
|
||
|
max_identifier_length = 31
|
||
|
|
||
|
supports_sequences = True
|
||
|
sequences_optional = False
|
||
|
supports_default_values = True
|
||
|
postfetch_lastrowid = False
|
||
|
|
||
|
supports_native_boolean = False
|
||
|
|
||
|
requires_name_normalize = True
|
||
|
supports_empty_insert = False
|
||
|
|
||
|
|
||
|
statement_compiler = FBCompiler
|
||
|
ddl_compiler = FBDDLCompiler
|
||
|
preparer = FBIdentifierPreparer
|
||
|
type_compiler = FBTypeCompiler
|
||
|
execution_ctx_cls = FBExecutionContext
|
||
|
|
||
|
colspecs = colspecs
|
||
|
ischema_names = ischema_names
|
||
|
|
||
|
# defaults to dialect ver. 3,
|
||
|
# will be autodetected off upon
|
||
|
# first connect
|
||
|
_version_two = True
|
||
|
|
||
|
def initialize(self, connection):
|
||
|
super(FBDialect, self).initialize(connection)
|
||
|
self._version_two = self.server_version_info > (2, )
|
||
|
if not self._version_two:
|
||
|
# TODO: whatever other pre < 2.0 stuff goes here
|
||
|
self.ischema_names = ischema_names.copy()
|
||
|
self.ischema_names['TIMESTAMP'] = sqltypes.DATE
|
||
|
self.colspecs = {
|
||
|
sqltypes.DateTime: sqltypes.DATE
|
||
|
}
|
||
|
else:
|
||
|
self.implicit_returning = True
|
||
|
|
||
|
def normalize_name(self, name):
|
||
|
# Remove trailing spaces: FB uses a CHAR() type,
|
||
|
# that is padded with spaces
|
||
|
name = name and name.rstrip()
|
||
|
if name is None:
|
||
|
return None
|
||
|
elif name.upper() == name and \
|
||
|
not self.identifier_preparer._requires_quotes(name.lower()):
|
||
|
return name.lower()
|
||
|
else:
|
||
|
return name
|
||
|
|
||
|
def denormalize_name(self, name):
|
||
|
if name is None:
|
||
|
return None
|
||
|
elif name.lower() == name and \
|
||
|
not self.identifier_preparer._requires_quotes(name.lower()):
|
||
|
return name.upper()
|
||
|
else:
|
||
|
return name
|
||
|
|
||
|
def has_table(self, connection, table_name, schema=None):
|
||
|
"""Return ``True`` if the given table exists, ignoring the `schema`."""
|
||
|
|
||
|
tblqry = """
|
||
|
SELECT 1 FROM rdb$database
|
||
|
WHERE EXISTS (SELECT rdb$relation_name
|
||
|
FROM rdb$relations
|
||
|
WHERE rdb$relation_name=?)
|
||
|
"""
|
||
|
c = connection.execute(tblqry, [self.denormalize_name(table_name)])
|
||
|
return c.first() is not None
|
||
|
|
||
|
def has_sequence(self, connection, sequence_name, schema=None):
|
||
|
"""Return ``True`` if the given sequence (generator) exists."""
|
||
|
|
||
|
genqry = """
|
||
|
SELECT 1 FROM rdb$database
|
||
|
WHERE EXISTS (SELECT rdb$generator_name
|
||
|
FROM rdb$generators
|
||
|
WHERE rdb$generator_name=?)
|
||
|
"""
|
||
|
c = connection.execute(genqry, [self.denormalize_name(sequence_name)])
|
||
|
return c.first() is not None
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_table_names(self, connection, schema=None, **kw):
|
||
|
s = """
|
||
|
SELECT DISTINCT rdb$relation_name
|
||
|
FROM rdb$relation_fields
|
||
|
WHERE rdb$system_flag=0 AND rdb$view_context IS NULL
|
||
|
"""
|
||
|
return [self.normalize_name(row[0]) for row in connection.execute(s)]
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_view_names(self, connection, schema=None, **kw):
|
||
|
s = """
|
||
|
SELECT distinct rdb$view_name
|
||
|
FROM rdb$view_relations
|
||
|
"""
|
||
|
return [self.normalize_name(row[0]) for row in connection.execute(s)]
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_view_definition(self, connection, view_name, schema=None, **kw):
|
||
|
qry = """
|
||
|
SELECT rdb$view_source AS view_source
|
||
|
FROM rdb$relations
|
||
|
WHERE rdb$relation_name=?
|
||
|
"""
|
||
|
rp = connection.execute(qry, [self.denormalize_name(view_name)])
|
||
|
row = rp.first()
|
||
|
if row:
|
||
|
return row['view_source']
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_primary_keys(self, connection, table_name, schema=None, **kw):
|
||
|
# Query to extract the PK/FK constrained fields of the given table
|
||
|
keyqry = """
|
||
|
SELECT se.rdb$field_name AS fname
|
||
|
FROM rdb$relation_constraints rc
|
||
|
JOIN rdb$index_segments se ON rc.rdb$index_name=se.rdb$index_name
|
||
|
WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=?
|
||
|
"""
|
||
|
tablename = self.denormalize_name(table_name)
|
||
|
# get primary key fields
|
||
|
c = connection.execute(keyqry, ["PRIMARY KEY", tablename])
|
||
|
pkfields = [self.normalize_name(r['fname']) for r in c.fetchall()]
|
||
|
return pkfields
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_column_sequence(self, connection, table_name, column_name, schema=None, **kw):
|
||
|
tablename = self.denormalize_name(table_name)
|
||
|
colname = self.denormalize_name(column_name)
|
||
|
# Heuristic-query to determine the generator associated to a PK field
|
||
|
genqry = """
|
||
|
SELECT trigdep.rdb$depended_on_name AS fgenerator
|
||
|
FROM rdb$dependencies tabdep
|
||
|
JOIN rdb$dependencies trigdep
|
||
|
ON tabdep.rdb$dependent_name=trigdep.rdb$dependent_name
|
||
|
AND trigdep.rdb$depended_on_type=14
|
||
|
AND trigdep.rdb$dependent_type=2
|
||
|
JOIN rdb$triggers trig ON trig.rdb$trigger_name=tabdep.rdb$dependent_name
|
||
|
WHERE tabdep.rdb$depended_on_name=?
|
||
|
AND tabdep.rdb$depended_on_type=0
|
||
|
AND trig.rdb$trigger_type=1
|
||
|
AND tabdep.rdb$field_name=?
|
||
|
AND (SELECT count(*)
|
||
|
FROM rdb$dependencies trigdep2
|
||
|
WHERE trigdep2.rdb$dependent_name = trigdep.rdb$dependent_name) = 2
|
||
|
"""
|
||
|
genr = connection.execute(genqry, [tablename, colname]).first()
|
||
|
if genr is not None:
|
||
|
return dict(name=self.normalize_name(genr['fgenerator']))
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_columns(self, connection, table_name, schema=None, **kw):
|
||
|
# Query to extract the details of all the fields of the given table
|
||
|
tblqry = """
|
||
|
SELECT DISTINCT r.rdb$field_name AS fname,
|
||
|
r.rdb$null_flag AS null_flag,
|
||
|
t.rdb$type_name AS ftype,
|
||
|
f.rdb$field_sub_type AS stype,
|
||
|
f.rdb$field_length/COALESCE(cs.rdb$bytes_per_character,1) AS flen,
|
||
|
f.rdb$field_precision AS fprec,
|
||
|
f.rdb$field_scale AS fscale,
|
||
|
COALESCE(r.rdb$default_source, f.rdb$default_source) AS fdefault
|
||
|
FROM rdb$relation_fields r
|
||
|
JOIN rdb$fields f ON r.rdb$field_source=f.rdb$field_name
|
||
|
JOIN rdb$types t
|
||
|
ON t.rdb$type=f.rdb$field_type AND t.rdb$field_name='RDB$FIELD_TYPE'
|
||
|
LEFT JOIN rdb$character_sets cs ON f.rdb$character_set_id=cs.rdb$character_set_id
|
||
|
WHERE f.rdb$system_flag=0 AND r.rdb$relation_name=?
|
||
|
ORDER BY r.rdb$field_position
|
||
|
"""
|
||
|
# get the PK, used to determine the eventual associated sequence
|
||
|
pkey_cols = self.get_primary_keys(connection, table_name)
|
||
|
|
||
|
tablename = self.denormalize_name(table_name)
|
||
|
# get all of the fields for this table
|
||
|
c = connection.execute(tblqry, [tablename])
|
||
|
cols = []
|
||
|
while True:
|
||
|
row = c.fetchone()
|
||
|
if row is None:
|
||
|
break
|
||
|
name = self.normalize_name(row['fname'])
|
||
|
orig_colname = row['fname']
|
||
|
|
||
|
# get the data type
|
||
|
colspec = row['ftype'].rstrip()
|
||
|
coltype = self.ischema_names.get(colspec)
|
||
|
if coltype is None:
|
||
|
util.warn("Did not recognize type '%s' of column '%s'" %
|
||
|
(colspec, name))
|
||
|
coltype = sqltypes.NULLTYPE
|
||
|
elif colspec == 'INT64':
|
||
|
coltype = coltype(precision=row['fprec'], scale=row['fscale'] * -1)
|
||
|
elif colspec in ('VARYING', 'CSTRING'):
|
||
|
coltype = coltype(row['flen'])
|
||
|
elif colspec == 'TEXT':
|
||
|
coltype = TEXT(row['flen'])
|
||
|
elif colspec == 'BLOB':
|
||
|
if row['stype'] == 1:
|
||
|
coltype = TEXT()
|
||
|
else:
|
||
|
coltype = BLOB()
|
||
|
else:
|
||
|
coltype = coltype(row)
|
||
|
|
||
|
# does it have a default value?
|
||
|
defvalue = None
|
||
|
if row['fdefault'] is not None:
|
||
|
# the value comes down as "DEFAULT 'value'": there may be
|
||
|
# more than one whitespace around the "DEFAULT" keyword
|
||
|
# (see also http://tracker.firebirdsql.org/browse/CORE-356)
|
||
|
defexpr = row['fdefault'].lstrip()
|
||
|
assert defexpr[:8].rstrip()=='DEFAULT', "Unrecognized default value: %s" % defexpr
|
||
|
defvalue = defexpr[8:].strip()
|
||
|
if defvalue == 'NULL':
|
||
|
# Redundant
|
||
|
defvalue = None
|
||
|
col_d = {
|
||
|
'name' : name,
|
||
|
'type' : coltype,
|
||
|
'nullable' : not bool(row['null_flag']),
|
||
|
'default' : defvalue
|
||
|
}
|
||
|
|
||
|
if orig_colname.lower() == orig_colname:
|
||
|
col_d['quote'] = True
|
||
|
|
||
|
# if the PK is a single field, try to see if its linked to
|
||
|
# a sequence thru a trigger
|
||
|
if len(pkey_cols)==1 and name==pkey_cols[0]:
|
||
|
seq_d = self.get_column_sequence(connection, tablename, name)
|
||
|
if seq_d is not None:
|
||
|
col_d['sequence'] = seq_d
|
||
|
|
||
|
cols.append(col_d)
|
||
|
return cols
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
|
||
|
# Query to extract the details of each UK/FK of the given table
|
||
|
fkqry = """
|
||
|
SELECT rc.rdb$constraint_name AS cname,
|
||
|
cse.rdb$field_name AS fname,
|
||
|
ix2.rdb$relation_name AS targetrname,
|
||
|
se.rdb$field_name AS targetfname
|
||
|
FROM rdb$relation_constraints rc
|
||
|
JOIN rdb$indices ix1 ON ix1.rdb$index_name=rc.rdb$index_name
|
||
|
JOIN rdb$indices ix2 ON ix2.rdb$index_name=ix1.rdb$foreign_key
|
||
|
JOIN rdb$index_segments cse ON cse.rdb$index_name=ix1.rdb$index_name
|
||
|
JOIN rdb$index_segments se
|
||
|
ON se.rdb$index_name=ix2.rdb$index_name
|
||
|
AND se.rdb$field_position=cse.rdb$field_position
|
||
|
WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=?
|
||
|
ORDER BY se.rdb$index_name, se.rdb$field_position
|
||
|
"""
|
||
|
tablename = self.denormalize_name(table_name)
|
||
|
|
||
|
c = connection.execute(fkqry, ["FOREIGN KEY", tablename])
|
||
|
fks = util.defaultdict(lambda:{
|
||
|
'name' : None,
|
||
|
'constrained_columns' : [],
|
||
|
'referred_schema' : None,
|
||
|
'referred_table' : None,
|
||
|
'referred_columns' : []
|
||
|
})
|
||
|
|
||
|
for row in c:
|
||
|
cname = self.normalize_name(row['cname'])
|
||
|
fk = fks[cname]
|
||
|
if not fk['name']:
|
||
|
fk['name'] = cname
|
||
|
fk['referred_table'] = self.normalize_name(row['targetrname'])
|
||
|
fk['constrained_columns'].append(self.normalize_name(row['fname']))
|
||
|
fk['referred_columns'].append(
|
||
|
self.normalize_name(row['targetfname']))
|
||
|
return fks.values()
|
||
|
|
||
|
@reflection.cache
|
||
|
def get_indexes(self, connection, table_name, schema=None, **kw):
|
||
|
qry = """
|
||
|
SELECT ix.rdb$index_name AS index_name,
|
||
|
ix.rdb$unique_flag AS unique_flag,
|
||
|
ic.rdb$field_name AS field_name
|
||
|
FROM rdb$indices ix
|
||
|
JOIN rdb$index_segments ic
|
||
|
ON ix.rdb$index_name=ic.rdb$index_name
|
||
|
LEFT OUTER JOIN rdb$relation_constraints
|
||
|
ON rdb$relation_constraints.rdb$index_name = ic.rdb$index_name
|
||
|
WHERE ix.rdb$relation_name=? AND ix.rdb$foreign_key IS NULL
|
||
|
AND rdb$relation_constraints.rdb$constraint_type IS NULL
|
||
|
ORDER BY index_name, field_name
|
||
|
"""
|
||
|
c = connection.execute(qry, [self.denormalize_name(table_name)])
|
||
|
|
||
|
indexes = util.defaultdict(dict)
|
||
|
for row in c:
|
||
|
indexrec = indexes[row['index_name']]
|
||
|
if 'name' not in indexrec:
|
||
|
indexrec['name'] = self.normalize_name(row['index_name'])
|
||
|
indexrec['column_names'] = []
|
||
|
indexrec['unique'] = bool(row['unique_flag'])
|
||
|
|
||
|
indexrec['column_names'].append(self.normalize_name(row['field_name']))
|
||
|
|
||
|
return indexes.values()
|
||
|
|
||
|
def do_execute(self, cursor, statement, parameters, **kwargs):
|
||
|
# kinterbase does not accept a None, but wants an empty list
|
||
|
# when there are no arguments.
|
||
|
cursor.execute(statement, parameters or [])
|
||
|
|
||
|
def do_rollback(self, connection):
|
||
|
# Use the retaining feature, that keeps the transaction going
|
||
|
connection.rollback(True)
|
||
|
|
||
|
def do_commit(self, connection):
|
||
|
# Use the retaining feature, that keeps the transaction going
|
||
|
connection.commit(True)
|