123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989 |
- # firebird/base.py
- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: https://www.opensource.org/licenses/mit-license.php
- r"""
- .. dialect:: firebird
- :name: Firebird
- .. note::
- The Firebird dialect within SQLAlchemy **is not currently supported**.
- It is not tested within continuous integration and is likely to have
- many issues and caveats not currently handled. Consider using the
- `external dialect <https://github.com/pauldex/sqlalchemy-firebird>`_
- instead.
- .. deprecated:: 1.4 The internal Firebird dialect is deprecated and will be
- removed in a future version. Use the external dialect.
- Firebird 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(text("select * from table"))
- row = result.fetchone()
- return
- Where above, the ``CursorResult`` 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
- ``CursorResult`` 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: https://mc-computing.com/Databases/Firebird/SQL_Dialect.html
- """
- import datetime
- from sqlalchemy import exc
- from sqlalchemy import sql
- from sqlalchemy import types as sqltypes
- from sqlalchemy import util
- from sqlalchemy.engine import default
- from sqlalchemy.engine import reflection
- from sqlalchemy.sql import compiler
- from sqlalchemy.sql import expression
- from sqlalchemy.types import BIGINT
- from sqlalchemy.types import BLOB
- from sqlalchemy.types import DATE
- from sqlalchemy.types import FLOAT
- from sqlalchemy.types import INTEGER
- from sqlalchemy.types import Integer
- from sqlalchemy.types import NUMERIC
- from sqlalchemy.types import SMALLINT
- from sqlalchemy.types import TEXT
- from sqlalchemy.types import TIME
- from sqlalchemy.types import TIMESTAMP
- 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",
- ]
- )
- class _StringType(sqltypes.String):
- """Base for Firebird string types."""
- def __init__(self, charset=None, **kw):
- self.charset = charset
- super(_StringType, self).__init__(**kw)
- class VARCHAR(_StringType, sqltypes.VARCHAR):
- """Firebird VARCHAR type"""
- __visit_name__ = "VARCHAR"
- def __init__(self, length=None, **kwargs):
- super(VARCHAR, self).__init__(length=length, **kwargs)
- class CHAR(_StringType, sqltypes.CHAR):
- """Firebird CHAR type"""
- __visit_name__ = "CHAR"
- def __init__(self, length=None, **kwargs):
- super(CHAR, self).__init__(length=length, **kwargs)
- class _FBDateTime(sqltypes.DateTime):
- def bind_processor(self, dialect):
- def process(value):
- if type(value) == datetime.date:
- return datetime.datetime(value.year, value.month, value.day)
- else:
- return value
- return process
- colspecs = {sqltypes.DateTime: _FBDateTime}
- ischema_names = {
- "SHORT": SMALLINT,
- "LONG": INTEGER,
- "QUAD": FLOAT,
- "FLOAT": FLOAT,
- "DATE": DATE,
- "TIME": TIME,
- "TEXT": TEXT,
- "INT64": BIGINT,
- "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_, **kw):
- return self.visit_SMALLINT(type_, **kw)
- def visit_datetime(self, type_, **kw):
- return self.visit_TIMESTAMP(type_, **kw)
- def visit_TEXT(self, type_, **kw):
- return "BLOB SUB_TYPE 1"
- def visit_BLOB(self, type_, **kw):
- return "BLOB SUB_TYPE 0"
- def _extend_string(self, type_, basic):
- charset = getattr(type_, "charset", None)
- if charset is None:
- return basic
- else:
- return "%s CHARACTER SET %s" % (basic, charset)
- def visit_CHAR(self, type_, **kw):
- basic = super(FBTypeCompiler, self).visit_CHAR(type_, **kw)
- return self._extend_string(type_, basic)
- def visit_VARCHAR(self, type_, **kw):
- if not type_.length:
- raise exc.CompileError(
- "VARCHAR requires a length on dialect %s" % self.dialect.name
- )
- basic = super(FBTypeCompiler, self).visit_VARCHAR(type_, **kw)
- return self._extend_string(type_, basic)
- class FBCompiler(sql.compiler.SQLCompiler):
- """Firebird specific idiosyncrasies"""
- ansi_bind_rules = True
- # def visit_contains_op_binary(self, binary, operator, **kw):
- # cant use CONTAINING b.c. it's case insensitive.
- # def visit_not_contains_op_binary(self, binary, operator, **kw):
- # cant use NOT CONTAINING b.c. it's case insensitive.
- def visit_now_func(self, fn, **kw):
- return "CURRENT_TIMESTAMP"
- def visit_startswith_op_binary(self, binary, operator, **kw):
- return "%s STARTING WITH %s" % (
- binary.left._compiler_dispatch(self, **kw),
- binary.right._compiler_dispatch(self, **kw),
- )
- def visit_not_startswith_op_binary(self, binary, operator, **kw):
- return "%s NOT STARTING WITH %s" % (
- binary.left._compiler_dispatch(self, **kw),
- binary.right._compiler_dispatch(self, **kw),
- )
- def visit_mod_binary(self, binary, operator, **kw):
- return "mod(%s, %s)" % (
- self.process(binary.left, **kw),
- self.process(binary.right, **kw),
- )
- 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._truncated_label)
- and self._truncated_identifier("alias", alias.name)
- or alias.name
- )
- return (
- self.process(alias.element, asfrom=asfrom, **kwargs)
- + " "
- + self.preparer.format_alias(alias, alias_name)
- )
- else:
- return self.process(alias.element, **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):
- # TODO: this probably will need to be
- # narrowed to a fixed list, some no-arg functions
- # may require parens - see similar example in the oracle
- # dialect
- if func.clauses is not None and len(func.clauses):
- return self.process(func.clause_expr, **kw)
- else:
- return ""
- def default_from(self):
- return " FROM rdb$database"
- def visit_sequence(self, seq, **kw):
- return "gen_id(%s, 1)" % self.preparer.format_sequence(seq)
- def get_select_precolumns(self, select, **kw):
- """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_clause is not None:
- result += "FIRST %s " % self.process(select._limit_clause, **kw)
- if select._offset_clause is not None:
- result += "SKIP %s " % self.process(select._offset_clause, **kw)
- result += super(FBCompiler, self).get_select_precolumns(select, **kw)
- return result
- def limit_clause(self, select, **kw):
- """Already taken care of in the `get_select_precolumns` method."""
- return ""
- def returning_clause(self, stmt, returning_cols):
- columns = [
- self._label_returning_column(stmt, c)
- for c in expression._select_iterables(returning_cols)
- ]
- return "RETURNING " + ", ".join(columns)
- class FBDDLCompiler(sql.compiler.DDLCompiler):
- """Firebird syntactic idiosyncrasies"""
- def visit_create_sequence(self, create):
- """Generate a ``CREATE GENERATOR`` statement for the sequence."""
- # no syntax for these
- # https://www.firebirdsql.org/manual/generatorguide-sqlsyntax.html
- if create.element.start is not None:
- raise NotImplementedError(
- "Firebird SEQUENCE doesn't support START WITH"
- )
- if create.element.increment is not None:
- raise NotImplementedError(
- "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
- )
- def visit_computed_column(self, generated):
- if generated.persisted is not None:
- raise exc.CompileError(
- "Firebird computed columns do not support a persistence "
- "method setting; set the 'persisted' flag to None for "
- "Firebird support."
- )
- return "GENERATED ALWAYS AS (%s)" % self.sql_compiler.process(
- generated.sqltext, include_table=False, literal_binds=True
- )
- class FBIdentifierPreparer(sql.compiler.IdentifierPreparer):
- """Install Firebird specific reserved words."""
- reserved_words = RESERVED_WORDS
- illegal_initial_characters = compiler.ILLEGAL_INITIAL_CHARACTERS.union(
- ["_"]
- )
- def __init__(self, dialect):
- super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True)
- class FBExecutionContext(default.DefaultExecutionContext):
- def fire_sequence(self, seq, type_):
- """Get the next value from the sequence using ``gen_id()``."""
- return self._execute_scalar(
- "SELECT gen_id(%s, 1) FROM rdb$database"
- % self.identifier_preparer.format_sequence(seq),
- type_,
- )
- class FBDialect(default.DefaultDialect):
- """Firebird dialect"""
- name = "firebird"
- supports_statement_cache = True
- 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
- construct_arguments = []
- # defaults to dialect ver. 3,
- # will be autodetected off upon
- # first connect
- _version_two = True
- def __init__(self, *args, **kwargs):
- util.warn_deprecated(
- "The firebird dialect is deprecated and will be removed "
- "in a future version. This dialect is superseded by the external "
- "dialect https://github.com/pauldex/sqlalchemy-firebird.",
- version="1.4",
- )
- super(FBDialect, self).__init__(*args, **kwargs)
- def initialize(self, connection):
- super(FBDialect, self).initialize(connection)
- self._version_two = (
- "firebird" in self.server_version_info
- and self.server_version_info >= (2,)
- ) or (
- "interbase" in self.server_version_info
- and self.server_version_info >= (6,)
- )
- 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}
- self.implicit_returning = self._version_two and self.__dict__.get(
- "implicit_returning", True
- )
- def has_table(self, connection, table_name, schema=None):
- """Return ``True`` if the given table exists, ignoring
- the `schema`."""
- self._ensure_has_table_connection(connection)
- tblqry = """
- SELECT 1 AS has_table FROM rdb$database
- WHERE EXISTS (SELECT rdb$relation_name
- FROM rdb$relations
- WHERE rdb$relation_name=?)
- """
- c = connection.exec_driver_sql(
- 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 AS has_sequence FROM rdb$database
- WHERE EXISTS (SELECT rdb$generator_name
- FROM rdb$generators
- WHERE rdb$generator_name=?)
- """
- c = connection.exec_driver_sql(
- genqry, [self.denormalize_name(sequence_name)]
- )
- return c.first() is not None
- @reflection.cache
- def get_table_names(self, connection, schema=None, **kw):
- # there are two queries commonly mentioned for this.
- # this one, using view_blr, is at the Firebird FAQ among other places:
- # https://www.firebirdfaq.org/faq174/
- s = """
- select rdb$relation_name
- from rdb$relations
- where rdb$view_blr is null
- and (rdb$system_flag is null or rdb$system_flag = 0);
- """
- # the other query is this one. It's not clear if there's really
- # any difference between these two. This link:
- # https://www.alberton.info/firebird_sql_meta_info.html#.Ur3vXfZGni8
- # states them as interchangeable. Some discussion at [ticket:2898]
- # 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.exec_driver_sql(s)
- ]
- @reflection.cache
- def get_view_names(self, connection, schema=None, **kw):
- # see https://www.firebirdfaq.org/faq174/
- s = """
- select rdb$relation_name
- from rdb$relations
- where rdb$view_blr is not null
- and (rdb$system_flag is null or rdb$system_flag = 0);
- """
- return [
- self.normalize_name(row[0])
- for row in connection.exec_driver_sql(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.exec_driver_sql(
- qry, [self.denormalize_name(view_name)]
- )
- row = rp.first()
- if row:
- return row["view_source"]
- else:
- return None
- @reflection.cache
- def get_pk_constraint(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.exec_driver_sql(keyqry, ["PRIMARY KEY", tablename])
- pkfields = [self.normalize_name(r["fname"]) for r in c.fetchall()]
- return {"constrained_columns": pkfields, "name": None}
- @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.exec_driver_sql(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 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
- pk_constraint = self.get_pk_constraint(connection, table_name)
- pkey_cols = pk_constraint["constrained_columns"]
- tablename = self.denormalize_name(table_name)
- # get all of the fields for this table
- c = connection.exec_driver_sql(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 issubclass(coltype, Integer) and row["fprec"] != 0:
- coltype = NUMERIC(
- 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()
- # 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
- # and it may also be lower case
- # (see also https://tracker.firebirdsql.org/browse/CORE-356)
- defexpr = row["fdefault"].lstrip()
- assert defexpr[:8].rstrip().upper() == "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,
- "autoincrement": "auto",
- }
- 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.exec_driver_sql(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 list(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, ic.rdb$field_position
- """
- c = connection.exec_driver_sql(
- 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 list(indexes.values())
|