You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

990 lines
30KB

  1. # firebird/base.py
  2. # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  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. r"""
  8. .. dialect:: firebird
  9. :name: Firebird
  10. .. note::
  11. The Firebird dialect within SQLAlchemy **is not currently supported**.
  12. It is not tested within continuous integration and is likely to have
  13. many issues and caveats not currently handled. Consider using the
  14. `external dialect <https://github.com/pauldex/sqlalchemy-firebird>`_
  15. instead.
  16. .. deprecated:: 1.4 The internal Firebird dialect is deprecated and will be
  17. removed in a future version. Use the external dialect.
  18. Firebird Dialects
  19. -----------------
  20. Firebird offers two distinct dialects_ (not to be confused with a
  21. SQLAlchemy ``Dialect``):
  22. dialect 1
  23. This is the old syntax and behaviour, inherited from Interbase pre-6.0.
  24. dialect 3
  25. This is the newer and supported syntax, introduced in Interbase 6.0.
  26. The SQLAlchemy Firebird dialect detects these versions and
  27. adjusts its representation of SQL accordingly. However,
  28. support for dialect 1 is not well tested and probably has
  29. incompatibilities.
  30. Locking Behavior
  31. ----------------
  32. Firebird locks tables aggressively. For this reason, a DROP TABLE may
  33. hang until other transactions are released. SQLAlchemy does its best
  34. to release transactions as quickly as possible. The most common cause
  35. of hanging transactions is a non-fully consumed result set, i.e.::
  36. result = engine.execute(text("select * from table"))
  37. row = result.fetchone()
  38. return
  39. Where above, the ``CursorResult`` has not been fully consumed. The
  40. connection will be returned to the pool and the transactional state
  41. rolled back once the Python garbage collector reclaims the objects
  42. which hold onto the connection, which often occurs asynchronously.
  43. The above use case can be alleviated by calling ``first()`` on the
  44. ``CursorResult`` which will fetch the first row and immediately close
  45. all remaining cursor/connection resources.
  46. RETURNING support
  47. -----------------
  48. Firebird 2.0 supports returning a result set from inserts, and 2.1
  49. extends that to deletes and updates. This is generically exposed by
  50. the SQLAlchemy ``returning()`` method, such as::
  51. # INSERT..RETURNING
  52. result = table.insert().returning(table.c.col1, table.c.col2).\
  53. values(name='foo')
  54. print(result.fetchall())
  55. # UPDATE..RETURNING
  56. raises = empl.update().returning(empl.c.id, empl.c.salary).\
  57. where(empl.c.sales>100).\
  58. values(dict(salary=empl.c.salary * 1.1))
  59. print(raises.fetchall())
  60. .. _dialects: http://mc-computing.com/Databases/Firebird/SQL_Dialect.html
  61. """
  62. import datetime
  63. from sqlalchemy import exc
  64. from sqlalchemy import sql
  65. from sqlalchemy import types as sqltypes
  66. from sqlalchemy import util
  67. from sqlalchemy.engine import default
  68. from sqlalchemy.engine import reflection
  69. from sqlalchemy.sql import compiler
  70. from sqlalchemy.sql import expression
  71. from sqlalchemy.types import BIGINT
  72. from sqlalchemy.types import BLOB
  73. from sqlalchemy.types import DATE
  74. from sqlalchemy.types import FLOAT
  75. from sqlalchemy.types import INTEGER
  76. from sqlalchemy.types import Integer
  77. from sqlalchemy.types import NUMERIC
  78. from sqlalchemy.types import SMALLINT
  79. from sqlalchemy.types import TEXT
  80. from sqlalchemy.types import TIME
  81. from sqlalchemy.types import TIMESTAMP
  82. RESERVED_WORDS = set(
  83. [
  84. "active",
  85. "add",
  86. "admin",
  87. "after",
  88. "all",
  89. "alter",
  90. "and",
  91. "any",
  92. "as",
  93. "asc",
  94. "ascending",
  95. "at",
  96. "auto",
  97. "avg",
  98. "before",
  99. "begin",
  100. "between",
  101. "bigint",
  102. "bit_length",
  103. "blob",
  104. "both",
  105. "by",
  106. "case",
  107. "cast",
  108. "char",
  109. "character",
  110. "character_length",
  111. "char_length",
  112. "check",
  113. "close",
  114. "collate",
  115. "column",
  116. "commit",
  117. "committed",
  118. "computed",
  119. "conditional",
  120. "connect",
  121. "constraint",
  122. "containing",
  123. "count",
  124. "create",
  125. "cross",
  126. "cstring",
  127. "current",
  128. "current_connection",
  129. "current_date",
  130. "current_role",
  131. "current_time",
  132. "current_timestamp",
  133. "current_transaction",
  134. "current_user",
  135. "cursor",
  136. "database",
  137. "date",
  138. "day",
  139. "dec",
  140. "decimal",
  141. "declare",
  142. "default",
  143. "delete",
  144. "desc",
  145. "descending",
  146. "disconnect",
  147. "distinct",
  148. "do",
  149. "domain",
  150. "double",
  151. "drop",
  152. "else",
  153. "end",
  154. "entry_point",
  155. "escape",
  156. "exception",
  157. "execute",
  158. "exists",
  159. "exit",
  160. "external",
  161. "extract",
  162. "fetch",
  163. "file",
  164. "filter",
  165. "float",
  166. "for",
  167. "foreign",
  168. "from",
  169. "full",
  170. "function",
  171. "gdscode",
  172. "generator",
  173. "gen_id",
  174. "global",
  175. "grant",
  176. "group",
  177. "having",
  178. "hour",
  179. "if",
  180. "in",
  181. "inactive",
  182. "index",
  183. "inner",
  184. "input_type",
  185. "insensitive",
  186. "insert",
  187. "int",
  188. "integer",
  189. "into",
  190. "is",
  191. "isolation",
  192. "join",
  193. "key",
  194. "leading",
  195. "left",
  196. "length",
  197. "level",
  198. "like",
  199. "long",
  200. "lower",
  201. "manual",
  202. "max",
  203. "maximum_segment",
  204. "merge",
  205. "min",
  206. "minute",
  207. "module_name",
  208. "month",
  209. "names",
  210. "national",
  211. "natural",
  212. "nchar",
  213. "no",
  214. "not",
  215. "null",
  216. "numeric",
  217. "octet_length",
  218. "of",
  219. "on",
  220. "only",
  221. "open",
  222. "option",
  223. "or",
  224. "order",
  225. "outer",
  226. "output_type",
  227. "overflow",
  228. "page",
  229. "pages",
  230. "page_size",
  231. "parameter",
  232. "password",
  233. "plan",
  234. "position",
  235. "post_event",
  236. "precision",
  237. "primary",
  238. "privileges",
  239. "procedure",
  240. "protected",
  241. "rdb$db_key",
  242. "read",
  243. "real",
  244. "record_version",
  245. "recreate",
  246. "recursive",
  247. "references",
  248. "release",
  249. "reserv",
  250. "reserving",
  251. "retain",
  252. "returning_values",
  253. "returns",
  254. "revoke",
  255. "right",
  256. "rollback",
  257. "rows",
  258. "row_count",
  259. "savepoint",
  260. "schema",
  261. "second",
  262. "segment",
  263. "select",
  264. "sensitive",
  265. "set",
  266. "shadow",
  267. "shared",
  268. "singular",
  269. "size",
  270. "smallint",
  271. "snapshot",
  272. "some",
  273. "sort",
  274. "sqlcode",
  275. "stability",
  276. "start",
  277. "starting",
  278. "starts",
  279. "statistics",
  280. "sub_type",
  281. "sum",
  282. "suspend",
  283. "table",
  284. "then",
  285. "time",
  286. "timestamp",
  287. "to",
  288. "trailing",
  289. "transaction",
  290. "trigger",
  291. "trim",
  292. "uncommitted",
  293. "union",
  294. "unique",
  295. "update",
  296. "upper",
  297. "user",
  298. "using",
  299. "value",
  300. "values",
  301. "varchar",
  302. "variable",
  303. "varying",
  304. "view",
  305. "wait",
  306. "when",
  307. "where",
  308. "while",
  309. "with",
  310. "work",
  311. "write",
  312. "year",
  313. ]
  314. )
  315. class _StringType(sqltypes.String):
  316. """Base for Firebird string types."""
  317. def __init__(self, charset=None, **kw):
  318. self.charset = charset
  319. super(_StringType, self).__init__(**kw)
  320. class VARCHAR(_StringType, sqltypes.VARCHAR):
  321. """Firebird VARCHAR type"""
  322. __visit_name__ = "VARCHAR"
  323. def __init__(self, length=None, **kwargs):
  324. super(VARCHAR, self).__init__(length=length, **kwargs)
  325. class CHAR(_StringType, sqltypes.CHAR):
  326. """Firebird CHAR type"""
  327. __visit_name__ = "CHAR"
  328. def __init__(self, length=None, **kwargs):
  329. super(CHAR, self).__init__(length=length, **kwargs)
  330. class _FBDateTime(sqltypes.DateTime):
  331. def bind_processor(self, dialect):
  332. def process(value):
  333. if type(value) == datetime.date:
  334. return datetime.datetime(value.year, value.month, value.day)
  335. else:
  336. return value
  337. return process
  338. colspecs = {sqltypes.DateTime: _FBDateTime}
  339. ischema_names = {
  340. "SHORT": SMALLINT,
  341. "LONG": INTEGER,
  342. "QUAD": FLOAT,
  343. "FLOAT": FLOAT,
  344. "DATE": DATE,
  345. "TIME": TIME,
  346. "TEXT": TEXT,
  347. "INT64": BIGINT,
  348. "DOUBLE": FLOAT,
  349. "TIMESTAMP": TIMESTAMP,
  350. "VARYING": VARCHAR,
  351. "CSTRING": CHAR,
  352. "BLOB": BLOB,
  353. }
  354. # TODO: date conversion types (should be implemented as _FBDateTime,
  355. # _FBDate, etc. as bind/result functionality is required)
  356. class FBTypeCompiler(compiler.GenericTypeCompiler):
  357. def visit_boolean(self, type_, **kw):
  358. return self.visit_SMALLINT(type_, **kw)
  359. def visit_datetime(self, type_, **kw):
  360. return self.visit_TIMESTAMP(type_, **kw)
  361. def visit_TEXT(self, type_, **kw):
  362. return "BLOB SUB_TYPE 1"
  363. def visit_BLOB(self, type_, **kw):
  364. return "BLOB SUB_TYPE 0"
  365. def _extend_string(self, type_, basic):
  366. charset = getattr(type_, "charset", None)
  367. if charset is None:
  368. return basic
  369. else:
  370. return "%s CHARACTER SET %s" % (basic, charset)
  371. def visit_CHAR(self, type_, **kw):
  372. basic = super(FBTypeCompiler, self).visit_CHAR(type_, **kw)
  373. return self._extend_string(type_, basic)
  374. def visit_VARCHAR(self, type_, **kw):
  375. if not type_.length:
  376. raise exc.CompileError(
  377. "VARCHAR requires a length on dialect %s" % self.dialect.name
  378. )
  379. basic = super(FBTypeCompiler, self).visit_VARCHAR(type_, **kw)
  380. return self._extend_string(type_, basic)
  381. class FBCompiler(sql.compiler.SQLCompiler):
  382. """Firebird specific idiosyncrasies"""
  383. ansi_bind_rules = True
  384. # def visit_contains_op_binary(self, binary, operator, **kw):
  385. # cant use CONTAINING b.c. it's case insensitive.
  386. # def visit_not_contains_op_binary(self, binary, operator, **kw):
  387. # cant use NOT CONTAINING b.c. it's case insensitive.
  388. def visit_now_func(self, fn, **kw):
  389. return "CURRENT_TIMESTAMP"
  390. def visit_startswith_op_binary(self, binary, operator, **kw):
  391. return "%s STARTING WITH %s" % (
  392. binary.left._compiler_dispatch(self, **kw),
  393. binary.right._compiler_dispatch(self, **kw),
  394. )
  395. def visit_not_startswith_op_binary(self, binary, operator, **kw):
  396. return "%s NOT STARTING WITH %s" % (
  397. binary.left._compiler_dispatch(self, **kw),
  398. binary.right._compiler_dispatch(self, **kw),
  399. )
  400. def visit_mod_binary(self, binary, operator, **kw):
  401. return "mod(%s, %s)" % (
  402. self.process(binary.left, **kw),
  403. self.process(binary.right, **kw),
  404. )
  405. def visit_alias(self, alias, asfrom=False, **kwargs):
  406. if self.dialect._version_two:
  407. return super(FBCompiler, self).visit_alias(
  408. alias, asfrom=asfrom, **kwargs
  409. )
  410. else:
  411. # Override to not use the AS keyword which FB 1.5 does not like
  412. if asfrom:
  413. alias_name = (
  414. isinstance(alias.name, expression._truncated_label)
  415. and self._truncated_identifier("alias", alias.name)
  416. or alias.name
  417. )
  418. return (
  419. self.process(alias.element, asfrom=asfrom, **kwargs)
  420. + " "
  421. + self.preparer.format_alias(alias, alias_name)
  422. )
  423. else:
  424. return self.process(alias.element, **kwargs)
  425. def visit_substring_func(self, func, **kw):
  426. s = self.process(func.clauses.clauses[0])
  427. start = self.process(func.clauses.clauses[1])
  428. if len(func.clauses.clauses) > 2:
  429. length = self.process(func.clauses.clauses[2])
  430. return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length)
  431. else:
  432. return "SUBSTRING(%s FROM %s)" % (s, start)
  433. def visit_length_func(self, function, **kw):
  434. if self.dialect._version_two:
  435. return "char_length" + self.function_argspec(function)
  436. else:
  437. return "strlen" + self.function_argspec(function)
  438. visit_char_length_func = visit_length_func
  439. def function_argspec(self, func, **kw):
  440. # TODO: this probably will need to be
  441. # narrowed to a fixed list, some no-arg functions
  442. # may require parens - see similar example in the oracle
  443. # dialect
  444. if func.clauses is not None and len(func.clauses):
  445. return self.process(func.clause_expr, **kw)
  446. else:
  447. return ""
  448. def default_from(self):
  449. return " FROM rdb$database"
  450. def visit_sequence(self, seq, **kw):
  451. return "gen_id(%s, 1)" % self.preparer.format_sequence(seq)
  452. def get_select_precolumns(self, select, **kw):
  453. """Called when building a ``SELECT`` statement, position is just
  454. before column list Firebird puts the limit and offset right
  455. after the ``SELECT``...
  456. """
  457. result = ""
  458. if select._limit_clause is not None:
  459. result += "FIRST %s " % self.process(select._limit_clause, **kw)
  460. if select._offset_clause is not None:
  461. result += "SKIP %s " % self.process(select._offset_clause, **kw)
  462. result += super(FBCompiler, self).get_select_precolumns(select, **kw)
  463. return result
  464. def limit_clause(self, select, **kw):
  465. """Already taken care of in the `get_select_precolumns` method."""
  466. return ""
  467. def returning_clause(self, stmt, returning_cols):
  468. columns = [
  469. self._label_select_column(None, c, True, False, {})
  470. for c in expression._select_iterables(returning_cols)
  471. ]
  472. return "RETURNING " + ", ".join(columns)
  473. class FBDDLCompiler(sql.compiler.DDLCompiler):
  474. """Firebird syntactic idiosyncrasies"""
  475. def visit_create_sequence(self, create):
  476. """Generate a ``CREATE GENERATOR`` statement for the sequence."""
  477. # no syntax for these
  478. # http://www.firebirdsql.org/manual/generatorguide-sqlsyntax.html
  479. if create.element.start is not None:
  480. raise NotImplementedError(
  481. "Firebird SEQUENCE doesn't support START WITH"
  482. )
  483. if create.element.increment is not None:
  484. raise NotImplementedError(
  485. "Firebird SEQUENCE doesn't support INCREMENT BY"
  486. )
  487. if self.dialect._version_two:
  488. return "CREATE SEQUENCE %s" % self.preparer.format_sequence(
  489. create.element
  490. )
  491. else:
  492. return "CREATE GENERATOR %s" % self.preparer.format_sequence(
  493. create.element
  494. )
  495. def visit_drop_sequence(self, drop):
  496. """Generate a ``DROP GENERATOR`` statement for the sequence."""
  497. if self.dialect._version_two:
  498. return "DROP SEQUENCE %s" % self.preparer.format_sequence(
  499. drop.element
  500. )
  501. else:
  502. return "DROP GENERATOR %s" % self.preparer.format_sequence(
  503. drop.element
  504. )
  505. def visit_computed_column(self, generated):
  506. if generated.persisted is not None:
  507. raise exc.CompileError(
  508. "Firebird computed columns do not support a persistence "
  509. "method setting; set the 'persisted' flag to None for "
  510. "Firebird support."
  511. )
  512. return "GENERATED ALWAYS AS (%s)" % self.sql_compiler.process(
  513. generated.sqltext, include_table=False, literal_binds=True
  514. )
  515. class FBIdentifierPreparer(sql.compiler.IdentifierPreparer):
  516. """Install Firebird specific reserved words."""
  517. reserved_words = RESERVED_WORDS
  518. illegal_initial_characters = compiler.ILLEGAL_INITIAL_CHARACTERS.union(
  519. ["_"]
  520. )
  521. def __init__(self, dialect):
  522. super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True)
  523. class FBExecutionContext(default.DefaultExecutionContext):
  524. def fire_sequence(self, seq, type_):
  525. """Get the next value from the sequence using ``gen_id()``."""
  526. return self._execute_scalar(
  527. "SELECT gen_id(%s, 1) FROM rdb$database"
  528. % self.identifier_preparer.format_sequence(seq),
  529. type_,
  530. )
  531. class FBDialect(default.DefaultDialect):
  532. """Firebird dialect"""
  533. name = "firebird"
  534. supports_statement_cache = True
  535. max_identifier_length = 31
  536. supports_sequences = True
  537. sequences_optional = False
  538. supports_default_values = True
  539. postfetch_lastrowid = False
  540. supports_native_boolean = False
  541. requires_name_normalize = True
  542. supports_empty_insert = False
  543. statement_compiler = FBCompiler
  544. ddl_compiler = FBDDLCompiler
  545. preparer = FBIdentifierPreparer
  546. type_compiler = FBTypeCompiler
  547. execution_ctx_cls = FBExecutionContext
  548. colspecs = colspecs
  549. ischema_names = ischema_names
  550. construct_arguments = []
  551. # defaults to dialect ver. 3,
  552. # will be autodetected off upon
  553. # first connect
  554. _version_two = True
  555. def __init__(self, *args, **kwargs):
  556. util.warn_deprecated(
  557. "The firebird dialect is deprecated and will be removed "
  558. "in a future version. This dialect is superseded by the external "
  559. "dialect https://github.com/pauldex/sqlalchemy-firebird.",
  560. version="1.4",
  561. )
  562. super(FBDialect, self).__init__(*args, **kwargs)
  563. def initialize(self, connection):
  564. super(FBDialect, self).initialize(connection)
  565. self._version_two = (
  566. "firebird" in self.server_version_info
  567. and self.server_version_info >= (2,)
  568. ) or (
  569. "interbase" in self.server_version_info
  570. and self.server_version_info >= (6,)
  571. )
  572. if not self._version_two:
  573. # TODO: whatever other pre < 2.0 stuff goes here
  574. self.ischema_names = ischema_names.copy()
  575. self.ischema_names["TIMESTAMP"] = sqltypes.DATE
  576. self.colspecs = {sqltypes.DateTime: sqltypes.DATE}
  577. self.implicit_returning = self._version_two and self.__dict__.get(
  578. "implicit_returning", True
  579. )
  580. def has_table(self, connection, table_name, schema=None):
  581. """Return ``True`` if the given table exists, ignoring
  582. the `schema`."""
  583. self._ensure_has_table_connection(connection)
  584. tblqry = """
  585. SELECT 1 AS has_table FROM rdb$database
  586. WHERE EXISTS (SELECT rdb$relation_name
  587. FROM rdb$relations
  588. WHERE rdb$relation_name=?)
  589. """
  590. c = connection.exec_driver_sql(
  591. tblqry, [self.denormalize_name(table_name)]
  592. )
  593. return c.first() is not None
  594. def has_sequence(self, connection, sequence_name, schema=None):
  595. """Return ``True`` if the given sequence (generator) exists."""
  596. genqry = """
  597. SELECT 1 AS has_sequence FROM rdb$database
  598. WHERE EXISTS (SELECT rdb$generator_name
  599. FROM rdb$generators
  600. WHERE rdb$generator_name=?)
  601. """
  602. c = connection.exec_driver_sql(
  603. genqry, [self.denormalize_name(sequence_name)]
  604. )
  605. return c.first() is not None
  606. @reflection.cache
  607. def get_table_names(self, connection, schema=None, **kw):
  608. # there are two queries commonly mentioned for this.
  609. # this one, using view_blr, is at the Firebird FAQ among other places:
  610. # http://www.firebirdfaq.org/faq174/
  611. s = """
  612. select rdb$relation_name
  613. from rdb$relations
  614. where rdb$view_blr is null
  615. and (rdb$system_flag is null or rdb$system_flag = 0);
  616. """
  617. # the other query is this one. It's not clear if there's really
  618. # any difference between these two. This link:
  619. # http://www.alberton.info/firebird_sql_meta_info.html#.Ur3vXfZGni8
  620. # states them as interchangeable. Some discussion at [ticket:2898]
  621. # SELECT DISTINCT rdb$relation_name
  622. # FROM rdb$relation_fields
  623. # WHERE rdb$system_flag=0 AND rdb$view_context IS NULL
  624. return [
  625. self.normalize_name(row[0])
  626. for row in connection.exec_driver_sql(s)
  627. ]
  628. @reflection.cache
  629. def get_view_names(self, connection, schema=None, **kw):
  630. # see http://www.firebirdfaq.org/faq174/
  631. s = """
  632. select rdb$relation_name
  633. from rdb$relations
  634. where rdb$view_blr is not null
  635. and (rdb$system_flag is null or rdb$system_flag = 0);
  636. """
  637. return [
  638. self.normalize_name(row[0])
  639. for row in connection.exec_driver_sql(s)
  640. ]
  641. @reflection.cache
  642. def get_view_definition(self, connection, view_name, schema=None, **kw):
  643. qry = """
  644. SELECT rdb$view_source AS view_source
  645. FROM rdb$relations
  646. WHERE rdb$relation_name=?
  647. """
  648. rp = connection.exec_driver_sql(
  649. qry, [self.denormalize_name(view_name)]
  650. )
  651. row = rp.first()
  652. if row:
  653. return row["view_source"]
  654. else:
  655. return None
  656. @reflection.cache
  657. def get_pk_constraint(self, connection, table_name, schema=None, **kw):
  658. # Query to extract the PK/FK constrained fields of the given table
  659. keyqry = """
  660. SELECT se.rdb$field_name AS fname
  661. FROM rdb$relation_constraints rc
  662. JOIN rdb$index_segments se ON rc.rdb$index_name=se.rdb$index_name
  663. WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=?
  664. """
  665. tablename = self.denormalize_name(table_name)
  666. # get primary key fields
  667. c = connection.exec_driver_sql(keyqry, ["PRIMARY KEY", tablename])
  668. pkfields = [self.normalize_name(r["fname"]) for r in c.fetchall()]
  669. return {"constrained_columns": pkfields, "name": None}
  670. @reflection.cache
  671. def get_column_sequence(
  672. self, connection, table_name, column_name, schema=None, **kw
  673. ):
  674. tablename = self.denormalize_name(table_name)
  675. colname = self.denormalize_name(column_name)
  676. # Heuristic-query to determine the generator associated to a PK field
  677. genqry = """
  678. SELECT trigdep.rdb$depended_on_name AS fgenerator
  679. FROM rdb$dependencies tabdep
  680. JOIN rdb$dependencies trigdep
  681. ON tabdep.rdb$dependent_name=trigdep.rdb$dependent_name
  682. AND trigdep.rdb$depended_on_type=14
  683. AND trigdep.rdb$dependent_type=2
  684. JOIN rdb$triggers trig ON
  685. trig.rdb$trigger_name=tabdep.rdb$dependent_name
  686. WHERE tabdep.rdb$depended_on_name=?
  687. AND tabdep.rdb$depended_on_type=0
  688. AND trig.rdb$trigger_type=1
  689. AND tabdep.rdb$field_name=?
  690. AND (SELECT count(*)
  691. FROM rdb$dependencies trigdep2
  692. WHERE trigdep2.rdb$dependent_name = trigdep.rdb$dependent_name) = 2
  693. """
  694. genr = connection.exec_driver_sql(genqry, [tablename, colname]).first()
  695. if genr is not None:
  696. return dict(name=self.normalize_name(genr["fgenerator"]))
  697. @reflection.cache
  698. def get_columns(self, connection, table_name, schema=None, **kw):
  699. # Query to extract the details of all the fields of the given table
  700. tblqry = """
  701. SELECT r.rdb$field_name AS fname,
  702. r.rdb$null_flag AS null_flag,
  703. t.rdb$type_name AS ftype,
  704. f.rdb$field_sub_type AS stype,
  705. f.rdb$field_length/
  706. COALESCE(cs.rdb$bytes_per_character,1) AS flen,
  707. f.rdb$field_precision AS fprec,
  708. f.rdb$field_scale AS fscale,
  709. COALESCE(r.rdb$default_source,
  710. f.rdb$default_source) AS fdefault
  711. FROM rdb$relation_fields r
  712. JOIN rdb$fields f ON r.rdb$field_source=f.rdb$field_name
  713. JOIN rdb$types t
  714. ON t.rdb$type=f.rdb$field_type AND
  715. t.rdb$field_name='RDB$FIELD_TYPE'
  716. LEFT JOIN rdb$character_sets cs ON
  717. f.rdb$character_set_id=cs.rdb$character_set_id
  718. WHERE f.rdb$system_flag=0 AND r.rdb$relation_name=?
  719. ORDER BY r.rdb$field_position
  720. """
  721. # get the PK, used to determine the eventual associated sequence
  722. pk_constraint = self.get_pk_constraint(connection, table_name)
  723. pkey_cols = pk_constraint["constrained_columns"]
  724. tablename = self.denormalize_name(table_name)
  725. # get all of the fields for this table
  726. c = connection.exec_driver_sql(tblqry, [tablename])
  727. cols = []
  728. while True:
  729. row = c.fetchone()
  730. if row is None:
  731. break
  732. name = self.normalize_name(row["fname"])
  733. orig_colname = row["fname"]
  734. # get the data type
  735. colspec = row["ftype"].rstrip()
  736. coltype = self.ischema_names.get(colspec)
  737. if coltype is None:
  738. util.warn(
  739. "Did not recognize type '%s' of column '%s'"
  740. % (colspec, name)
  741. )
  742. coltype = sqltypes.NULLTYPE
  743. elif issubclass(coltype, Integer) and row["fprec"] != 0:
  744. coltype = NUMERIC(
  745. precision=row["fprec"], scale=row["fscale"] * -1
  746. )
  747. elif colspec in ("VARYING", "CSTRING"):
  748. coltype = coltype(row["flen"])
  749. elif colspec == "TEXT":
  750. coltype = TEXT(row["flen"])
  751. elif colspec == "BLOB":
  752. if row["stype"] == 1:
  753. coltype = TEXT()
  754. else:
  755. coltype = BLOB()
  756. else:
  757. coltype = coltype()
  758. # does it have a default value?
  759. defvalue = None
  760. if row["fdefault"] is not None:
  761. # the value comes down as "DEFAULT 'value'": there may be
  762. # more than one whitespace around the "DEFAULT" keyword
  763. # and it may also be lower case
  764. # (see also http://tracker.firebirdsql.org/browse/CORE-356)
  765. defexpr = row["fdefault"].lstrip()
  766. assert defexpr[:8].rstrip().upper() == "DEFAULT", (
  767. "Unrecognized default value: %s" % defexpr
  768. )
  769. defvalue = defexpr[8:].strip()
  770. if defvalue == "NULL":
  771. # Redundant
  772. defvalue = None
  773. col_d = {
  774. "name": name,
  775. "type": coltype,
  776. "nullable": not bool(row["null_flag"]),
  777. "default": defvalue,
  778. "autoincrement": "auto",
  779. }
  780. if orig_colname.lower() == orig_colname:
  781. col_d["quote"] = True
  782. # if the PK is a single field, try to see if its linked to
  783. # a sequence thru a trigger
  784. if len(pkey_cols) == 1 and name == pkey_cols[0]:
  785. seq_d = self.get_column_sequence(connection, tablename, name)
  786. if seq_d is not None:
  787. col_d["sequence"] = seq_d
  788. cols.append(col_d)
  789. return cols
  790. @reflection.cache
  791. def get_foreign_keys(self, connection, table_name, schema=None, **kw):
  792. # Query to extract the details of each UK/FK of the given table
  793. fkqry = """
  794. SELECT rc.rdb$constraint_name AS cname,
  795. cse.rdb$field_name AS fname,
  796. ix2.rdb$relation_name AS targetrname,
  797. se.rdb$field_name AS targetfname
  798. FROM rdb$relation_constraints rc
  799. JOIN rdb$indices ix1 ON ix1.rdb$index_name=rc.rdb$index_name
  800. JOIN rdb$indices ix2 ON ix2.rdb$index_name=ix1.rdb$foreign_key
  801. JOIN rdb$index_segments cse ON
  802. cse.rdb$index_name=ix1.rdb$index_name
  803. JOIN rdb$index_segments se
  804. ON se.rdb$index_name=ix2.rdb$index_name
  805. AND se.rdb$field_position=cse.rdb$field_position
  806. WHERE rc.rdb$constraint_type=? AND rc.rdb$relation_name=?
  807. ORDER BY se.rdb$index_name, se.rdb$field_position
  808. """
  809. tablename = self.denormalize_name(table_name)
  810. c = connection.exec_driver_sql(fkqry, ["FOREIGN KEY", tablename])
  811. fks = util.defaultdict(
  812. lambda: {
  813. "name": None,
  814. "constrained_columns": [],
  815. "referred_schema": None,
  816. "referred_table": None,
  817. "referred_columns": [],
  818. }
  819. )
  820. for row in c:
  821. cname = self.normalize_name(row["cname"])
  822. fk = fks[cname]
  823. if not fk["name"]:
  824. fk["name"] = cname
  825. fk["referred_table"] = self.normalize_name(row["targetrname"])
  826. fk["constrained_columns"].append(self.normalize_name(row["fname"]))
  827. fk["referred_columns"].append(
  828. self.normalize_name(row["targetfname"])
  829. )
  830. return list(fks.values())
  831. @reflection.cache
  832. def get_indexes(self, connection, table_name, schema=None, **kw):
  833. qry = """
  834. SELECT ix.rdb$index_name AS index_name,
  835. ix.rdb$unique_flag AS unique_flag,
  836. ic.rdb$field_name AS field_name
  837. FROM rdb$indices ix
  838. JOIN rdb$index_segments ic
  839. ON ix.rdb$index_name=ic.rdb$index_name
  840. LEFT OUTER JOIN rdb$relation_constraints
  841. ON rdb$relation_constraints.rdb$index_name =
  842. ic.rdb$index_name
  843. WHERE ix.rdb$relation_name=? AND ix.rdb$foreign_key IS NULL
  844. AND rdb$relation_constraints.rdb$constraint_type IS NULL
  845. ORDER BY index_name, ic.rdb$field_position
  846. """
  847. c = connection.exec_driver_sql(
  848. qry, [self.denormalize_name(table_name)]
  849. )
  850. indexes = util.defaultdict(dict)
  851. for row in c:
  852. indexrec = indexes[row["index_name"]]
  853. if "name" not in indexrec:
  854. indexrec["name"] = self.normalize_name(row["index_name"])
  855. indexrec["column_names"] = []
  856. indexrec["unique"] = bool(row["unique_flag"])
  857. indexrec["column_names"].append(
  858. self.normalize_name(row["field_name"])
  859. )
  860. return list(indexes.values())