├── .github └── FUNDING.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _static │ ├── basic.css │ ├── dialog-note.png │ ├── dialog-seealso.png │ ├── dialog-topic.png │ ├── dialog-warning.png │ └── fb-favicon.png ├── changelog.txt ├── conf.py ├── getting-started.txt ├── index.txt ├── license.txt ├── make-docset ├── make.bat ├── python-db-api-compliance.txt ├── ref-config.txt ├── ref-core.txt ├── ref-fbapi.txt ├── ref-hooks.txt ├── ref-intf.txt ├── ref-main.txt ├── ref-types.txt ├── reference.txt ├── requirements.txt └── usage-guide.txt ├── pyproject.toml ├── src └── firebird │ └── driver │ ├── __init__.py │ ├── config.py │ ├── core.py │ ├── fbapi.py │ ├── hooks.py │ ├── interfaces.py │ └── types.py └── tests ├── conftest.py ├── fbtest30-base.fbk ├── fbtest30-src.fbk ├── fbtest30.fdb ├── fbtest40-base.fbk ├── fbtest40.fdb ├── fbtest40.sql ├── fbtest50-base.fbk ├── fbtest50.fdb ├── test_array.py ├── test_blob.py ├── test_charset_conv.py ├── test_connection.py ├── test_cursor.py ├── test_db_createdrop.py ├── test_dbapi_compliance.py ├── test_distributed_trans.py ├── test_events.py ├── test_fb4.py ├── test_hooks.py ├── test_info_providers.py ├── test_insert_data.py ├── test_issues.py ├── test_param_buffers.py ├── test_server.py ├── test_statement.py ├── test_stored_proc.py └── test_transaction.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pcisar] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # https://firebirdsql.org/en/donate/ 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | .hatch/ 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # WingIDE 133 | *.wpr 134 | *.wpu 135 | 136 | # VSCode 137 | .vscode 138 | 139 | # Personal 140 | notes/ 141 | store/ 142 | 143 | # Sphinx build 144 | docs/_build 145 | docs/firebird-driver.docset 146 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | build: 13 | os: "ubuntu-22.04" 14 | tools: 15 | python: "3.11" 16 | 17 | # Optionally set the version of Python and requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The Firebird Project (www.firebirdsql.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebird-driver 2 | 3 | ## Firebird driver for Python 4 | 5 | [![PyPI - Version](https://img.shields.io/pypi/v/firebird-driver.svg)](https://pypi.org/project/firebird-driver) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/firebird-driver.svg)](https://pypi.org/project/firebird-driver) 7 | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) 8 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/firebird-driver)](https://pypi.org/project/firebird-driver) 9 | [![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/pypi/firebird-driver)](https://libraries.io/pypi/firebird-driver) 10 | 11 | This package provides official Python Database API 2.0-compliant driver for the open 12 | source relational database Firebird®. In addition to the minimal feature set of 13 | the standard Python DB API, this driver also exposes the new (interface-based) 14 | client API introduced in Firebird 3, and number of additional extensions and 15 | enhancements for convenient use of Firebird RDBMS. 16 | 17 | ----- 18 | 19 | **Table of Contents** 20 | 21 | - [Installation](#installation) 22 | - [License](#license) 23 | - [Documentation](#documentation) 24 | 25 | ## Installation 26 | 27 | Requires: Firebird 3+ 28 | 29 | ```console 30 | pip install firebird-driver 31 | ``` 32 | See [firebird-lib](https://pypi.org/project/firebird-lib/) package for optional extensions 33 | to this driver. 34 | 35 | ## License 36 | 37 | `firebird-driver` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. 38 | 39 | ## Documentation 40 | 41 | The documentation for this package is available at [https://firebird-driver.readthedocs.io](https://firebird-driver.readthedocs.io) 42 | 43 | ## Running tests 44 | 45 | This project uses [hatch](https://hatch.pypa.io/latest/) , so you can use: 46 | ```console 47 | hatch test 48 | ``` 49 | to run all tests for default Python version (3.11). To run tests for all Python versions 50 | defined in matrix, use `-a` switch. 51 | 52 | This project is using [pytest](https://docs.pytest.org/en/stable/) for testing, and our 53 | tests add several options via `tests/conftest.py`. 54 | 55 | By default, tests are configured to use local Firebird installation via network access. 56 | To use local installation in `embedded` mode, comment out the section: 57 | ``` 58 | [tool.hatch.envs.hatch-test] 59 | extra-args = ["--host=localhost"] 60 | ``` 61 | in `pyproject.toml`. 62 | 63 | You can also use firebird driver configuration file to specify server(s) that should be 64 | used for testing, and then pass `--driver-config` and `--server` options to `pytest`. 65 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* -- main layout ----------------------------------------------------------- */ 2 | 3 | div.clearer { 4 | clear: both; 5 | } 6 | 7 | /* -- relbar ---------------------------------------------------------------- */ 8 | 9 | div.related { 10 | width: 100%; 11 | font-size: 90%; 12 | } 13 | 14 | div.related h3 { 15 | display: none; 16 | } 17 | 18 | div.related ul { 19 | margin: 0; 20 | padding: 0 0 0 10px; 21 | list-style: none; 22 | } 23 | 24 | div.related li { 25 | display: inline; 26 | } 27 | 28 | div.related li.right { 29 | float: right; 30 | margin-right: 5px; 31 | } 32 | 33 | /* -- sidebar --------------------------------------------------------------- */ 34 | 35 | div.sphinxsidebarwrapper { 36 | padding: 10px 5px 0 10px; 37 | } 38 | 39 | div.sphinxsidebar { 40 | float: left; 41 | width: 230px; 42 | margin-left: -100%; 43 | font-size: 90%; 44 | word-wrap: break-word; 45 | overflow-wrap : break-word; 46 | } 47 | 48 | div.sphinxsidebar ul { 49 | list-style: none; 50 | } 51 | 52 | div.sphinxsidebar ul ul, 53 | div.sphinxsidebar ul.want-points { 54 | margin-left: 20px; 55 | list-style: square; 56 | } 57 | 58 | div.sphinxsidebar ul ul { 59 | margin-top: 0; 60 | margin-bottom: 0; 61 | } 62 | 63 | div.sphinxsidebar form { 64 | margin-top: 10px; 65 | } 66 | 67 | div.sphinxsidebar input { 68 | border: 1px solid #98dbcc; 69 | font-family: sans-serif; 70 | font-size: 1em; 71 | } 72 | 73 | div.sphinxsidebar #searchbox input[type="text"] { 74 | float: left; 75 | width: 80%; 76 | padding: 0.25em; 77 | box-sizing: border-box; 78 | } 79 | 80 | div.sphinxsidebar #searchbox input[type="submit"] { 81 | float: left; 82 | width: 20%; 83 | border-left: none; 84 | padding: 0.25em; 85 | box-sizing: border-box; 86 | } 87 | 88 | 89 | img { 90 | border: 0; 91 | max-width: 100%; 92 | } 93 | 94 | /* -- search page ----------------------------------------------------------- */ 95 | 96 | ul.search { 97 | margin: 10px 0 0 20px; 98 | padding: 0; 99 | } 100 | 101 | ul.search li { 102 | padding: 5px 0 5px 20px; 103 | background-image: url(file.png); 104 | background-repeat: no-repeat; 105 | background-position: 0 7px; 106 | } 107 | 108 | ul.search li a { 109 | font-weight: bold; 110 | } 111 | 112 | ul.search li div.context { 113 | color: #888; 114 | margin: 2px 0 0 30px; 115 | text-align: left; 116 | } 117 | 118 | ul.keywordmatches li.goodmatch a { 119 | font-weight: bold; 120 | } 121 | 122 | /* -- index page ------------------------------------------------------------ */ 123 | 124 | table.contentstable { 125 | width: 90%; 126 | margin-left: auto; 127 | margin-right: auto; 128 | } 129 | 130 | table.contentstable p.biglink { 131 | line-height: 150%; 132 | } 133 | 134 | a.biglink { 135 | font-size: 1.3em; 136 | } 137 | 138 | span.linkdescr { 139 | font-style: italic; 140 | padding-top: 5px; 141 | font-size: 90%; 142 | } 143 | 144 | /* -- general index --------------------------------------------------------- */ 145 | 146 | table.indextable { 147 | width: 100%; 148 | } 149 | 150 | table.indextable td { 151 | text-align: left; 152 | vertical-align: top; 153 | } 154 | 155 | table.indextable ul { 156 | margin-top: 0; 157 | margin-bottom: 0; 158 | list-style-type: none; 159 | } 160 | 161 | table.indextable > tbody > tr > td > ul { 162 | padding-left: 0em; 163 | } 164 | 165 | table.indextable tr.pcap { 166 | height: 10px; 167 | } 168 | 169 | table.indextable tr.cap { 170 | margin-top: 10px; 171 | background-color: #f2f2f2; 172 | } 173 | 174 | img.toggler { 175 | margin-right: 3px; 176 | margin-top: 3px; 177 | cursor: pointer; 178 | } 179 | 180 | div.modindex-jumpbox { 181 | border-top: 1px solid #ddd; 182 | border-bottom: 1px solid #ddd; 183 | margin: 1em 0 1em 0; 184 | padding: 0.4em; 185 | } 186 | 187 | div.genindex-jumpbox { 188 | border-top: 1px solid #ddd; 189 | border-bottom: 1px solid #ddd; 190 | margin: 1em 0 1em 0; 191 | padding: 0.4em; 192 | } 193 | 194 | /* -- domain module index --------------------------------------------------- */ 195 | 196 | table.modindextable td { 197 | padding: 2px; 198 | border-collapse: collapse; 199 | } 200 | 201 | /* -- general body styles --------------------------------------------------- */ 202 | 203 | div.body { 204 | min-width: 450px; 205 | max-width: 1920px; 206 | } 207 | 208 | div.body p, div.body dd, div.body li, div.body blockquote { 209 | -moz-hyphens: auto; 210 | -ms-hyphens: auto; 211 | -webkit-hyphens: auto; 212 | hyphens: auto; 213 | } 214 | 215 | a.headerlink { 216 | visibility: hidden; 217 | } 218 | 219 | h1:hover > a.headerlink, 220 | h2:hover > a.headerlink, 221 | h3:hover > a.headerlink, 222 | h4:hover > a.headerlink, 223 | h5:hover > a.headerlink, 224 | h6:hover > a.headerlink, 225 | dt:hover > a.headerlink, 226 | caption:hover > a.headerlink, 227 | p.caption:hover > a.headerlink, 228 | div.code-block-caption:hover > a.headerlink { 229 | visibility: visible; 230 | } 231 | 232 | div.body p.caption { 233 | text-align: inherit; 234 | } 235 | 236 | div.body td { 237 | text-align: left; 238 | } 239 | 240 | .first { 241 | margin-top: 0 !important; 242 | } 243 | 244 | p.rubric { 245 | margin-top: 30px; 246 | font-weight: bold; 247 | } 248 | 249 | img.align-left, .figure.align-left, object.align-left { 250 | clear: left; 251 | float: left; 252 | margin-right: 1em; 253 | } 254 | 255 | img.align-right, .figure.align-right, object.align-right { 256 | clear: right; 257 | float: right; 258 | margin-left: 1em; 259 | } 260 | 261 | img.align-center, .figure.align-center, object.align-center { 262 | display: block; 263 | margin-left: auto; 264 | margin-right: auto; 265 | } 266 | 267 | .align-left { 268 | text-align: left; 269 | } 270 | 271 | .align-center { 272 | text-align: center; 273 | } 274 | 275 | .align-right { 276 | text-align: right; 277 | } 278 | 279 | /* -- sidebars -------------------------------------------------------------- */ 280 | 281 | div.sidebar { 282 | margin: 0 0 0.5em 1em; 283 | border: 1px solid #ddb; 284 | padding: 7px 7px 0 7px; 285 | background-color: #ffe; 286 | width: 40%; 287 | float: right; 288 | } 289 | 290 | p.sidebar-title { 291 | font-weight: bold; 292 | } 293 | 294 | /* -- topics ---------------------------------------------------------------- */ 295 | 296 | div.topic { 297 | border: 1px solid #ccc; 298 | padding: 7px 7px 0 7px; 299 | margin: 10px 0 10px 0; 300 | } 301 | 302 | p.topic-title { 303 | font-size: 1.1em; 304 | font-weight: bold; 305 | margin-top: 10px; 306 | } 307 | 308 | /* -- admonitions ----------------------------------------------------------- */ 309 | 310 | div.admonition { 311 | margin-top: 10px; 312 | margin-bottom: 10px; 313 | padding: 7px; 314 | } 315 | 316 | div.admonition dt { 317 | font-weight: bold; 318 | } 319 | 320 | div.admonition dl { 321 | margin-bottom: 0; 322 | } 323 | 324 | p.admonition-title { 325 | margin: 0px 10px 5px 0px; 326 | font-weight: bold; 327 | } 328 | 329 | div.body p.centered { 330 | text-align: center; 331 | margin-top: 25px; 332 | } 333 | 334 | /* -- code displays --------------------------------------------------------- */ 335 | 336 | pre { 337 | overflow: auto; 338 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 339 | } 340 | 341 | span.pre { 342 | -moz-hyphens: none; 343 | -ms-hyphens: none; 344 | -webkit-hyphens: none; 345 | hyphens: none; 346 | } 347 | 348 | td.linenos pre { 349 | padding: 5px 0px; 350 | border: 0; 351 | background-color: transparent; 352 | color: #aaa; 353 | } 354 | 355 | table.highlighttable { 356 | margin-left: 0.5em; 357 | } 358 | 359 | table.highlighttable td { 360 | padding: 0 0.5em 0 0.5em; 361 | } 362 | 363 | div.code-block-caption { 364 | padding: 2px 5px; 365 | font-size: small; 366 | } 367 | 368 | div.code-block-caption code { 369 | background-color: transparent; 370 | } 371 | 372 | div.code-block-caption + div > div.highlight > pre { 373 | margin-top: 0; 374 | } 375 | 376 | div.code-block-caption span.caption-number { 377 | padding: 0.1em 0.3em; 378 | font-style: italic; 379 | } 380 | 381 | div.code-block-caption span.caption-text { 382 | } 383 | 384 | div.literal-block-wrapper { 385 | padding: 1em 1em 0; 386 | } 387 | 388 | div.literal-block-wrapper div.highlight { 389 | margin: 0; 390 | } 391 | 392 | code.descname { 393 | background-color: transparent; 394 | font-weight: bold; 395 | font-size: 1.2em; 396 | border-style: none; 397 | padding: 0; 398 | } 399 | 400 | code.descclassname { 401 | background-color: transparent; 402 | border-style: none; 403 | padding: 0; 404 | } 405 | 406 | code.xref, a code { 407 | background-color: transparent; 408 | font-weight: bold; 409 | border-style: none; 410 | padding: 0; 411 | } 412 | 413 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 414 | background-color: transparent; 415 | } 416 | 417 | .viewcode-link { 418 | float: right; 419 | } 420 | 421 | .viewcode-back { 422 | float: right; 423 | font-family: sans-serif; 424 | } 425 | 426 | div.viewcode-block:target { 427 | margin: -1px -10px; 428 | padding: 0 10px; 429 | } 430 | 431 | /* -- math display ---------------------------------------------------------- */ 432 | 433 | img.math { 434 | vertical-align: middle; 435 | } 436 | 437 | div.body div.math p { 438 | text-align: center; 439 | } 440 | 441 | span.eqno { 442 | float: right; 443 | } 444 | 445 | span.eqno a.headerlink { 446 | position: relative; 447 | left: 0px; 448 | z-index: 1; 449 | } 450 | 451 | div.math:hover a.headerlink { 452 | visibility: visible; 453 | } 454 | 455 | /* -- printout stylesheet --------------------------------------------------- */ 456 | 457 | @media print { 458 | div.document, 459 | div.documentwrapper, 460 | div.bodywrapper { 461 | margin: 0 !important; 462 | width: 100%; 463 | } 464 | 465 | div.sphinxsidebar, 466 | div.related, 467 | div.footer, 468 | #top-link { 469 | display: none; 470 | } 471 | } 472 | 473 | /* -- My additions ---------------------------------------------------------- */ 474 | 475 | div.note { 476 | color: black; 477 | border: 2px solid #7a9eec; 478 | border-right-style: none; 479 | border-left-style: none; 480 | padding: 10px 20px 0px 60px; 481 | background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px; 482 | } 483 | 484 | div.danger { 485 | color: black; 486 | border: 2px solid #fbc2c4; 487 | border-right-style: none; 488 | border-left-style: none; 489 | padding: 10px 20px 0px 60px; 490 | background: #fbe3e4 url(dialog-note.png) no-repeat 10px 8px; 491 | } 492 | 493 | div.attention { 494 | color: black; 495 | border: 2px solid #ffd324; 496 | border-right-style: none; 497 | border-left-style: none; 498 | padding: 10px 20px 0px 60px; 499 | background: #fff6bf url(dialog-note.png) no-repeat 10px 8px; 500 | } 501 | 502 | div.caution { 503 | color: black; 504 | border: 2px solid #ffd324; 505 | border-right-style: none; 506 | border-left-style: none; 507 | padding: 10px 20px 0px 60px; 508 | background: #fff6bf url(dialog-warning.png) no-repeat 10px 8px; 509 | } 510 | 511 | div.important { 512 | color: black; 513 | background: #fbe3e4 url(dialog-seealso.png) no-repeat 10px 8px; 514 | border: 2px solid #fbc2c4; 515 | border-left-style: none; 516 | border-right-style: none; 517 | padding: 10px 20px 0px 60px; 518 | } 519 | 520 | div.seealso { 521 | color: black; 522 | background: #fff6bf url(dialog-seealso.png) no-repeat 10px 8px; 523 | border: 2px solid #ffd324; 524 | border-left-style: none; 525 | border-right-style: none; 526 | padding: 10px 20px 0px 60px; 527 | } 528 | 529 | div.hint, div.tip { 530 | color: black; 531 | background: #eeffcc url(dialog-topic.png) no-repeat 10px 8px; 532 | border: 2px solid #aacc99; 533 | border-left-style: none; 534 | border-right-style: none; 535 | padding: 10px 20px 0px 60px; 536 | } 537 | 538 | div.admonition-example { 539 | color: black; 540 | background: white url(dialog-topic.png) no-repeat 10px 8px; 541 | border: 2px solid #aacc99; 542 | border-left-style: none; 543 | border-right-style: none; 544 | padding: 10px 0px 20px 60px; 545 | } 546 | div.warning, div.error { 547 | color: black; 548 | background: #fbe3e4 url(dialog-warning.png) no-repeat 10px 8px; 549 | border: 2px solid #fbc2c4; 550 | border-right-style: none; 551 | border-left-style: none; 552 | padding: 10px 20px 0px 60px; 553 | } 554 | 555 | p { 556 | text-align: justify; 557 | padding-bottom: 5px; 558 | } 559 | 560 | h1 { 561 | background: #fff6bf; 562 | border: 2px solid #ffd324; 563 | border-left-style: none; 564 | border-right-style: none; 565 | padding: 10px 10px 10px 10px; 566 | text-align: center; 567 | } 568 | 569 | h2 { 570 | /* background: #eeffcc; */ 571 | border: 2px solid #aacc99; 572 | border-left-style: none; 573 | border-right-style: none; 574 | border-top-style: none; 575 | padding: 10px 0px 0px 0px; 576 | /* text-align: center; */ 577 | } 578 | 579 | h3 { 580 | /* background: #eeffcc; */ 581 | border: 1px solid #7a9eec; 582 | border-left-style: none; 583 | border-right-style: none; 584 | border-top-style: none; 585 | padding: 0; 586 | /* text-align: center; */ 587 | } 588 | 589 | h4 { 590 | background: #eeffcc; 591 | /* border: 1px solid #aacc99; */ 592 | border-left-style: none; 593 | border-right-style: none; 594 | border-top-style: none; 595 | padding: 5px 5px 5px 5px; 596 | /* text-align: center; */ 597 | } 598 | 599 | cite { 600 | -webkit-border-radius: 3px; 601 | -moz-border-radius: 3px; 602 | border-radius: 3px; 603 | border: 1px solid #e1e1e8; 604 | background: #f7f7f9; 605 | margin: 0 0 10px; 606 | padding: 0 5px 0 5px; 607 | font-size: 13px; 608 | font-style: italic; 609 | } 610 | 611 | .program { 612 | -webkit-border-radius: 3px; 613 | -moz-border-radius: 3px; 614 | border-radius: 3px; 615 | border: 1px solid #e1e1e8; 616 | background: #f7f7f9; 617 | margin: 0 0 10px; 618 | padding: 0 5px 0 5px; 619 | font-size: 13px; 620 | } 621 | 622 | /* dt/dd on single line */ 623 | 624 | dl.field-list { 625 | display: grid; 626 | grid-template-columns: max-content auto; 627 | } 628 | 629 | dt.field-list { 630 | grid-column-start: 1; 631 | } 632 | 633 | dt.field-odd:after { 634 | content: ':'; 635 | } 636 | 637 | dt.field-even:after { 638 | content: ':'; 639 | } 640 | 641 | dd.field-list { 642 | grid-column-start: 2; 643 | } 644 | 645 | hr.docutils { 646 | font-weight: bold; 647 | /* border: 2px solid #7a9eec; */ 648 | border: 2px solid grey; 649 | border-left-style: none; 650 | border-right-style: none; 651 | border-top-style: none; 652 | } 653 | 654 | div.versionadded { 655 | color: black; 656 | /* padding: 10px 0 0 10px; */ 657 | font-weight: bold; 658 | margin: 5px 0; 659 | } 660 | 661 | div.versionchanged { 662 | color: black; 663 | /* padding: 0 10px 0 0; */ 664 | font-weight: bold; 665 | margin: 5px 0; 666 | } 667 | 668 | span.added { 669 | background: #eeffcc ; 670 | border: 2px solid #aacc99; 671 | border-left-style: none; 672 | border-right-style: none; 673 | padding: 5px; 674 | } 675 | 676 | span.changed { 677 | background: #fbe3e4 ; 678 | border: 2px solid #fbc2c4; 679 | border-left-style: none; 680 | border-right-style: none; 681 | padding: 5px; 682 | } 683 | 684 | dl.class, dl.exception, dl.data, dl.function { 685 | /* background: #fbe3e4 ;*/ 686 | border: 1px solid grey; 687 | border-top-style: none; 688 | border-left-style: none; 689 | border-right-style: none; 690 | /* padding: 3px;*/ 691 | } 692 | 693 | dt.py { 694 | background: #f8f8f8; 695 | /* border: 2px solid #aacc99;*/ 696 | /* border: 2px solid #ffcccc; /*#ffd324;*/ 697 | /* border: 2px solid #fbc2c4;*/ 698 | border: 1px solid #e1e1e8; 699 | border-top-style: none; 700 | border-left-style: none; 701 | /* border-right-style: none; */ 702 | padding: 2px 5px; 703 | } 704 | -------------------------------------------------------------------------------- /docs/_static/dialog-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/docs/_static/dialog-note.png -------------------------------------------------------------------------------- /docs/_static/dialog-seealso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/docs/_static/dialog-seealso.png -------------------------------------------------------------------------------- /docs/_static/dialog-topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/docs/_static/dialog-topic.png -------------------------------------------------------------------------------- /docs/_static/dialog-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/docs/_static/dialog-warning.png -------------------------------------------------------------------------------- /docs/_static/fb-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/docs/_static/fb-favicon.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | import sphinx_bootstrap_theme 18 | from firebird.driver import __VERSION__ 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'firebird-driver' 23 | copyright = '2020-present, The Firebird Project' 24 | author = 'Pavel Císař' 25 | 26 | # The short X.Y version 27 | version = __VERSION__ 28 | 29 | # The full version, including alpha/beta/rc tags 30 | release = __VERSION__ 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.napoleon', 42 | 'sphinx.ext.viewcode', 43 | 'sphinx.ext.autosectionlabel', 44 | #'sphinx_autodoc_typehints', 45 | 'sphinx.ext.todo', 46 | #'sphinx.ext.coverage', 47 | ] 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.txt' 57 | 58 | # List of patterns, relative to source directory, that match files and 59 | # directories to ignore when looking for source files. 60 | # This pattern also affects html_static_path and html_extra_path. 61 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'requirements.txt'] 62 | 63 | default_role = 'py:obj' 64 | 65 | # -- Options for HTML output ------------------------------------------------- 66 | 67 | # The theme to use for HTML and HTML Help pages. See the documentation for 68 | # a list of builtin themes. 69 | # 70 | #html_theme = 'alabaster' 71 | 72 | html_theme = "bootstrap" 73 | html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 74 | 75 | # bootstrap theme config 76 | 77 | # (Optional) Logo. Should be small enough to fit the navbar (ideally 24x24). 78 | # Path should be relative to the ``_static`` files directory. 79 | #html_logo = "my_logo.png" 80 | 81 | # Theme options are theme-specific and customize the look and feel of a 82 | # theme further. 83 | html_theme_options = { 84 | # Navigation bar title. (Default: ``project`` value) 85 | #'navbar_title': "Firebird-driver", 86 | 87 | # Tab name for entire site. (Default: "Site") 88 | 'navbar_site_name': "Content", 89 | 90 | # A list of tuples containing pages or urls to link to. 91 | # Valid tuples should be in the following forms: 92 | # (name, page) # a link to a page 93 | # (name, "/aa/bb", 1) # a link to an arbitrary relative url 94 | # (name, "http://example.com", True) # arbitrary absolute url 95 | # Note the "1" or "True" value above as the third argument to indicate 96 | # an arbitrary url. 97 | 'navbar_links': [ 98 | ("Usage Guide", "usage-guide"), 99 | ("Reference", "reference"), 100 | ("Index", "genindex"), 101 | ], 102 | 103 | # Render the next and previous page links in navbar. (Default: true) 104 | 'navbar_sidebarrel': False, 105 | 106 | # Render the current pages TOC in the navbar. (Default: true) 107 | #'navbar_pagenav': True, 108 | 109 | # Tab name for the current pages TOC. (Default: "Page") 110 | #'navbar_pagenav_name': "Page", 111 | 112 | # Global TOC depth for "site" navbar tab. (Default: 1) 113 | # Switching to -1 shows all levels. 114 | 'globaltoc_depth': 3, 115 | 116 | # Include hidden TOCs in Site navbar? 117 | # 118 | # Note: If this is "false", you cannot have mixed ``:hidden:`` and 119 | # non-hidden ``toctree`` directives in the same page, or else the build 120 | # will break. 121 | # 122 | # Values: "true" (default) or "false" 123 | 'globaltoc_includehidden': "true", 124 | 125 | # HTML navbar class (Default: "navbar") to attach to
element. 126 | # For black navbar, do "navbar navbar-inverse" 127 | 'navbar_class': "navbar navbar-inverse", 128 | 129 | # Fix navigation bar to top of page? 130 | # Values: "true" (default) or "false" 131 | 'navbar_fixed_top': "true", 132 | 133 | # Location of link to source. 134 | # Options are "nav" (default), "footer" or anything else to exclude. 135 | 'source_link_position': "none", 136 | 137 | # Bootswatch (http://bootswatch.com/) theme. 138 | # 139 | # Options are nothing (default) or the name of a valid theme 140 | # such as "cosmo" or "sandstone". 141 | # 142 | # The set of valid themes depend on the version of Bootstrap 143 | # that's used (the next config option). 144 | # 145 | # Currently, the supported themes are: 146 | # - Bootstrap 2: https://bootswatch.com/2 147 | # - Bootstrap 3: https://bootswatch.com/3 148 | #'bootswatch_theme': "united", # cerulean, flatly, lumen, materia, united, yeti 149 | 'bootswatch_theme': "cerulean", 150 | 151 | # Choose Bootstrap version. 152 | # Values: "3" (default) or "2" (in quotes) 153 | 'bootstrap_version': "2", 154 | } 155 | 156 | # Add any paths that contain custom static files (such as style sheets) here, 157 | # relative to this directory. They are copied after the builtin static files, 158 | # so a file named "default.css" will overwrite the builtin "default.css". 159 | html_static_path = ['_static'] 160 | 161 | 162 | # -- Extension configuration ------------------------------------------------- 163 | 164 | autosectionlabel_prefix_document = True 165 | 166 | # Autodoc options 167 | # --------------- 168 | autodoc_default_options = { 169 | 'content': 'both', 170 | 'members': True, 171 | 'member-order': 'groupwise', 172 | 'undoc-members': True, 173 | 'exclude-members': '__weakref__', 174 | 'show-inheritance': True, 175 | 'no-inherited-members': True, 176 | 'no-private-members': True, 177 | } 178 | set_type_checking_flag = True 179 | autodoc_class_signature = 'mixed' 180 | always_document_param_types = True 181 | autodoc_typehints = 'both' # default 'signature' 182 | autodoc_typehints_format = 'short' 183 | autodoc_typehints_description_target = 'all' 184 | 185 | # Napoleon options 186 | # ---------------- 187 | napoleon_include_init_with_doc = True 188 | napoleon_include_private_with_doc = True 189 | napoleon_include_special_with_doc = True 190 | napoleon_use_admonition_for_examples = False 191 | napoleon_use_admonition_for_notes = True 192 | napoleon_use_admonition_for_references = True 193 | napoleon_use_ivar = False 194 | napoleon_use_rtype = True 195 | napoleon_use_param = True 196 | napoleon_use_keyword = True 197 | napoleon_attr_annotations = True 198 | napoleon_preprocess_types = True 199 | 200 | # -- Options for intersphinx extension --------------------------------------- 201 | 202 | # intersphinx 203 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None), 204 | 'base': ('https://firebird-base.rtfd.io/en/latest', None), 205 | 'lib': ('https://firebird-lib.rtfd.io/en/latest', None), 206 | } 207 | 208 | # -- Options for todo extension ---------------------------------------------- 209 | 210 | # If true, `todo` and `todoList` produce output, else they produce nothing. 211 | todo_include_todos = True 212 | -------------------------------------------------------------------------------- /docs/getting-started.txt: -------------------------------------------------------------------------------- 1 | 2 | ############### 3 | Getting Started 4 | ############### 5 | 6 | Installation 7 | ************ 8 | 9 | Firebird-driver is written as pure-Python module (requires Python 3.8+) on top of 10 | Firebird client library (fbclient.so/dll) using ctypes_. Driver supports Firebird version 11 | 3.0 and higher. 12 | 13 | Firebird-driver is distributed as `setuptools`_ package and the preferred installation 14 | method is via pip_ tool. 15 | 16 | Installation from PYPI_ 17 | ======================= 18 | 19 | Run pip:: 20 | 21 | $ pip install firebird-driver 22 | 23 | Quick-start Guide 24 | ***************** 25 | 26 | This brief tutorial aims to get the reader started by demonstrating elementary usage of 27 | Firebird-driver. It is not a comprehensive Python Database API tutorial, nor is it 28 | comprehensive in its coverage of anything else. 29 | 30 | The numerous advanced features of Firebird-driver are covered in another section of this 31 | documentation, which is not in a tutorial format, though it is replete with examples. 32 | 33 | Driver configuration 34 | ==================== 35 | 36 | The driver uses configuration built on top of `configuration system ` 37 | provided by `firebird-base`_ package. In addition to global settings, the configuration 38 | also includes the definition of connection parameters to Firebird servers and databases. 39 | 40 | The default configuration connects to embedded server using direct/local connection method. 41 | To access remote servers and databases (or local ones through remote protocols), it's 42 | necessary to adjust default configuration, or `register` them in configuration manager. 43 | 44 | You can manipulate the configuration objects directly, or load configuration from files or 45 | strings (in `.ini-style` `ConfigParser` format). 46 | 47 | Connecting to a Database 48 | ======================== 49 | 50 | **Example 1:** 51 | 52 | A simple database connection is typically established with code such as this: 53 | 54 | .. sourcecode:: python 55 | 56 | from firebird.driver import connect 57 | 58 | # Attach to 'employee' database/alias using embedded server connection 59 | con = connect('employee', user='sysdba', password='masterkey') 60 | 61 | # Attach to 'employee' database/alias using local server connection 62 | from firebird.driver import driver_config 63 | driver_config.server_defaults.host.value = 'localhost' 64 | con = connect('employee', user='sysdba', password='masterkey') 65 | 66 | # Set 'user' and 'password' via configuration 67 | driver_config.server_defaults.user.value = 'SYSDBA' 68 | driver_config.server_defaults.password.value = 'masterkey' 69 | con = connect('employee') 70 | 71 | **Example 2:** 72 | 73 | A database connection typically uses specific configuration, and is established with code 74 | such as this: 75 | 76 | .. sourcecode:: python 77 | 78 | from firebird.driver import connect, driver_config 79 | 80 | # Register Firebird server 81 | srv_cfg = """[local] 82 | host = localhost 83 | user = SYSDBA 84 | password = masterkey 85 | """ 86 | driver_config.register_server('local', srv_cfg) 87 | 88 | # Register database 89 | db_cfg = """[employee] 90 | server = local 91 | database = employee.fdb 92 | protocol = inet 93 | charset = utf8 94 | """ 95 | driver_config.register_database('employee', db_cfg) 96 | 97 | # Attach to 'employee' database 98 | con = connect('employee') 99 | 100 | .. note:: 101 | 102 | Some parameters like 'user' and 'password' could be overridden with keyword parameters. 103 | Few parameters like 'crypt_callback' or 'no_db_triggers' could be specified **ONLY** 104 | as keyword arguments. 105 | 106 | Creating a Database 107 | =================== 108 | 109 | A database is created using `~firebird.driver.core.create_database()` function. 110 | Like `~firebird.driver.core.connect()`, this function uses configuration for specification of 111 | database parameters like page size, sweep interval etc. 112 | 113 | Executing SQL Statements 114 | ======================== 115 | 116 | For this section, suppose we have a table defined and populated by the following SQL code: 117 | 118 | .. sourcecode:: sql 119 | 120 | create table languages 121 | ( 122 | name varchar(20), 123 | year_released integer 124 | ); 125 | 126 | insert into languages (name, year_released) values ('C', 1972); 127 | insert into languages (name, year_released) values ('Python', 1991); 128 | 129 | **Example 1** 130 | 131 | This example shows the *simplest* way to print the entire contents of 132 | the `languages` table: 133 | 134 | .. sourcecode:: python 135 | 136 | from firebird.driver import connect 137 | 138 | con = connect('test.fdb', user='sysdba', password='masterkey') 139 | 140 | # Create a Cursor object that operates in the context of Connection con: 141 | cur = con.cursor() 142 | 143 | # Execute the SELECT statement: 144 | cur.execute("select * from languages order by year_released") 145 | 146 | # Retrieve all rows as a sequence and print that sequence: 147 | print(cur.fetchall()) 148 | 149 | Sample output: 150 | 151 | .. sourcecode:: python 152 | 153 | [('C', 1972), ('Python', 1991)] 154 | 155 | **Example 2** 156 | 157 | Here's another trivial example that demonstrates various ways of fetching a single row at a time from a `SELECT`-cursor: 158 | 159 | .. sourcecode:: python 160 | 161 | from firebird.driver import connect 162 | 163 | con = connect('test.fdb', user='sysdba', password='masterkey') 164 | 165 | cur = con.cursor() 166 | SELECT = "select name, year_released from languages order by year_released" 167 | 168 | # 1. Iterate over the rows available from the cursor, unpacking the 169 | # resulting sequences to yield their elements (name, year_released): 170 | cur.execute(SELECT) 171 | for (name, year_released) in cur: 172 | print(f'{name} has been publicly available since {year_released}.') 173 | 174 | # 2. Equivalently: 175 | cur.execute(SELECT) 176 | for row in cur: 177 | print(f'{row[0]} has been publicly available since {row[1]}.') 178 | 179 | Sample output: 180 | 181 | .. sourcecode:: python 182 | 183 | C has been publicly available since 1972. 184 | Python has been publicly available since 1991. 185 | C has been publicly available since 1972. 186 | Python has been publicly available since 1991. 187 | C has been publicly available since 1972. 188 | Python has been publicly available since 1991. 189 | 190 | **Example 3** 191 | 192 | The following program is a simplistic table printer (applied in this example to `languages`): 193 | 194 | .. sourcecode:: python 195 | 196 | from firebird.driver import connect, DESCRIPTION_NAME, DESCRIPTION_DISPLAY_SIZE 197 | 198 | TABLE_NAME = 'languages' 199 | SELECT = f'select * from {TABLE_NAME} order by year_released' 200 | 201 | con = connect('test.fdb', user='sysdba', password='masterkey') 202 | 203 | cur = con.cursor() 204 | cur.execute(SELECT) 205 | 206 | # Print a header. 207 | for fieldDesc in cur.description: 208 | print(fieldDesc[DESCRIPTION_NAME].ljust(fieldDesc[DESCRIPTION_DISPLAY_SIZE]), end='') 209 | print() # Finish the header with a newline. 210 | print('-' * 78) 211 | 212 | # For each row, print the value of each field left-justified within 213 | # the maximum possible width of that field. 214 | fieldIndices = range(len(cur.description)) 215 | for row in cur: 216 | for fieldIndex in fieldIndices: 217 | fieldValue = str(row[fieldIndex]) 218 | fieldMaxWidth = cur.description[fieldIndex][DESCRIPTION_DISPLAY_SIZE] 219 | 220 | print(fieldValue.ljust(fieldMaxWidth), end='') 221 | 222 | print() # Finish the row with a newline. 223 | 224 | 225 | Sample output: 226 | 227 | .. sourcecode:: python 228 | 229 | NAME YEAR_RELEASED 230 | ------------------------------------------------------------------------------ 231 | C 1972 232 | Python 1991 233 | 234 | 235 | **Example 4** 236 | 237 | Let's insert more languages: 238 | 239 | .. sourcecode:: python 240 | 241 | from firebird.driver import connect 242 | 243 | con = connect('test.fdb', user='sysdba', password='masterkey') 244 | 245 | cur = con.cursor() 246 | 247 | newLanguages = [ 248 | ('Lisp', 1958), 249 | ('Dylan', 1995), 250 | ] 251 | 252 | cur.executemany("insert into languages (name, year_released) values (?, ?)", 253 | newLanguages 254 | ) 255 | 256 | # The changes will not be saved unless the transaction is committed explicitly: 257 | con.commit() 258 | 259 | 260 | Note the use of a *parameterized* SQL statement above. When dealing with repetitive 261 | statements, this is much faster and less error-prone than assembling each SQL statement 262 | manually. (You can read more about parameterized SQL statements in the section on 263 | :ref:`Prepared Statements `.) 264 | 265 | After running Example 4, the table printer from Example 3 would print: 266 | 267 | .. sourcecode:: python 268 | 269 | NAME YEAR_RELEASED 270 | ------------------------------------------------------------------------------ 271 | Lisp 1958 272 | C 1972 273 | Python 1991 274 | Dylan 1995 275 | 276 | 277 | Calling Stored Procedures 278 | ========================= 279 | 280 | Firebird supports stored procedures written in a proprietary procedural SQL language. 281 | Firebird stored procedures can have *input* parameters and/or *output* parameters. Some 282 | databases support *input/output* parameters, where the same parameter is used for both 283 | input and output; Firebird does not support this. 284 | 285 | It is important to distinguish between procedures that *return a result set* and procedures 286 | that *populate and return their output parameters exactly once*. Conceptually, the latter 287 | "return their output parameters" like a Python function, whereas the former "yield result 288 | rows" like a Python generator. 289 | 290 | Firebird's *server-side* procedural SQL syntax makes no such distinction, but *client-side* 291 | SQL code (and C API code) must. A result set is retrieved from a stored procedure by 292 | `SELECT`-ing from the procedure, whereas output parameters are retrieved with an 293 | `EXECUTE PROCEDURE` statement. 294 | 295 | To *retrieve a result set* from a stored procedure with Firebird-driver, use code such as this: 296 | 297 | .. sourcecode:: python 298 | 299 | cur.execute("select output1, output2 from the_proc(?, ?)", (input1, input2)) 300 | 301 | # Ordinary fetch code here, such as: 302 | for row in cur: 303 | ... # process row 304 | 305 | con.commit() # If the procedure had any side effects, commit them. 306 | 307 | 308 | To *execute* a stored procedure and *access its output parameters*, use code such as this: 309 | 310 | .. sourcecode:: python 311 | 312 | cur.callproc("the_proc", (input1, input2)) 313 | 314 | # If there are output parameters, retrieve them as though they were the 315 | # first row of a result set. For example: 316 | outputParams = cur.fetchone() 317 | 318 | con.commit() # If the procedure had any side effects, commit them. 319 | 320 | 321 | This latter is not very elegant; it would be preferable to access the procedure's output 322 | parameters as the return value of `Cursor.callproc()`. The Python DB API specification 323 | requires the current behavior, however. 324 | 325 | .. _setuptools: https://pypi.org/project/setuptools/ 326 | .. _PYPI: https://pypi.org/ 327 | .. _ctypes: http://docs.python.org/library/ctypes.html 328 | .. _pip: https://pypi.org/project/pip/ 329 | .. _firebird-base: https://firebird-base.rtfd.io 330 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | 2 | ############################## 3 | The Python driver for Firebird 4 | ############################## 5 | 6 | The `firebird-driver` package provides official `Python Database API 2.0`_-compliant driver 7 | for the open source relational database `Firebird`_ ®. In addition to the minimal feature 8 | set of the standard Python DB API, this driver also exposes the new (interface-based) client 9 | API introduced in Firebird 3, and number of additional extensions and enhancements for 10 | convenient use of Firebird RDBMS. 11 | 12 | This documentation set is not a tutorial on SQL or Firebird; rather, it is a topical 13 | presentation of driver's feature set, with example code to demonstrate basic usage patterns. 14 | For detailed information about Firebird features, see the 15 | `Firebird documentation `__, and especially 16 | the excellent `The Firebird Book `__ 17 | written by Helen Borrie and published by IBPhoenix_. 18 | 19 | .. seealso:: `firebird-lib`_ package for optional extensions to this driver. 20 | 21 | .. note:: Requires Python 3.11+ 22 | 23 | .. tip:: You can download docset for Dash_ (MacOS) or Zeal_ (Windows / Linux) documentation 24 | readers from releases_ at github. 25 | 26 | Content 27 | ******* 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | :caption: Contents: 32 | 33 | getting-started 34 | usage-guide 35 | python-db-api-compliance 36 | reference 37 | changelog 38 | license 39 | 40 | Driver development is sponsored by IBPhoenix_. 41 | 42 | Indices and tables 43 | ****************** 44 | 45 | * :ref:`genindex` 46 | * :ref:`modindex` 47 | 48 | .. _IBPhoenix: http://www.ibphoenix.com 49 | .. _Python: http://python.org 50 | .. _Python Database API 2.0: http://www.python.org/dev/peps/pep-0249/ 51 | .. _Firebird: http://www.firebirdsql.org 52 | .. _under: http://www.firebirdsql.org/en/devel-python-driver/ 53 | .. _Firebird Project: http://www.firebirdsql.org 54 | .. _IBPhoenix: http://www.ibphoenix.com 55 | .. _firebird-lib: https://pypi.org/project/firebird-lib/ 56 | .. _releases: https://github.com/FirebirdSQL/python3-driver/releases 57 | .. _Dash: https://kapeli.com/dash 58 | .. _Zeal: https://zealdocs.org/ 59 | -------------------------------------------------------------------------------- /docs/license.txt: -------------------------------------------------------------------------------- 1 | ####### 2 | License 3 | ####### 4 | 5 | .. include:: ../LICENSE 6 | 7 | -------------------------------------------------------------------------------- /docs/make-docset: -------------------------------------------------------------------------------- 1 | doc2dash -u https://firebird-driver.readthedocs.io/en/latest/ -f -i ./_static/fb-favicon.png -n firebird-driver ./_build/html/ 2 | tar --exclude='.DS_Store' -cvzf ../dist/firebird-driver-docset.tgz firebird-driver.docset 3 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/python-db-api-compliance.txt: -------------------------------------------------------------------------------- 1 | ########################## 2 | Compliance to PyDB API 2.0 3 | ########################## 4 | 5 | .. currentmodule:: firebird.driver 6 | 7 | Full text of Python Database API 2.0 (PEP 249) is available at 8 | `http://www.python.org/dev/peps/pep-0249/ `__ 9 | 10 | Unsupported Optional Features 11 | ============================= 12 | 13 | `Cursor.nextset` 14 | 15 | This method is not implemented because the database engine does not support 16 | opening multiple result sets simultaneously with a single cursor. 17 | 18 | Supported Optional Features 19 | =========================== 20 | 21 | - `Connection.Error`, `Connection.ProgrammingError`, etc. 22 | 23 | All exception classes defined by the DB API standard are exposed on the Connection objects 24 | as attributes (in addition to being available at module scope). 25 | - `Cursor.connection` 26 | 27 | This read-only attribute return a reference to the Connection object on which the cursor was created. 28 | 29 | 30 | Nominally Supported Optional Features 31 | ===================================== 32 | 33 | `.Cursor` 34 | 35 | `~.Cursor.arraysize` 36 | 37 | As required by the spec, the value of this attribute is observed with 38 | respect to the `fetchmany` method. However, changing the value of this 39 | attribute does not make any difference in fetch efficiency because 40 | the database engine only supports fetching a single row at a time. 41 | 42 | `~.Cursor.setinputsizes` 43 | 44 | Although this method is present, it does nothing, as allowed by the spec. 45 | 46 | `~.Cursor.setoutputsize` 47 | 48 | Although this method is present, it does nothing, as allowed by the spec. 49 | 50 | 51 | Caveats 52 | ======= 53 | 54 | Firebird-driver offers a large feature set beyond the minimal requirements 55 | of the Python DB API. This section attempts to document only those 56 | features that overlap with the DB API. 57 | 58 | 59 | `.Connection` 60 | 61 | `~.Connection.commit(retaining=False)` 62 | `~.Connection.rollback(retaining=False, savepoint=None)` 63 | 64 | The `commit` and `rollback` methods accept an optional boolean parameter `retaining` 65 | (default `False`) that indicates whether the transactional context of the transaction 66 | being resolved should be recycled. For details, see the Advanced 67 | Transaction Control: Retaining Operations section of this document. 68 | The `rollback` method accepts an optional string parameter `savepoint` 69 | that causes the transaction to roll back only as far as the designated 70 | savepoint, rather than rolling back entirely. For details, see the 71 | Advanced Transaction Control: Savepoints section of this document. 72 | 73 | 74 | `.Cursor` 75 | 76 | `~.Cursor.description` 77 | 78 | Firebird-driver makes absolutely no guarantees about `description` except those 79 | required by the Python Database API Specification 2.0 (that is, `description` 80 | is either `None` or a sequence of 7-element sequences). Therefore, client 81 | programmers should *not* rely on `description` being an instance of a particular 82 | class or type. Firebird-driver provides several named positional constants to be 83 | used as indices into a given element of `description` . The contents 84 | of all `description` elements are defined by the DB API spec; these 85 | constants are provided merely for convenience. 86 | 87 | .. sourcecode:: python 88 | 89 | DESCRIPTION_NAME 90 | DESCRIPTION_TYPE_CODE 91 | DESCRIPTION_DISPLAY_SIZE 92 | DESCRIPTION_INTERNAL_SIZE 93 | DESCRIPTION_PRECISION 94 | DESCRIPTION_SCALE 95 | DESCRIPTION_NULL_OK 96 | 97 | Here is an example of accessing the *name* of the first field in the 98 | `description` of cursor `cur`: 99 | 100 | .. sourcecode:: python 101 | 102 | nameOfFirstField = cur.description[0][firebird.driver.DESCRIPTION_NAME] 103 | 104 | For more information, see the documentation of Cursor.description in 105 | the `DB API Specification `__. 106 | 107 | `~.Cursor.rowcount` 108 | 109 | Although `Cursor` in Firebird-driver implement this attribute, 110 | the database engine's own support for the determination of 111 | "rows affected"/"rows selected" is quirky. The database engine only 112 | supports the determination of rowcount for `INSERT`, `UPDATE`, 113 | `DELETE`, and `SELECT` statements. When stored procedures become 114 | involved, row count figures are usually not available to the client. 115 | Determining rowcount for `SELECT` statements is problematic: the 116 | rowcount is reported as zero until at least one row has been fetched 117 | from the result set, and the rowcount is misreported if the result set 118 | is larger than 1302 rows. The server apparently marshals result sets 119 | internally in batches of 1302, and will misreport the rowcount for 120 | result sets larger than 1302 rows until the 1303rd row is fetched, 121 | result sets larger than 2604 rows until the 2605th row is fetched, and 122 | so on, in increments of 1302. As required by the Python DB API Spec, 123 | the rowcount attribute "is -1 in case no executeXX() has been 124 | performed on the cursor or the rowcount of the last operation is not 125 | determinable by the interface". 126 | 127 | .. note:: 128 | 129 | This attribute is just an alias for `.Cursor.affected_rows` property. 130 | -------------------------------------------------------------------------------- /docs/ref-config.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.config 2 | :synopsis: Driver configuration 3 | 4 | ====================== 5 | firebird.driver.config 6 | ====================== 7 | 8 | This module defines the configuration system for the firebird-driver. 9 | It uses an INI-style format managed via the `DriverConfig` class, which 10 | allows defining settings for the driver itself, default server/database 11 | parameters, and named configurations for specific servers and databases. 12 | 13 | Configuration can be loaded from files, strings, or dictionaries, and 14 | supports environment variable interpolation. The primary interaction point 15 | is usually the global `driver_config` instance. 16 | 17 | Classes 18 | ======= 19 | 20 | .. autoclass:: DriverConfig 21 | .. autoclass:: ServerConfig 22 | .. autoclass:: DatabaseConfig 23 | 24 | Globals 25 | ======= 26 | 27 | .. autodata:: driver_config 28 | :no-value: 29 | -------------------------------------------------------------------------------- /docs/ref-core.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.core 2 | :synopsis: Main Firebird driver code 3 | 4 | ==================== 5 | firebird.driver.core 6 | ==================== 7 | 8 | This is the main code module of the Firebird driver. 9 | 10 | Constants and variables 11 | ======================= 12 | 13 | C integer limit constants 14 | ------------------------- 15 | 16 | .. hlist:: 17 | :columns: 4 18 | 19 | - SHRT_MIN 20 | - SHRT_MAX 21 | - USHRT_MAX 22 | - INT_MIN 23 | - INT_MAX 24 | - UINT_MAX 25 | - LONG_MIN 26 | - LONG_MAX 27 | 28 | Translation dictionaries 29 | ------------------------ 30 | 31 | .. autodata:: CHARSET_MAP 32 | :no-value: 33 | 34 | Other constants and variables 35 | ----------------------------- 36 | 37 | .. autodata:: MAX_BLOB_SEGMENT_SIZE 38 | 39 | .. autodata:: FS_ENCODING 40 | :no-value: 41 | 42 | .. autodata:: _master 43 | :no-value: 44 | 45 | .. autodata:: _util 46 | :no-value: 47 | 48 | .. autodata:: TIMEOUT 49 | :no-value: 50 | 51 | Context managers 52 | ================ 53 | 54 | .. autofunction:: transaction 55 | .. autofunction:: temp_database 56 | 57 | Functions 58 | ========= 59 | 60 | .. autofunction:: connect 61 | .. autofunction:: create_database 62 | .. autofunction:: connect_server 63 | .. autofunction:: tpb 64 | 65 | Managers for parameter buffers 66 | ============================== 67 | 68 | .. autoclass:: TPB 69 | .. autoclass:: DPB 70 | .. autoclass:: SPB_ATTACH 71 | .. autoclass:: Buffer 72 | .. autoclass:: CBuffer 73 | 74 | Classes 75 | ======= 76 | 77 | .. autoclass:: Connection 78 | .. autoclass:: TransactionManager 79 | .. autoclass:: DistributedTransactionManager 80 | .. autoclass:: Statement 81 | .. autoclass:: Cursor 82 | .. autoclass:: Server 83 | .. autoclass:: ServerServiceProvider 84 | .. autoclass:: ServerDbServices3 85 | .. autoclass:: ServerDbServices 86 | .. autoclass:: ServerUserServices 87 | .. autoclass:: ServerTraceServices 88 | .. autoclass:: InfoProvider 89 | .. autoclass:: DatabaseInfoProvider3 90 | .. autoclass:: DatabaseInfoProvider 91 | .. autoclass:: TransactionInfoProvider3 92 | .. autoclass:: TransactionInfoProvider 93 | .. autoclass:: StatementInfoProvider3 94 | .. autoclass:: StatementInfoProvider 95 | .. autoclass:: ServerInfoProvider 96 | .. autoclass:: EventCollector 97 | .. autoclass:: EventBlock 98 | .. autoclass:: BlobReader 99 | .. autoclass:: EngineVersionProvider 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/ref-fbapi.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.fbapi 2 | :synopsis: Python ctypes interface to Firebird client library 3 | 4 | ===================== 5 | firebird.driver.fbapi 6 | ===================== 7 | 8 | This module contains low-level :ref:`ctypes ` interface to 9 | Firebird client library (`fbclient.so/dll`). 10 | 11 | Constants 12 | ========= 13 | 14 | Type codes 15 | ---------- 16 | 17 | .. hlist:: 18 | :columns: 6 19 | 20 | - SQL_TEXT 21 | - SQL_VARYING 22 | - SQL_SHORT 23 | - SQL_LONG 24 | - SQL_FLOAT 25 | - SQL_DOUBLE 26 | - SQL_D_FLOAT 27 | - SQL_TIMESTAMP 28 | - SQL_BLOB 29 | - SQL_ARRAY 30 | - SQL_QUAD 31 | - SQL_TYPE_TIME 32 | - SQL_TYPE_DATE 33 | - SQL_INT64 34 | - SQL_BOOLEAN 35 | - SQL_NULL 36 | - SUBTYPE_NUMERIC 37 | - SUBTYPE_DECIMAL 38 | 39 | Internal type codes (for example used by ARRAY descriptor) 40 | ---------------------------------------------------------- 41 | 42 | .. hlist:: 43 | :columns: 6 44 | 45 | - blr_text 46 | - blr_text2 47 | - blr_short 48 | - blr_long 49 | - blr_quad 50 | - blr_float 51 | - blr_double 52 | - blr_d_float 53 | - blr_timestamp 54 | - blr_varying 55 | - blr_varying2 56 | - blr_blob 57 | - blr_cstring 58 | - blr_cstring2 59 | - blr_blob_id 60 | - blr_sql_date 61 | - blr_sql_time 62 | - blr_int64 63 | - blr_blob2 64 | - blr_domain_name 65 | - blr_domain_name2 66 | - blr_not_nullable 67 | - blr_column_name 68 | - blr_column_name2 69 | - blr_bool 70 | - blr_dec64 71 | - blr_dec128 72 | - blr_dec_fixed 73 | - blr_sql_time_tz 74 | - blr_timestamp_tz 75 | - blr_ex_time_tz 76 | - blr_ex_timestamp_tz 77 | 78 | Types 79 | ===== 80 | 81 | .. autoclass:: Int 82 | :no-show-inheritance: 83 | 84 | .. autoclass:: IntPtr 85 | :no-show-inheritance: 86 | 87 | .. autoclass:: Int64 88 | :no-show-inheritance: 89 | 90 | .. autoclass:: Int64Ptr 91 | :no-show-inheritance: 92 | 93 | .. autoclass:: QWord 94 | :no-show-inheritance: 95 | 96 | .. autoclass:: STRING 97 | :no-show-inheritance: 98 | 99 | .. autoclass:: ISC_LONG 100 | :no-show-inheritance: 101 | 102 | .. autoclass:: ISC_LONG_PTR 103 | :no-show-inheritance: 104 | 105 | .. autoclass:: ISC_ULONG 106 | :no-show-inheritance: 107 | 108 | .. autoclass:: ISC_SHORT 109 | :no-show-inheritance: 110 | 111 | .. autoclass:: ISC_USHORT 112 | :no-show-inheritance: 113 | 114 | .. autoclass:: ISC_UCHAR 115 | :no-show-inheritance: 116 | 117 | .. autoclass:: ISC_INT64 118 | :no-show-inheritance: 119 | 120 | .. autoclass:: ISC_UINT64 121 | :no-show-inheritance: 122 | 123 | .. autoclass:: ISC_DATE 124 | :no-show-inheritance: 125 | 126 | .. autoclass:: ISC_TIME 127 | :no-show-inheritance: 128 | 129 | .. autoclass:: FB_DEC16 130 | :no-show-inheritance: 131 | 132 | .. autoclass:: FB_DEC16Ptr 133 | :no-show-inheritance: 134 | 135 | .. autoclass:: FB_DEC34 136 | :no-show-inheritance: 137 | 138 | .. autoclass:: FB_DEC34Ptr 139 | :no-show-inheritance: 140 | 141 | .. autoclass:: FB_I128 142 | :no-show-inheritance: 143 | 144 | .. autoclass:: FB_I128Ptr 145 | :no-show-inheritance: 146 | 147 | .. autoclass:: ISC_QUAD 148 | :no-show-inheritance: 149 | 150 | .. autoclass:: ISC_QUAD_PTR 151 | :no-show-inheritance: 152 | 153 | .. autoclass:: FB_API_HANDLE 154 | :no-show-inheritance: 155 | 156 | .. autoclass:: FB_API_HANDLE_PTR 157 | :no-show-inheritance: 158 | 159 | .. autoclass:: ISC_STATUS 160 | :no-show-inheritance: 161 | 162 | .. autoclass:: ISC_STATUS_PTR 163 | :no-show-inheritance: 164 | 165 | .. autoclass:: ISC_STATUS_ARRAY 166 | :no-show-inheritance: 167 | 168 | .. autoclass:: ISC_STATUS_ARRAY_PTR 169 | :no-show-inheritance: 170 | 171 | .. autoclass:: ISC_ARRAY_BOUND 172 | :no-show-inheritance: 173 | 174 | .. autoclass:: ISC_ARRAY_DESC 175 | :no-show-inheritance: 176 | 177 | .. autoclass:: ISC_ARRAY_DESC_PTR 178 | :no-show-inheritance: 179 | 180 | .. autoclass:: RESULT_VECTOR 181 | :no-show-inheritance: 182 | 183 | .. autoclass:: ISC_TIME_TZ 184 | :no-show-inheritance: 185 | 186 | .. autoclass:: ISC_TIME_TZ_EX 187 | :no-show-inheritance: 188 | 189 | .. autoclass:: ISC_TIMESTAMP 190 | :no-show-inheritance: 191 | 192 | .. autoclass:: ISC_TIMESTAMP_TZ 193 | :no-show-inheritance: 194 | 195 | .. autoclass:: ISC_TIMESTAMP_TZ_EX 196 | :no-show-inheritance: 197 | 198 | .. autoclass:: TraceCounts 199 | :no-show-inheritance: 200 | 201 | .. autoclass:: PerformanceInfo 202 | :no-show-inheritance: 203 | 204 | Variables 205 | ========= 206 | 207 | .. autodata:: err_encoding 208 | :no-value: 209 | 210 | Functions 211 | ========= 212 | 213 | .. autofunction:: has_api 214 | .. autofunction:: load_api 215 | .. autofunction:: get_api 216 | 217 | Classes 218 | ======= 219 | 220 | .. autoclass:: FirebirdAPI 221 | 222 | Firebird API Interface definitions 223 | ================================== 224 | 225 | .. autoclass:: IVersioned_VTable 226 | :no-show-inheritance: 227 | 228 | .. autoclass:: IVersioned_struct 229 | :no-show-inheritance: 230 | 231 | .. autoclass:: IReferenceCounted_VTable 232 | :no-show-inheritance: 233 | 234 | .. autoclass:: IReferenceCounted_struct 235 | :no-show-inheritance: 236 | 237 | .. autoclass:: IDisposable_VTable 238 | :no-show-inheritance: 239 | 240 | .. autoclass:: IDisposable_struct 241 | :no-show-inheritance: 242 | 243 | .. autoclass:: IStatus_VTable 244 | :no-show-inheritance: 245 | 246 | .. autoclass:: IStatus_struct 247 | :no-show-inheritance: 248 | 249 | .. autoclass:: IMaster_VTable 250 | :no-show-inheritance: 251 | 252 | .. autoclass:: IMaster_struct 253 | :no-show-inheritance: 254 | 255 | .. autoclass:: IPluginBase_VTable 256 | :no-show-inheritance: 257 | 258 | .. autoclass:: IPluginBase_struct 259 | :no-show-inheritance: 260 | 261 | .. autoclass:: IPluginSet_VTable 262 | :no-show-inheritance: 263 | 264 | .. autoclass:: IPluginSet_struct 265 | :no-show-inheritance: 266 | 267 | .. autoclass:: IConfigEntry_VTable 268 | :no-show-inheritance: 269 | 270 | .. autoclass:: IConfigEntry_struct 271 | :no-show-inheritance: 272 | 273 | .. autoclass:: IConfig_VTable 274 | :no-show-inheritance: 275 | 276 | .. autoclass:: IConfig_struct 277 | :no-show-inheritance: 278 | 279 | .. autoclass:: IFirebirdConf_VTable 280 | :no-show-inheritance: 281 | 282 | .. autoclass:: IFirebirdConf_struct 283 | :no-show-inheritance: 284 | 285 | .. autoclass:: IPluginManager_VTable 286 | :no-show-inheritance: 287 | 288 | .. autoclass:: IPluginManager_struct 289 | :no-show-inheritance: 290 | 291 | .. autoclass:: IConfigManager_VTable 292 | :no-show-inheritance: 293 | 294 | .. autoclass:: IConfigManager_struct 295 | :no-show-inheritance: 296 | 297 | .. autoclass:: IEventCallback_VTable 298 | :no-show-inheritance: 299 | 300 | .. autoclass:: IEventCallback_struct 301 | :no-show-inheritance: 302 | 303 | .. autoclass:: IBlob_VTable 304 | :no-show-inheritance: 305 | 306 | .. autoclass:: IBlob_struct 307 | :no-show-inheritance: 308 | 309 | .. autoclass:: ITransaction_VTable 310 | :no-show-inheritance: 311 | 312 | .. autoclass:: ITransaction_struct 313 | :no-show-inheritance: 314 | 315 | .. autoclass:: IMessageMetadata_VTable 316 | :no-show-inheritance: 317 | 318 | .. autoclass:: IMessageMetadata_struct 319 | :no-show-inheritance: 320 | 321 | .. autoclass:: IMetadataBuilder_VTable 322 | :no-show-inheritance: 323 | 324 | .. autoclass:: IMetadataBuilder_struct 325 | :no-show-inheritance: 326 | 327 | .. autoclass:: IResultSet_VTable 328 | :no-show-inheritance: 329 | 330 | .. autoclass:: IResultSet_struct 331 | :no-show-inheritance: 332 | 333 | .. autoclass:: IStatement_VTable 334 | :no-show-inheritance: 335 | 336 | .. autoclass:: IStatement_struct 337 | :no-show-inheritance: 338 | 339 | .. autoclass:: IBatch_VTable 340 | :no-show-inheritance: 341 | 342 | .. autoclass:: IBatch_struct 343 | :no-show-inheritance: 344 | 345 | .. autoclass:: IBatchCompletionState_VTable 346 | :no-show-inheritance: 347 | 348 | .. autoclass:: IBatchCompletionState_struct 349 | :no-show-inheritance: 350 | 351 | .. autoclass:: IRequest_VTable 352 | :no-show-inheritance: 353 | 354 | .. autoclass:: IRequest_struct 355 | :no-show-inheritance: 356 | 357 | .. autoclass:: IEvents_VTable 358 | :no-show-inheritance: 359 | 360 | .. autoclass:: IEvents_struct 361 | :no-show-inheritance: 362 | 363 | .. autoclass:: IAttachment_VTable 364 | :no-show-inheritance: 365 | 366 | .. autoclass:: IAttachment_struct 367 | :no-show-inheritance: 368 | 369 | .. autoclass:: IService_VTable 370 | :no-show-inheritance: 371 | 372 | .. autoclass:: IService_struct 373 | :no-show-inheritance: 374 | 375 | .. autoclass:: IProvider_VTable 376 | :no-show-inheritance: 377 | 378 | .. autoclass:: IProvider_struct 379 | :no-show-inheritance: 380 | 381 | .. autoclass:: IDtcStart_VTable 382 | :no-show-inheritance: 383 | 384 | .. autoclass:: IDtcStart_struct 385 | :no-show-inheritance: 386 | 387 | .. autoclass:: IDtc_VTable 388 | :no-show-inheritance: 389 | 390 | .. autoclass:: IDtc_struct 391 | :no-show-inheritance: 392 | 393 | .. autoclass:: ICryptKeyCallback_VTable 394 | :no-show-inheritance: 395 | 396 | .. autoclass:: ICryptKeyCallback_struct 397 | :no-show-inheritance: 398 | 399 | .. autoclass:: ITimer_VTable 400 | :no-show-inheritance: 401 | 402 | .. autoclass:: ITimer_struct 403 | :no-show-inheritance: 404 | 405 | .. autoclass:: ITimerControl_VTable 406 | :no-show-inheritance: 407 | 408 | .. autoclass:: ITimerControl_struct 409 | :no-show-inheritance: 410 | 411 | .. autoclass:: IVersionCallback_VTable 412 | :no-show-inheritance: 413 | 414 | .. autoclass:: IVersionCallback_struct 415 | :no-show-inheritance: 416 | 417 | .. autoclass:: IUtil_VTable 418 | :no-show-inheritance: 419 | 420 | .. autoclass:: IUtil_struct 421 | :no-show-inheritance: 422 | 423 | .. autoclass:: IOffsetsCallback_VTable 424 | :no-show-inheritance: 425 | 426 | .. autoclass:: IOffsetsCallback_struct 427 | :no-show-inheritance: 428 | 429 | .. autoclass:: IXpbBuilder_VTable 430 | :no-show-inheritance: 431 | 432 | .. autoclass:: IXpbBuilder_struct 433 | :no-show-inheritance: 434 | 435 | .. autoclass:: IDecFloat16_VTable 436 | :no-show-inheritance: 437 | 438 | .. autoclass:: IDecFloat16_struct 439 | :no-show-inheritance: 440 | 441 | .. autoclass:: IDecFloat34_VTable 442 | :no-show-inheritance: 443 | 444 | .. autoclass:: IDecFloat34_struct 445 | :no-show-inheritance: 446 | 447 | .. autoclass:: IInt128_VTable 448 | :no-show-inheritance: 449 | 450 | .. autoclass:: IInt128_struct 451 | :no-show-inheritance: 452 | -------------------------------------------------------------------------------- /docs/ref-hooks.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.hooks 2 | :synopsis: Drivers hooks 3 | 4 | ===================== 5 | firebird.driver.hooks 6 | ===================== 7 | 8 | This module contains firebird-driver hooks. Uses hook mechanism from firebird-base package. 9 | 10 | Imports from `firebird.base.hooks`: `~firebird.base.hooks.register_class`, 11 | `~firebird.base.hooks.get_callbacks`, `~firebird.base.hooks.add_hook` and 12 | `~firebird.base.hooks.hook_manager`. 13 | 14 | Enums 15 | ===== 16 | 17 | .. autoclass:: APIHook 18 | .. autoclass:: ConnectionHook 19 | .. autoclass:: ServerHook 20 | -------------------------------------------------------------------------------- /docs/ref-intf.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.interfaces 2 | :synopsis: Interface wrappers for Firebird new API 3 | 4 | ========================== 5 | firebird.driver.interfaces 6 | ========================== 7 | 8 | This module contains interface wrappers for Firebird new API. 9 | 10 | .. important:: 11 | 12 | 1. Firebird OO API interfaces use inheritance, i.e. they could be inherited from other 13 | interface. In fact, all interfaces returned by Firebird are inherited from `IVersioned` 14 | interface. 15 | 16 | 2. Firebird OO API interfaces are versioned. Any addition to particular interface 17 | increases its version number. Application developers are responsible to check the 18 | version of returned interface to verify that it supports methods they want to use. 19 | 20 | If you want to use Firebird OO API interfaces directly in you application, read next 21 | section very carefuly. 22 | 23 | In Python driver, interfaces are represented as instances of interface wrapper classes 24 | that expose the methods provided by particular Firebird interface version. The wrapper 25 | class hierarchy thus represent not only inheritabce between Firebird interfaces, but also 26 | between versions of particular Firebird interface. 27 | 28 | Because all interfaces returned by Firebird are inherited from `IVersioned` interface, 29 | all wrapper classes have `VERSION` class attribute that contain version number of wrapped 30 | interface. 31 | 32 | Each Firebird interface has it's "canonical" Python wrapper with coresponding name. For 33 | example interface `IService` has wrapper class `.iService`. However, if there are multiple 34 | public versions of Firebird interface, there are multiple wrapper classes for each published 35 | interface version (interim, non-public versions used during Firebird development are skipped). 36 | These wrapper classes have names based on their canonical name with suffix that represent 37 | the interface version they wrap. It means that canonical wrapper **always** represents 38 | the highest interface version. 39 | 40 | Whenever Firebird interface is returned from Firebird OO API call, it's wrapped to its 41 | Python wrapper class according to interface type and version. The Python driver ensures 42 | that correct wrapper class is used according to returned interface version. 43 | 44 | However, this architecture has several important consequences: 45 | 46 | 1. The interface wrapper classes may change between driver releases as new interface versions 47 | are introduced. For example, driver versions up to 1.5.2 had only canonical `.iService` 48 | (version 3), but in version 1.6.0 it was renamed to `.iService_v3`, new wrappers 49 | `.iService_v4` and (new canonical) `.iService` (version 5) were added. 50 | 2. Instead using `isinstance` to check interface versions, you should always use 51 | `VERSION` attribute on wrapper class instance. 52 | 53 | 54 | Metaclasses 55 | =========== 56 | 57 | .. autoclass:: iVersionedMeta 58 | 59 | Firebird API Interface wrappers 60 | =============================== 61 | 62 | Base interfaces 63 | --------------- 64 | .. autoclass:: iVersioned 65 | .. autoclass:: iReferenceCounted 66 | .. autoclass:: iDisposable 67 | .. autoclass:: iStatus 68 | .. autoclass:: iPluginBase 69 | .. autoclass:: iMaster 70 | 71 | Configuration 72 | ------------- 73 | .. autoclass:: iConfigEntry 74 | .. autoclass:: iConfig 75 | .. autoclass:: iFirebirdConf_v3 76 | .. autoclass:: iFirebirdConf 77 | .. autoclass:: iConfigManager_v2 78 | .. autoclass:: iConfigManager 79 | 80 | Database and service attachments 81 | -------------------------------- 82 | .. autoclass:: iProvider 83 | .. autoclass:: iAttachment_v3 84 | .. autoclass:: iAttachment_v4 85 | .. autoclass:: iAttachment 86 | .. autoclass:: iService_v3 87 | .. autoclass:: iService_v4 88 | .. autoclass:: iService 89 | .. autoclass:: iXpbBuilder 90 | 91 | Blobs 92 | ----- 93 | .. autoclass:: iBlob_v3 94 | .. autoclass:: iBlob 95 | 96 | Transactions 97 | ------------ 98 | .. autoclass:: iTransaction_v3 99 | .. autoclass:: iTransaction 100 | .. autoclass:: iDtcStart 101 | .. autoclass:: iDtc 102 | 103 | Metadata 104 | -------- 105 | .. autoclass:: iMessageMetadata_v3 106 | .. autoclass:: iMessageMetadata 107 | .. autoclass:: iMetadataBuilder_v3 108 | .. autoclass:: iMetadataBuilder 109 | 110 | SQL execution 111 | ------------- 112 | .. autoclass:: iStatement_v3 113 | .. autoclass:: iStatement_v4 114 | .. autoclass:: iStatement 115 | .. autoclass:: iResultSet_v3 116 | .. autoclass:: iResultSet 117 | .. autoclass:: iBatch_v3 118 | .. autoclass:: iBatch 119 | .. autoclass:: iBatchCompletionState 120 | 121 | Events 122 | ------ 123 | .. autoclass:: iEvents_v3 124 | .. autoclass:: iEvents 125 | 126 | Utilities 127 | --------- 128 | .. autoclass:: iTimerControl 129 | .. autoclass:: iUtil_v2 130 | .. autoclass:: iUtil 131 | .. autoclass:: iDecFloat16 132 | .. autoclass:: iDecFloat34 133 | .. autoclass:: iInt128 134 | 135 | Other 136 | ----- 137 | .. autoclass:: iPluginManager 138 | .. autoclass:: iRequest_v3 139 | .. autoclass:: iRequest 140 | 141 | Interface implementations 142 | ========================= 143 | 144 | .. autoclass:: iVersionedImpl 145 | .. autoclass:: iReferenceCountedImpl 146 | .. autoclass:: iDisposableImpl 147 | .. autoclass:: iVersionCallbackImpl 148 | .. autoclass:: iCryptKeyCallbackImpl 149 | .. autoclass:: iOffsetsCallbackImp 150 | .. autoclass:: iEventCallbackImpl 151 | .. autoclass:: iTimerImpl 152 | 153 | -------------------------------------------------------------------------------- /docs/ref-main.txt: -------------------------------------------------------------------------------- 1 | ===================== 2 | Main driver namespace 3 | ===================== 4 | 5 | .. module:: firebird.driver 6 | :synopsis: Python 3+ Database API 2.0 Compliant driver for Firebird 3+ 7 | 8 | Constants 9 | ========= 10 | 11 | .. autodata:: __VERSION__ 12 | :no-value: 13 | 14 | 15 | Imports from sub-modules 16 | ======================== 17 | 18 | config 19 | ------ 20 | 21 | .. py:currentmodule:: firebird.driver.config 22 | 23 | Globals: `driver_config` 24 | 25 | core 26 | ---- 27 | 28 | .. py:currentmodule:: firebird.driver.core 29 | 30 | Functions: 31 | `connect()`, `create_database()`, `connect_server()`, `transaction()` and `tpb()` 32 | 33 | Translation dictionaries: 34 | `CHARSET_MAP` 35 | 36 | Classes: 37 | `DistributedTransactionManager`, `Connection`, `Cursor`, `Server` and `TPB` 38 | 39 | types 40 | ----- 41 | 42 | .. py:currentmodule:: firebird.driver.types 43 | 44 | Exceptions: 45 | `Warning`, `Error`, `InterfaceError`, `DatabaseError`, `DataError`, 46 | `OperationalError`, `IntegrityError`, `InternalError`, `ProgrammingError` 47 | and `NotSupportedError` 48 | 49 | Enums: 50 | `NetProtocol`, `DirectoryCode`, `PageSize`, `DBKeyScope`, `DbInfoCode`, `Features`, 51 | `TraInfoCode`, `ReplicaMode`, `StmtInfoCode`, `TraInfoIsolation`, 52 | `TraInfoReadCommitted`, `TraInfoAccess`, `TraIsolation`, `TraReadCommitted`, 53 | `TraLockResolution`, `TraAccessMode`, `TableShareMode`, `TableAccessMode`, `Isolation`, 54 | `DefaultAction`, `StatementType`, `BlobType`, `DbAccessMode`, `DbSpaceReservation`, 55 | `DbWriteMode`, `ShutdownMode`, `OnlineMode`, `ShutdownMethod`, `CancelType`, 56 | `DecfloatRound` and `DecfloatTraps` 57 | 58 | Sentinels: 59 | `~firebird.driver.core.TIMEOUT` 60 | 61 | Flags: 62 | `ServerCapability`, `SrvRepairFlag`, `SrvStatFlag`, `SrvBackupFlag`, 63 | `SrvRestoreFlag`, `SrvNBackupFlag`, `ConnectionFlag` and `EncryptionFlag` 64 | 65 | Globals and other objects required by Python DB API 2.0: 66 | `apilevel`, `threadsafety`, `paramstyle`, `Date`, `Time`, `Timestamp`, 67 | `DateFromTicks`, `TimeFromTicks`, `TimestampFromTicks`, `STRING`, `BINARY`, 68 | `NUMBER`, `DATETIME` and `ROWID` 69 | 70 | Helper constants: 71 | `DESCRIPTION_NAME`, `DESCRIPTION_TYPE_CODE`, `DESCRIPTION_DISPLAY_SIZE`, 72 | `DESCRIPTION_INTERNAL_SIZE`, `DESCRIPTION_PRECISION`, `DESCRIPTION_SCALE` 73 | and `DESCRIPTION_NULL_OK` 74 | 75 | Helper functions: 76 | `get_timezone()` 77 | 78 | fbapi 79 | ----- 80 | 81 | .. py:currentmodule:: firebird.driver.fbapi 82 | 83 | Functions `load_api()` and `get_api()`. 84 | 85 | -------------------------------------------------------------------------------- /docs/ref-types.txt: -------------------------------------------------------------------------------- 1 | .. module:: firebird.driver.types 2 | :synopsis: Firebird driver types 3 | 4 | ===================== 5 | firebird.driver.types 6 | ===================== 7 | 8 | Exceptions 9 | ========== 10 | 11 | Next exceptions are required by Python DB API 2.0 12 | 13 | `Error` is imported from `firebird.base.types`. 14 | 15 | .. autoexception:: Error 16 | .. autoexception:: InterfaceError 17 | .. autoexception:: DatabaseError 18 | .. autoexception:: DataError 19 | .. autoexception:: OperationalError 20 | .. autoexception:: IntegrityError 21 | .. autoexception:: InternalError 22 | .. autoexception:: ProgrammingError 23 | .. autoexception:: NotSupportedError 24 | .. autoexception:: FirebirdWarning 25 | 26 | This is the exception inheritance layout:: 27 | 28 | StandardError 29 | |__UserWarning 30 | |__FirebirdWarning 31 | |__Error 32 | |__InterfaceError 33 | |__DatabaseError 34 | |__DataError 35 | |__OperationalError 36 | |__IntegrityError 37 | |__InternalError 38 | |__ProgrammingError 39 | |__NotSupportedError 40 | 41 | Other constants and types required by Python DB API 2.0 specification 42 | ===================================================================== 43 | 44 | Globals 45 | ------- 46 | 47 | .. autodata:: apilevel 48 | :no-value: 49 | 50 | .. autodata:: threadsafety 51 | :no-value: 52 | 53 | .. autodata:: paramstyle 54 | :no-value: 55 | 56 | Helper constants for work with :attr:`.Cursor.description` content 57 | ------------------------------------------------------------------ 58 | 59 | - DESCRIPTION_NAME 60 | - DESCRIPTION_TYPE_CODE 61 | - DESCRIPTION_DISPLAY_SIZE 62 | - DESCRIPTION_INTERNAL_SIZE 63 | - DESCRIPTION_PRECISION 64 | - DESCRIPTION_SCALE 65 | - DESCRIPTION_NULL_OK 66 | 67 | Types 68 | ----- 69 | 70 | .. autodata:: STRING 71 | :no-value: 72 | 73 | .. autodata:: BINARY 74 | :no-value: 75 | 76 | .. autodata:: NUMBER 77 | :no-value: 78 | 79 | .. autodata:: DATETIME 80 | :no-value: 81 | 82 | .. autodata:: ROWID 83 | :no-value: 84 | 85 | Constructors for data types 86 | --------------------------- 87 | 88 | .. autodata:: Date 89 | .. autodata:: Time 90 | .. autodata:: Timestamp 91 | .. autofunction:: DateFromTicks 92 | .. autofunction:: TimeFromTicks 93 | .. autofunction:: TimestampFromTicks 94 | .. autodata:: Binary 95 | 96 | Types for type hints 97 | ==================== 98 | 99 | .. autodata:: DESCRIPTION 100 | .. autodata:: CB_OUTPUT_LINE 101 | .. autoclass:: Transactional 102 | 103 | Enums 104 | ===== 105 | 106 | .. autoclass:: NetProtocol 107 | .. autoclass:: DirectoryCode 108 | .. autoclass:: XpbKind 109 | .. autoclass:: StateResult 110 | .. autoclass:: PageSize 111 | .. autoclass:: DBKeyScope 112 | .. autoclass:: InfoItemType 113 | .. autoclass:: SrvInfoCode 114 | .. autoclass:: BlobInfoCode 115 | .. autoclass:: DbInfoCode 116 | .. autoclass:: ResultSetInfoCode 117 | .. autoclass:: Features 118 | .. autoclass:: ReplicaMode 119 | .. autoclass:: StmtInfoCode 120 | .. autoclass:: ReqInfoCode 121 | .. autoclass:: ReqState 122 | .. autoclass:: TraInfoCode 123 | .. autoclass:: TraInfoIsolation 124 | .. autoclass:: TraInfoReadCommitted 125 | .. autoclass:: TraInfoAccess 126 | .. autoclass:: TraAccessMode 127 | .. autoclass:: TraIsolation 128 | .. autoclass:: TraReadCommitted 129 | .. autoclass:: Isolation 130 | .. autoclass:: TraLockResolution 131 | .. autoclass:: TableShareMode 132 | .. autoclass:: TableAccessMode 133 | .. autoclass:: DefaultAction 134 | .. autoclass:: StatementType 135 | .. autoclass:: SQLDataType 136 | .. autoclass:: DPBItem 137 | .. autoclass:: TPBItem 138 | .. autoclass:: SPBItem 139 | .. autoclass:: BPBItem 140 | .. autoclass:: BlobType 141 | .. autoclass:: BlobStorage 142 | .. autoclass:: ServerAction 143 | .. autoclass:: SrvDbInfoOption 144 | .. autoclass:: SrvRepairOption 145 | .. autoclass:: SrvBackupOption 146 | .. autoclass:: SrvRestoreOption 147 | .. autoclass:: SrvNBackupOption 148 | .. autoclass:: SrvTraceOption 149 | .. autoclass:: SrvPropertiesOption 150 | .. autoclass:: SrvValidateOption 151 | .. autoclass:: SrvUserOption 152 | .. autoclass:: DbAccessMode 153 | .. autoclass:: DbSpaceReservation 154 | .. autoclass:: DbWriteMode 155 | .. autoclass:: ShutdownMode 156 | .. autoclass:: OnlineMode 157 | .. autoclass:: ShutdownMethod 158 | .. autoclass:: TransactionState 159 | .. autoclass:: DbProvider 160 | .. autoclass:: DbClass 161 | .. autoclass:: Implementation 162 | .. autoclass:: ImpCPU 163 | .. autoclass:: ImpOS 164 | .. autoclass:: ImpCompiler 165 | .. autoclass:: CancelType 166 | .. autoclass:: DecfloatRound 167 | .. autoclass:: DecfloatTraps 168 | 169 | Flags 170 | ===== 171 | 172 | .. autoclass:: StateFlag 173 | .. autoclass:: PreparePrefetchFlag 174 | .. autoclass:: StatementFlag 175 | .. autoclass:: CursorFlag 176 | .. autoclass:: ConnectionFlag 177 | .. autoclass:: EncryptionFlag 178 | .. autoclass:: ServerCapability 179 | .. autoclass:: SrvRepairFlag 180 | .. autoclass:: SrvStatFlag 181 | .. autoclass:: SrvBackupFlag 182 | .. autoclass:: SrvRestoreFlag 183 | .. autoclass:: SrvNBackupFlag 184 | .. autoclass:: SrvPropertiesFlag 185 | .. autoclass:: ImpFlags 186 | 187 | Dataclasses 188 | =========== 189 | 190 | .. autoclass:: ItemMetadata 191 | :no-members: 192 | 193 | .. autoclass:: TableAccessStats 194 | :no-members: 195 | 196 | .. autoclass:: UserInfo 197 | :no-members: 198 | 199 | .. autoclass:: BCD 200 | :no-members: 201 | 202 | .. autoclass:: TraceSession 203 | :no-members: 204 | 205 | .. autoclass:: ImpData 206 | :no-members: 207 | 208 | .. autoclass:: ImpDataOld 209 | :no-members: 210 | 211 | Helper functions 212 | ================ 213 | 214 | .. autofunction:: get_timezone 215 | -------------------------------------------------------------------------------- /docs/reference.txt: -------------------------------------------------------------------------------- 1 | 2 | ######################### 3 | Firebird-driver Reference 4 | ######################### 5 | 6 | Driver modules 7 | ============== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | ref-main 13 | ref-types 14 | ref-config 15 | ref-core 16 | ref-hooks 17 | ref-intf 18 | ref-fbapi 19 | 20 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-bootstrap-theme>=0.8.1 2 | sphinx-autodoc-typehints>=1.24.0 3 | sphinx==7.2.6 4 | . 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "firebird-driver" 7 | description = "Firebird driver for Python" 8 | dynamic = ["version"] 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | license = { file = "LICENSE" } 12 | authors = [ 13 | { name = "Pavel Cisar", email = "pcisar@users.sourceforge.net"}, 14 | ] 15 | keywords = ["Firebird", "RDBMS", "driver"] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Operating System :: POSIX :: Linux", 25 | "Operating System :: Microsoft :: Windows", 26 | "Operating System :: MacOS", 27 | "Topic :: Software Development", 28 | "Topic :: Database", 29 | ] 30 | dependencies = [ 31 | "firebird-base~=2.0", 32 | "python-dateutil~=2.8", 33 | ] 34 | 35 | [project.urls] 36 | Home = "https://github.com/FirebirdSQL/python3-driver" 37 | Documentation = "https://firebird-driver.rtfd.io" 38 | Issues = "https://github.com/FirebirdSQL/python3-driver/issues" 39 | Funding = "https://github.com/sponsors/pcisar" 40 | Source = "https://github.com/FirebirdSQL/python3-driver" 41 | 42 | [tool.hatch.version] 43 | path = "src/firebird/driver/__init__.py" 44 | 45 | [tool.hatch.build.targets.sdist] 46 | include = ["src"] 47 | 48 | [tool.hatch.build.targets.wheel] 49 | packages = ["src/firebird"] 50 | 51 | [tool.hatch.metadata] 52 | allow-direct-references = true 53 | 54 | [tool.hatch.envs.default] 55 | dependencies = [ 56 | ] 57 | 58 | [tool.hatch.envs.hatch-test] 59 | extra-args = ["--host=localhost"] 60 | extra-dependencies = [ 61 | "packaging>=25.0", 62 | ] 63 | 64 | [[tool.hatch.envs.hatch-test.matrix]] 65 | python = ["3.11", "3.12", "3.13"] 66 | 67 | [tool.hatch.envs.doc] 68 | detached = false 69 | platforms = ["linux"] 70 | dependencies = [ 71 | "Sphinx==7.2.6", 72 | "sphinx-bootstrap-theme>=0.8.1", 73 | "sphinx-autodoc-typehints>=1.24.0", 74 | "doc2dash>=3.0.0" 75 | ] 76 | [tool.hatch.envs.doc.scripts] 77 | build = "cd docs ; make html" 78 | docset = [ 79 | "cd docs ; doc2dash -u https://firebird-driver.readthedocs.io/en/latest/ -f -i ./_static/fb-favicon.png -n firebird-driver ./_build/html/", 80 | "cd docs; VERSION=`hatch version` ; tar --exclude='.DS_Store' -cvzf ../dist/firebird-driver-$VERSION-docset.tgz firebird-driver.docset", 81 | ] 82 | 83 | [tool.ruff] 84 | target-version = "py311" 85 | line-length = 120 86 | 87 | [tool.ruff.lint] 88 | select = ["A", "ARG", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "ISC", "N", 89 | "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "T", "TID", "UP", "W", "YTT", 90 | ] 91 | ignore = [ 92 | # Allow non-abstract empty methods in abstract base classes 93 | "B027", 94 | # Allow boolean positional values in function calls, like `dict.get(... True)` 95 | "FBT003", 96 | # Ignore checks for possible passwords 97 | "S105", "S106", "S107", 98 | # Ignore complexity 99 | "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", 100 | # 101 | "E741", 102 | # Allow relative imports 103 | "TID252", 104 | # Allow literals in exceptions 105 | "EM101", "EM102", 106 | # Single quotes instead double 107 | "Q000" 108 | ] 109 | unfixable = [ 110 | # Don't touch unused imports 111 | "F401", 112 | # Don't change single quotes to double 113 | "Q000" 114 | ] 115 | exclude = ["*_pb2.py", "*.pyi", "tests/*", "docs/*", "work/*"] 116 | 117 | [tool.ruff.lint.isort] 118 | known-first-party = ["firebird.driver", "firebird.base"] 119 | 120 | [tool.ruff.lint.flake8-tidy-imports] 121 | ban-relative-imports = "all" 122 | 123 | [tool.ruff.lint.per-file-ignores] 124 | # Tests can use magic values, assertions, and relative imports 125 | "test_*" = ["PLR2004", "S101", "TID252"] 126 | "fbapi.py" = ["N801", "E501"] 127 | "interfaces.py" = ["ARG001", "ARG002", "N801", "N803", "E501", "FBT001"] 128 | "hooks.py" = ["F401"] 129 | "core.py" = ["PLR2004", "DTZ007", "S104", "B028", "E501"] 130 | "config.py" = ["E501"] 131 | "__init__.py" = ["F401"] 132 | 133 | [tool.coverage.run] 134 | source_pkgs = ["firebird.driver", "tests"] 135 | branch = true 136 | parallel = true 137 | omit = [ 138 | "src/firebird/driver/__about__.py", 139 | ] 140 | 141 | [tool.coverage.paths] 142 | firebird_base = ["src/firebird/driver"] 143 | tests = ["tests"] 144 | 145 | [tool.coverage.report] 146 | exclude_lines = [ 147 | "no cov", 148 | "if __name__ == .__main__.:", 149 | "if TYPE_CHECKING:", 150 | ] 151 | -------------------------------------------------------------------------------- /src/firebird/driver/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: firebird/driver/__init__.py 7 | # DESCRIPTION: The Firebird driver for Python 3 8 | # CREATED: 4.3.2020 9 | # 10 | # The contents of this file are subject to the MIT License 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documentation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furnished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in all 20 | # copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | # SOFTWARE. 29 | # 30 | # Copyright (c) 2020 Firebird Project (www.firebirdsql.org) 31 | # All Rights Reserved. 32 | # 33 | # Contributor(s): Pavel Císař (original code) 34 | # ______________________________________ 35 | 36 | """firebird-driver - The Firebird driver for Python 3 37 | 38 | 39 | """ 40 | from .config import DatabaseConfig, DriverConfig, ServerConfig, driver_config 41 | from .core import ( 42 | CHARSET_MAP, 43 | TIMEOUT, 44 | TPB, 45 | Connection, 46 | Cursor, 47 | DistributedTransactionManager, 48 | Server, 49 | Statement, 50 | TransactionManager, 51 | connect, 52 | connect_server, 53 | create_database, 54 | temp_database, 55 | tpb, 56 | transaction, 57 | ) 58 | from .fbapi import get_api, load_api 59 | from .types import ( 60 | BINARY, 61 | DATETIME, 62 | DESCRIPTION_DISPLAY_SIZE, 63 | DESCRIPTION_INTERNAL_SIZE, 64 | DESCRIPTION_NAME, 65 | DESCRIPTION_NULL_OK, 66 | DESCRIPTION_PRECISION, 67 | DESCRIPTION_SCALE, 68 | DESCRIPTION_TYPE_CODE, 69 | NUMBER, 70 | ROWID, 71 | STRING, 72 | BlobType, 73 | CancelType, 74 | ConnectionFlag, 75 | DatabaseError, 76 | DataError, 77 | Date, 78 | DateFromTicks, 79 | DbAccessMode, 80 | DbInfoCode, 81 | DBKeyScope, 82 | DbSpaceReservation, 83 | DbWriteMode, 84 | DecfloatRound, 85 | DecfloatTraps, 86 | DefaultAction, 87 | DirectoryCode, 88 | EncryptionFlag, 89 | Error, 90 | Features, 91 | FirebirdWarning, 92 | IntegrityError, 93 | InterfaceError, 94 | InternalError, 95 | Isolation, 96 | NetProtocol, 97 | NotSupportedError, 98 | OnlineMode, 99 | OperationalError, 100 | PageSize, 101 | ProgrammingError, 102 | ReplicaMode, 103 | ResultSetInfoCode, 104 | ServerCapability, 105 | ShutdownMethod, 106 | ShutdownMode, 107 | SrvBackupFlag, 108 | SrvInfoCode, 109 | SrvNBackupFlag, 110 | SrvRepairFlag, 111 | SrvRestoreFlag, 112 | SrvStatFlag, 113 | StatementType, 114 | StmtInfoCode, 115 | TableAccessMode, 116 | TableShareMode, 117 | Time, 118 | TimeFromTicks, 119 | Timestamp, 120 | TimestampFromTicks, 121 | TraAccessMode, 122 | TraInfoAccess, 123 | TraInfoCode, 124 | TraInfoIsolation, 125 | TraInfoReadCommitted, 126 | TraIsolation, 127 | TraLockResolution, 128 | TraReadCommitted, 129 | apilevel, 130 | get_timezone, 131 | paramstyle, 132 | threadsafety, 133 | ) 134 | 135 | #: Current driver version, SEMVER string. 136 | __VERSION__ = '2.0.2' 137 | -------------------------------------------------------------------------------- /src/firebird/driver/hooks.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: firebird/driver/hooks.py 7 | # DESCRIPTION: Drivers hooks 8 | # CREATED: 24.3.2020 9 | # 10 | # The contents of this file are subject to the MIT License 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documentation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furnished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in all 20 | # copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | # SOFTWARE. 29 | # 30 | # Copyright (c) 2020 Firebird Project (www.firebirdsql.org) 31 | # All Rights Reserved. 32 | # 33 | # Contributor(s): Pavel Císař (original code) 34 | # ______________________________________ 35 | 36 | """firebird-driver - Driver Hooks 37 | 38 | This module defines specific hook points (events) within the firebird-driver 39 | lifecycle where custom functions can be registered and executed. These hooks 40 | allow for extending or modifying driver behavior, logging, or monitoring. 41 | 42 | Hooks are registered using `firebird.driver.add_hook()` or the `firebird.base.hooks.hook_manager`. 43 | The specific signature required for each hook function and the context in which 44 | it's called are documented within the driver methods that trigger these hooks 45 | (primarily in `firebird.driver.core`). 46 | """ 47 | 48 | from __future__ import annotations 49 | 50 | from enum import Enum, auto 51 | 52 | from firebird.base.hooks import add_hook, get_callbacks, hook_manager, register_class 53 | 54 | 55 | class APIHook(Enum): 56 | """Hooks related to the loading and initialization of the underlying Firebird client API. 57 | """ 58 | #: Called after the Firebird client library has been successfully loaded and basic interfaces obtained. 59 | LOADED = auto() 60 | 61 | class ConnectionHook(Enum): 62 | """Hooks related to the lifecycle of a database connection (attachment, detachment, dropping). 63 | """ 64 | #: Called before attempting to attach to a database, allows interception or modification. 65 | ATTACH_REQUEST = auto() 66 | #: Called after a database connection (attachment) has been successfully established. 67 | ATTACHED = auto() 68 | #: Called before attempting to detach from a database, allows cancellation. 69 | DETACH_REQUEST = auto() 70 | #: Called after a database connection has been successfully closed (detached). 71 | CLOSED = auto() 72 | #: Called after a database has been successfully dropped. 73 | DROPPED = auto() 74 | 75 | class ServerHook(Enum): 76 | """Hooks related to the lifecycle of a service manager connection. 77 | """ 78 | #: Called after connecting to the Firebird service manager. 79 | ATTACHED = auto() 80 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-base 6 | # FILE: tests/conftest.py 7 | # DESCRIPTION: Common fixtures 8 | # CREATED: 28.1.2025 9 | # 10 | # The contents of this file are subject to the MIT License 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documentation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furnished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in all 20 | # copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | # SOFTWARE. 29 | # 30 | # Copyright (c) 2025 Firebird Project (www.firebirdsql.org) 31 | # All Rights Reserved. 32 | # 33 | # Contributor(s): Pavel Císař (original code) 34 | # ______________________________________. 35 | 36 | from __future__ import annotations 37 | 38 | from pathlib import Path 39 | import platform 40 | from shutil import copyfile 41 | from configparser import ConfigParser 42 | 43 | import pytest 44 | from packaging.specifiers import SpecifierSet 45 | from packaging.version import parse 46 | from firebird.base.config import EnvExtendedInterpolation 47 | from firebird.driver import driver_config, get_api, connect_server, connect, DbInfoCode 48 | from firebird.base.config import ConfigProto 49 | 50 | _vars_: dict = {'client-lib': None, 51 | 'firebird-config': None, 52 | 'server': None, 53 | 'host': None, 54 | 'port': None, 55 | 'user': 'SYSDBA', 56 | 'password': 'masterkey', 57 | } 58 | 59 | _platform: str = platform.system() 60 | 61 | # Configuration 62 | 63 | def pytest_addoption(parser, pluginmanager): 64 | """Adds specific pytest command-line options. 65 | 66 | .. seealso:: `pytest documentation <_pytest.hookspec.pytest_addoption>` for details. 67 | """ 68 | grp = parser.getgroup('firebird', "Firebird driver QA", 'general') 69 | grp.addoption('--host', help="Server host", default=None, required=False) 70 | grp.addoption('--port', help="Server port", default=None, required=False) 71 | grp.addoption('--client-lib', help="Firebird client library", default=None, required=False) 72 | grp.addoption('--server', help="Server configuration name", default='', required=False) 73 | grp.addoption('--driver-config', help="Firebird driver configuration filename", default=None) 74 | 75 | @pytest.hookimpl(trylast=True) 76 | def pytest_configure(config): 77 | """General configuration. 78 | 79 | .. seealso:: `pytest documentation <_pytest.hookspec.pytest_configure>` for details. 80 | """ 81 | if config.getoption('help'): 82 | return 83 | # Base paths 84 | root_path: Path = Path(config.rootpath) 85 | _vars_['root'] = root_path 86 | path = config.rootpath / 'tests' / 'databases' 87 | _vars_['databases'] = path if path.is_dir() else config.rootpath / 'tests' 88 | path = config.rootpath / 'tests' / 'backups' 89 | _vars_['backups'] = path if path.is_dir() else config.rootpath / 'tests' 90 | path = config.rootpath / 'tests' / 'files' 91 | _vars_['files'] = path if path.is_dir() else config.rootpath / 'tests' 92 | # Driver configuration 93 | db_config = driver_config.register_database('pytest') 94 | if server := config.getoption('server'): 95 | db_config.server.value = server 96 | _vars_['server'] = server 97 | 98 | config_path: Path = root_path / 'tests' / 'firebird-driver.conf' 99 | if cfg_path := config.getoption('driver_config'): 100 | config_path = Path(cfg_path) 101 | if config_path.is_file(): 102 | driver_config.read(str(config_path)) 103 | _vars_['firebird-config'] = config_path 104 | srv_conf = driver_config.get_server(_vars_['server']) 105 | _vars_['host'] = srv_conf.host.value 106 | _vars_['port'] = srv_conf.port.value 107 | _vars_['user'] = srv_conf.user.value 108 | _vars_['password'] = srv_conf.password.value 109 | # Handle server-specific "fb_client_library" configuration option 110 | #_vars_['client-lib'] = 'UNKNOWN' 111 | cfg = ConfigParser(interpolation=EnvExtendedInterpolation()) 112 | cfg.read(str(config_path)) 113 | if cfg.has_option(_vars_['server'], 'fb_client_library'): 114 | fbclient = Path(cfg.get(_vars_['server'], 'fb_client_library')) 115 | if not fbclient.is_file(): 116 | pytest.exit(f"Client library '{fbclient}' not found!") 117 | driver_config.fb_client_library.value = str(fbclient) 118 | cfg.clear() 119 | else: 120 | # No configuration file, so we process 'host' and 'client-lib' options 121 | if client_lib := config.getoption('client_lib'): 122 | client_lib = Path(client_lib) 123 | if not client_lib.is_file(): 124 | pytest.exit(f"Client library '{client_lib}' not found!") 125 | driver_config.fb_client_library.value = client_lib 126 | # 127 | if host := config.getoption('host'): 128 | _vars_['host'] = host 129 | _vars_['port'] = config.getoption('port') 130 | driver_config.server_defaults.host.value = config.getoption('host') 131 | driver_config.server_defaults.port.value = config.getoption('port') 132 | driver_config.server_defaults.user.value = 'SYSDBA' 133 | driver_config.server_defaults.password.value = 'masterkey' 134 | # THIS should load the driver API, do not connect db or server earlier! 135 | _vars_['client-lib'] = get_api().client_library_name 136 | # Information from server 137 | with connect_server('') as srv: 138 | version = parse(srv.info.version.replace('-dev', '')) 139 | _vars_['version'] = version 140 | _vars_['home-dir'] = Path(srv.info.home_directory) 141 | bindir = _vars_['home-dir'] / 'bin' 142 | if not bindir.exists(): 143 | bindir = _vars_['home-dir'] 144 | _vars_['bin-dir'] = bindir 145 | _vars_['lock-dir'] = Path(srv.info.lock_directory) 146 | _vars_['bin-dir'] = Path(bindir) if bindir else _vars_['home-dir'] 147 | _vars_['security-db'] = Path(srv.info.security_database) 148 | _vars_['arch'] = srv.info.architecture 149 | # Create copy of test database 150 | if version in SpecifierSet('>=3.0, <4'): 151 | source_filename = 'fbtest30.fdb' 152 | elif version in SpecifierSet('>=4.0, <5'): 153 | source_filename = 'fbtest40.fdb' 154 | elif version in SpecifierSet('>=5.0, <6'): 155 | source_filename = 'fbtest50.fdb' 156 | else: 157 | pytest.exit(f"Unsupported Firebird version {version}") 158 | source_db_file: Path = _vars_['databases'] / source_filename 159 | if not source_db_file.is_file(): 160 | pytest.exit(f"Source test database '{source_db_file}' not found!") 161 | _vars_['source_db'] = source_db_file 162 | 163 | def pytest_report_header(config): 164 | """Returns plugin-specific test session header. 165 | 166 | .. seealso:: `pytest documentation <_pytest.hookspec.pytest_report_header>` for details. 167 | """ 168 | return ["Firebird:", 169 | f" configuration: {_vars_['firebird-config']}", 170 | f" server: {_vars_['server']} [v{_vars_['version']}, {_vars_['arch']}]", 171 | f" host: {_vars_['host']}", 172 | f" home: {_vars_['home-dir']}", 173 | f" bin: {_vars_['bin-dir']}", 174 | f" client library: {_vars_['client-lib']}", 175 | f" test database: {_vars_['source_db']}", 176 | ] 177 | 178 | @pytest.fixture(scope='session') 179 | def fb_vars(): 180 | yield _vars_ 181 | 182 | @pytest.fixture(scope='session') 183 | def tmp_dir(tmp_path_factory): 184 | path = tmp_path_factory.mktemp('db') 185 | if _platform != 'Windows': 186 | wdir = path 187 | while wdir is not wdir.parent: 188 | try: 189 | wdir.chmod(16895) 190 | except: 191 | pass 192 | wdir = wdir.parent 193 | yield path 194 | 195 | @pytest.fixture(scope='session', autouse=True) 196 | def db_file(tmp_dir): 197 | test_db_filename: Path = tmp_dir / 'test-db.fdb' 198 | copyfile(_vars_['source_db'], test_db_filename) 199 | if _platform != 'Windows': 200 | test_db_filename.chmod(33206) 201 | driver_config.get_database('pytest').database.value = str(test_db_filename) 202 | return test_db_filename 203 | 204 | @pytest.fixture(scope='session') 205 | def dsn(db_file): 206 | host = _vars_['host'] 207 | port = _vars_['port'] 208 | if host is None: 209 | result = str(db_file) 210 | else: 211 | result = f'{host}/{port}:{db_file}' if port else f'{host}:{db_file}' 212 | yield result 213 | 214 | @pytest.fixture() 215 | def driver_cfg(tmp_path_factory): 216 | proto = ConfigProto() 217 | driver_config.save_proto(proto) 218 | yield driver_config 219 | driver_config.load_proto(proto) 220 | 221 | @pytest.fixture 222 | def db_connection(driver_cfg): 223 | conn = connect('pytest') 224 | yield conn 225 | if not conn.is_closed(): 226 | conn.close() 227 | 228 | @pytest.fixture(autouse=True) 229 | def db_cleanup(db_connection): 230 | # Clean common test tables before each test using this fixture 231 | try: 232 | with db_connection.cursor() as cur: 233 | cur.execute("delete from t") 234 | cur.execute("delete from t2") 235 | cur.execute("delete from FB4") 236 | db_connection.commit() 237 | except Exception as e: 238 | # Ignore errors if tables don't exist, log others 239 | if "Table unknown" not in str(e): 240 | print(f"Warning: Error during pre-test cleanup: {e}") 241 | 242 | @pytest.fixture 243 | def server_connection(fb_vars): 244 | with connect_server(fb_vars['host'], user=fb_vars['user'], password=fb_vars['password']) as svc: 245 | yield svc 246 | -------------------------------------------------------------------------------- /tests/fbtest30-base.fbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest30-base.fbk -------------------------------------------------------------------------------- /tests/fbtest30-src.fbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest30-src.fbk -------------------------------------------------------------------------------- /tests/fbtest30.fdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest30.fdb -------------------------------------------------------------------------------- /tests/fbtest40-base.fbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest40-base.fbk -------------------------------------------------------------------------------- /tests/fbtest40.fdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest40.fdb -------------------------------------------------------------------------------- /tests/fbtest50-base.fbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest50-base.fbk -------------------------------------------------------------------------------- /tests/fbtest50.fdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebirdSQL/python3-driver/c8293502226d499baf33c1dd8dcb94bf56482769/tests/fbtest50.fdb -------------------------------------------------------------------------------- /tests/test_array.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_array.py 7 | # DESCRIPTION: Tests for Array type 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import datetime 26 | import decimal 27 | import pytest 28 | from firebird.driver import InterfaceError, DatabaseError 29 | 30 | # Common data setup 31 | c2 = [[[1, 1], [2, 2], [3, 3], [4, 4]], [[5, 5], [6, 6], [7, 7], [8, 8]], [[9, 9], [10, 10], [11, 11], [12, 12]], [[13, 13], [14, 14], [15, 15], [16, 16]]] 32 | c3 = [['a', 'a'], ['bb', 'bb'], ['ccc', 'ccc'], ['dddd', 'dddd'], ['eeeee', 'eeeee'], ['fffffff78901234', 'fffffff78901234']] 33 | c4 = ['a ', 'bb ', 'ccc ', 'dddd ', 'eeeee'] 34 | c5 = [datetime.datetime(2012, 11, 22, 12, 8, 24, 474800), datetime.datetime(2012, 11, 22, 12, 8, 24, 474800)] 35 | c6 = [datetime.time(12, 8, 24, 474800), datetime.time(12, 8, 24, 474800)] 36 | c7 = [decimal.Decimal('10.22'), decimal.Decimal('100000.33')] 37 | c8 = [decimal.Decimal('10.22'), decimal.Decimal('100000.33')] 38 | c9 = [1, 0] 39 | c10 = [5555555, 7777777] 40 | c11 = [3.140000104904175, 3.140000104904175] 41 | c12 = [3.14, 3.14] 42 | c13 = [decimal.Decimal('10.2'), decimal.Decimal('100000.3')] 43 | c14 = [decimal.Decimal('10.22222'), decimal.Decimal('100000.333')] 44 | c15 = [decimal.Decimal('1000000000000.22222'), decimal.Decimal('1000000000000.333')] 45 | c16 = [True, False, True] 46 | 47 | @pytest.fixture(autouse=True) 48 | def setup_array_test(db_connection): 49 | con = db_connection 50 | # Ensure table exists or skip 51 | try: 52 | with con.cursor() as cur: 53 | # Simplified check, assume table exists if no error 54 | cur.execute("SELECT c1 FROM AR WHERE 1=0") 55 | except DatabaseError as e: 56 | if "Table unknown AR" in str(e): 57 | pytest.skip("Table 'AR' needed for array tests does not exist.") 58 | else: 59 | raise 60 | # Insert initial data needed for read tests 61 | with con.cursor() as cur: 62 | cur.execute("delete from AR") # Clean first 63 | cur.execute("insert into ar (c1,c2) values (2,?)",[c2]) 64 | cur.execute("insert into ar (c1,c3) values (3,?)",[c3]) 65 | cur.execute("insert into ar (c1,c4) values (4,?)",[c4]) 66 | cur.execute("insert into ar (c1,c5) values (5,?)",[c5]) 67 | cur.execute("insert into ar (c1,c6) values (6,?)",[c6]) 68 | cur.execute("insert into ar (c1,c7) values (7,?)",[c7]) 69 | cur.execute("insert into ar (c1,c8) values (8,?)",[c8]) 70 | cur.execute("insert into ar (c1,c9) values (9,?)",[c9]) 71 | cur.execute("insert into ar (c1,c10) values (10,?)",[c10]) 72 | cur.execute("insert into ar (c1,c11) values (11,?)",[c11]) 73 | cur.execute("insert into ar (c1,c12) values (12,?)",[c12]) 74 | cur.execute("insert into ar (c1,c13) values (13,?)",[c13]) 75 | cur.execute("insert into ar (c1,c14) values (14,?)",[c14]) 76 | cur.execute("insert into ar (c1,c15) values (15,?)",[c15]) 77 | con.commit() 78 | yield 79 | 80 | def test_basic(db_connection): 81 | with db_connection.cursor() as cur: 82 | cur.execute("select LANGUAGE_REQ from job "\ 83 | "where job_code='Eng' and job_grade=3 and job_country='Japan'") 84 | row = cur.fetchone() 85 | assert row == (['Japanese\n', 'Mandarin\n', 'English\n', '\n', '\n'],) 86 | cur.execute('select QUART_HEAD_CNT from proj_dept_budget') 87 | row = cur.fetchall() 88 | # ... (assert list contents) ... 89 | assert len(row) > 10 # Example check 90 | 91 | def test_read_full(db_connection): 92 | with db_connection.cursor() as cur: 93 | cur.execute("select c1,c2 from ar where c1=2") 94 | row = cur.fetchone() 95 | assert row[1] == c2 96 | # ... (rest of the read tests using assert) ... 97 | cur.execute("select c1,c15 from ar where c1=15") 98 | row = cur.fetchone() 99 | assert row[1] == c15 100 | 101 | def test_write_full(db_connection): 102 | with db_connection.cursor() as cur: 103 | # INTEGER 104 | cur.execute("insert into ar (c1,c2) values (102,?)", [c2]) 105 | db_connection.commit() 106 | cur.execute("select c1,c2 from ar where c1=102") 107 | row = cur.fetchone() 108 | assert row[1] == c2 109 | # ... (rest of the write tests using assert) ... 110 | # BOOLEAN 111 | cur.execute("insert into ar (c1,c16) values (116,?)", [c16]) 112 | db_connection.commit() 113 | cur.execute("select c1,c16 from ar where c1=116") 114 | row = cur.fetchone() 115 | assert row[1] == c16 116 | 117 | def test_write_wrong(db_connection): 118 | with db_connection.cursor() as cur: 119 | with pytest.raises(ValueError, match='Incorrect ARRAY field value.'): 120 | cur.execute("insert into ar (c1,c2) values (102,?)", [c3]) # Wrong type 121 | with pytest.raises(ValueError, match='Incorrect ARRAY field value.'): 122 | cur.execute("insert into ar (c1,c2) values (102,?)", [c2[:-1]]) # Wrong dimensions 123 | -------------------------------------------------------------------------------- /tests/test_blob.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_blob.py 7 | # DESCRIPTION: Tests for stream BLOBs 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | from io import StringIO 26 | import pytest 27 | from firebird import driver 28 | 29 | def test_stream_blob_basic(db_connection): 30 | blob_content = """Firebird supports two types of blobs, stream and segmented. 31 | The database stores segmented blobs in chunks. 32 | Each chunk starts with a two byte length indicator followed by however many bytes of data were passed as a segment. 33 | Stream blobs are stored as a continuous array of data bytes with no length indicators included.""" 34 | blob_lines = StringIO(blob_content).readlines() 35 | 36 | with db_connection.cursor() as cur: 37 | # Use StringIO for inserting stream-like data 38 | cur.execute('insert into T2 (C1,C9) values (?,?)', [4, StringIO(blob_content)]) 39 | db_connection.commit() 40 | 41 | p = cur.prepare('select C1,C9 from T2 where C1 = 4') 42 | cur.stream_blobs.append('C9') # Request C9 as stream 43 | cur.execute(p) 44 | row = cur.fetchone() 45 | assert row is not None 46 | blob_reader = row[1] 47 | assert isinstance(blob_reader, driver.core.BlobReader) 48 | 49 | with blob_reader: # Use context manager for BlobReader 50 | assert isinstance(blob_reader.blob_id, driver.fbapi.ISC_QUAD) 51 | # assert blob_reader.blob_type == BlobType.STREAM # Type might not be exposed directly 52 | assert blob_reader.is_text() 53 | assert blob_reader.read(20) == 'Firebird supports tw' 54 | assert blob_reader.read(20) == 'o types of blobs, st' 55 | # ... (rest of the read/seek assertions) ... 56 | assert blob_reader.read() == blob_content[40:] # Read remainder 57 | assert blob_reader.tell() == len(blob_content) 58 | blob_reader.seek(0) 59 | assert blob_reader.tell() == 0 60 | assert blob_reader.readlines() == blob_lines 61 | blob_reader.seek(0) 62 | read_lines = list(blob_reader) # Iterate directly 63 | assert read_lines == blob_lines 64 | 65 | def test_stream_blob_extended(db_connection): 66 | blob_content = "Another test blob content." * 5 # Make it slightly longer 67 | with db_connection.cursor() as cur: 68 | cur.execute('insert into T2 (C1,C9) values (?,?)', [1, StringIO(blob_content)]) 69 | cur.execute('insert into T2 (C1,C9) values (?,?)', [2, StringIO(blob_content)]) 70 | db_connection.commit() 71 | 72 | p = cur.prepare('select C1,C9 from T2 where C1 in (1, 2)') 73 | cur.stream_blobs.append('C9') 74 | cur.execute(p) 75 | count = 0 76 | for row in cur: 77 | count += 1 78 | assert row[0] in (1, 2) 79 | blob_reader = row[1] 80 | assert isinstance(blob_reader, driver.core.BlobReader) 81 | with blob_reader: 82 | assert blob_reader.read() == blob_content 83 | assert count == 2 # Ensure both rows were processed 84 | -------------------------------------------------------------------------------- /tests/test_charset_conv.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_charset_conv.py 7 | # DESCRIPTION: Tests for Character Set conversions 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from firebird.driver.core import BlobReader 27 | from firebird.driver import connect, DatabaseError 28 | 29 | @pytest.fixture 30 | def utf8_connection(dsn): 31 | # Separate connection with UTF8 charset 32 | with connect(dsn, charset='utf-8') as con_utf8: 33 | yield con_utf8 34 | 35 | @pytest.fixture(autouse=True) 36 | def setup_charset_test(db_connection): 37 | # Clean tables 38 | with db_connection.cursor() as cur: 39 | cur.execute("delete from t3") 40 | cur.execute("delete from t4") 41 | db_connection.commit() 42 | yield 43 | 44 | def test_octets(db_connection): # Request fixture 45 | bytestring = bytes([1, 2, 3, 4, 5]) 46 | with db_connection.cursor() as cur: 47 | cur.execute("insert into T4 (C1, C_OCTETS, V_OCTETS) values (?,?,?)", 48 | (1, bytestring, bytestring)) 49 | db_connection.commit() 50 | cur.execute("select C1, C_OCTETS, V_OCTETS from T4 where C1 = 1") 51 | row = cur.fetchone() 52 | assert row == (1, b'\x01\x02\x03\x04\x05', b'\x01\x02\x03\x04\x05') 53 | 54 | def test_utf82win1250(dsn, utf8_connection): 55 | s5 = 'ěščřž' 56 | s30 = 'ěščřžýáíéúůďťňóĚŠČŘŽÝÁÍÉÚŮĎŤŇÓ' 57 | 58 | # Create the win1250 connection within the test if not provided by fixture 59 | with connect(dsn, charset='win1250') as con1250: 60 | with utf8_connection.cursor() as c_utf8, con1250.cursor() as c_win1250: 61 | # Insert unicode data via UTF8 connection 62 | c_utf8.execute("insert into T4 (C1, C_WIN1250, V_WIN1250, C_UTF8, V_UTF8)" 63 | "values (?,?,?,?,?)", 64 | (1, s5, s30, s5, s30)) 65 | utf8_connection.commit() 66 | 67 | # Read from win1250 connection 68 | c_win1250.execute("select C1, C_WIN1250, V_WIN1250, C_UTF8, V_UTF8 from T4 where C1 = 1") 69 | row_win = c_win1250.fetchone() 70 | # Read from utf8 connection 71 | c_utf8.execute("select C1, C_WIN1250, V_WIN1250, C_UTF8, V_UTF8 from T4 where C1 = 1") 72 | row_utf = c_utf8.fetchone() 73 | 74 | # Compare results - CHAR fields might be padded differently depending on charset/driver interpretation 75 | assert row_win[0] == 1 76 | assert row_utf[0] == 1 77 | assert row_win[1].strip() == s5 # Check content ignoring padding 78 | assert row_utf[1].strip() == s5 79 | assert row_win[2] == s30 # VARCHAR should be exact 80 | assert row_utf[2] == s30 81 | assert row_win[3].strip() == s5 82 | assert row_utf[3].strip() == s5 83 | assert row_win[4] == s30 84 | assert row_utf[4] == s30 85 | 86 | def testCharVarchar(utf8_connection): 87 | s = 'Introdução' # Requires UTF8 connection/charset 88 | assert len(s) == 10 89 | data = tuple([1, s, s]) 90 | with utf8_connection.cursor() as cur: # Use UTF8 connection 91 | cur.execute('insert into T3 (C1,C2,C3) values (?,?,?)', data) 92 | utf8_connection.commit() 93 | cur.execute('select C1,C2,C3 from T3 where C1 = 1') 94 | row = cur.fetchone() 95 | assert row[0] == 1 96 | assert row[1].strip() == s # CHAR padding 97 | assert row[2] == s # VARCHAR exact 98 | 99 | def testBlob(utf8_connection): 100 | s = """Introdução 101 | 102 | Este artigo descreve como você pode fazer o InterBase e o Firebird 1.5 103 | coehabitarem pacificamente seu computador Windows. Por favor, note que esta 104 | solução não permitirá que o Interbase e o Firebird rodem ao mesmo tempo. 105 | Porém você poderá trocar entre ambos com um mínimo de luta. """ 106 | assert len(s) == 292 107 | data = tuple([2, s]) 108 | b_data = tuple([3, b'bytestring']) 109 | with utf8_connection.cursor() as cur: # Use UTF8 connection for text blob 110 | # Text BLOB 111 | cur.execute('insert into T3 (C1,C4) values (?,?)', data) 112 | utf8_connection.commit() 113 | cur.execute('select C1,C4 from T3 where C1 = 2') 114 | row = cur.fetchone() 115 | assert row == data 116 | 117 | # Insert Unicode into non-textual BLOB (should fail) 118 | with pytest.raises(TypeError, match="String value is not acceptable type for a non-textual BLOB column."): 119 | cur.execute('insert into T3 (C1,C5) values (?,?)', data) 120 | # utf8_connection.commit() # Commit likely won't be reached 121 | 122 | utf8_connection.rollback() # Rollback the failed attempt 123 | 124 | # Read binary from non-textual BLOB 125 | cur.execute('insert into T3 (C1,C5) values (?,?)', b_data) 126 | utf8_connection.commit() 127 | cur.execute('select C1,C5 from T3 where C1 = 3') 128 | row = cur.fetchone() 129 | assert row == b_data 130 | -------------------------------------------------------------------------------- /tests/test_cursor.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_cursor.py 7 | # DESCRIPTION: Tests for Cursor 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from packaging.specifiers import SpecifierSet 27 | from firebird.driver import InterfaceError 28 | 29 | def test_execute(db_connection): 30 | with db_connection.cursor() as cur: 31 | cur.execute('select * from country') 32 | row = cur.fetchone() 33 | assert row == ('USA', 'Dollar') 34 | # again the same SQL (should use the same Statement) 35 | stmt = cur._stmt 36 | cur.execute('select * from country') 37 | assert stmt is cur._stmt 38 | row = cur.fetchone() 39 | assert row == ('USA', 'Dollar') 40 | # prepared statement 41 | ps = cur.prepare('select * from country') 42 | cur.execute(ps) 43 | assert stmt is not cur._stmt 44 | row = cur.fetchone() 45 | assert row == ('USA', 'Dollar') 46 | 47 | def test_executemany(db_connection): 48 | with db_connection.cursor() as cur: 49 | cur.executemany("insert into t values(?)", [(1,), (2,)]) 50 | cur.executemany("insert into t values(?)", [(3,)]) 51 | cur.executemany("insert into t values(?)", [(4,), (5,), (6,)]) 52 | db_connection.commit() 53 | p = cur.prepare("insert into t values(?)") 54 | cur.executemany(p, [(7,), (8,)]) 55 | cur.executemany(p, [(9,)]) 56 | cur.executemany(p, [(10,), (11,), (12,)]) 57 | db_connection.commit() 58 | cur.execute("select * from T order by c1") 59 | rows = cur.fetchall() 60 | assert rows == [(1,), (2,), (3,), (4,), 61 | (5,), (6,), (7,), (8,), 62 | (9,), (10,), (11,), (12,)] 63 | 64 | def test_iteration(db_connection): 65 | data = [('USA', 'Dollar'), ('England', 'Pound'), ('Canada', 'CdnDlr'), 66 | ('Switzerland', 'SFranc'), ('Japan', 'Yen'), ('Italy', 'Euro'), 67 | ('France', 'Euro'), ('Germany', 'Euro'), ('Australia', 'ADollar'), 68 | ('Hong Kong', 'HKDollar'), ('Netherlands', 'Euro'), ('Belgium', 'Euro'), 69 | ('Austria', 'Euro'), ('Fiji', 'FDollar'), ('Russia', 'Ruble'), 70 | ('Romania', 'RLeu')] 71 | with db_connection.cursor() as cur: 72 | cur.execute('select * from country') 73 | rows = [row for row in cur] 74 | assert len(rows) == len(data) 75 | assert rows == data 76 | cur.execute('select * from country') 77 | rows = [] 78 | for row in cur: 79 | rows.append(row) 80 | assert len(rows) == len(data) 81 | assert rows == data 82 | cur.execute('select * from country') 83 | i = 0 84 | for row in cur: 85 | i += 1 86 | assert row in data 87 | assert i == len(data) 88 | 89 | def test_description(db_connection): 90 | with db_connection.cursor() as cur: 91 | cur.execute('select * from country') 92 | assert len(cur.description) == 2 93 | assert repr(cur.description) == \ 94 | "(('COUNTRY', , 15, 15, 0, 0, False), " \ 95 | "('CURRENCY', , 10, 10, 0, 0, False))" 96 | cur.execute('select country as CT, currency as CUR from country') 97 | assert len(cur.description) == 2 98 | cur.execute('select * from customer') 99 | assert repr(cur.description) == \ 100 | "(('CUST_NO', , 11, 4, 0, 0, False), " \ 101 | "('CUSTOMER', , 25, 25, 0, 0, False), " \ 102 | "('CONTACT_FIRST', , 15, 15, 0, 0, True), " \ 103 | "('CONTACT_LAST', , 20, 20, 0, 0, True), " \ 104 | "('PHONE_NO', , 20, 20, 0, 0, True), " \ 105 | "('ADDRESS_LINE1', , 30, 30, 0, 0, True), " \ 106 | "('ADDRESS_LINE2', , 30, 30, 0, 0, True), " \ 107 | "('CITY', , 25, 25, 0, 0, True), " \ 108 | "('STATE_PROVINCE', , 15, 15, 0, 0, True), " \ 109 | "('COUNTRY', , 15, 15, 0, 0, True), " \ 110 | "('POSTAL_CODE', , 12, 12, 0, 0, True), " \ 111 | "('ON_HOLD', , 1, 1, 0, 0, True))" 112 | cur.execute('select * from job') 113 | assert repr(cur.description) == \ 114 | "(('JOB_CODE', , 5, 5, 0, 0, False), " \ 115 | "('JOB_GRADE', , 6, 2, 0, 0, False), " \ 116 | "('JOB_COUNTRY', , 15, 15, 0, 0, False), " \ 117 | "('JOB_TITLE', , 25, 25, 0, 0, False), " \ 118 | "('MIN_SALARY', , 20, 8, 10, -2, False), " \ 119 | "('MAX_SALARY', , 20, 8, 10, -2, False), " \ 120 | "('JOB_REQUIREMENT', , 0, 8, 0, 1, True), " \ 121 | "('LANGUAGE_REQ', , -1, 8, 0, 0, True))" 122 | cur.execute('select * from proj_dept_budget') 123 | assert repr(cur.description) == \ 124 | "(('FISCAL_YEAR', , 11, 4, 0, 0, False), " \ 125 | "('PROJ_ID', , 5, 5, 0, 0, False), " \ 126 | "('DEPT_NO', , 3, 3, 0, 0, False), " \ 127 | "('QUART_HEAD_CNT', , -1, 8, 0, 0, True), " \ 128 | "('PROJECTED_BUDGET', , 20, 8, 12, -2, True))" 129 | # Check for precision cache (implicit check by running twice) 130 | with db_connection.cursor() as cur2: 131 | cur2.execute('select * from proj_dept_budget') 132 | assert repr(cur2.description) == \ 133 | "(('FISCAL_YEAR', , 11, 4, 0, 0, False), " \ 134 | "('PROJ_ID', , 5, 5, 0, 0, False), " \ 135 | "('DEPT_NO', , 3, 3, 0, 0, False), " \ 136 | "('QUART_HEAD_CNT', , -1, 8, 0, 0, True), " \ 137 | "('PROJECTED_BUDGET', , 20, 8, 12, -2, True))" 138 | 139 | def test_exec_after_close(db_connection): 140 | with db_connection.cursor() as cur: 141 | cur.execute('select * from country') 142 | row = cur.fetchone() 143 | assert row == ('USA', 'Dollar') 144 | cur.close() 145 | # Execute again on the same closed cursor object should re-initialize 146 | cur.execute('select * from country') 147 | row = cur.fetchone() 148 | assert row == ('USA', 'Dollar') 149 | 150 | def test_fetchone(db_connection): 151 | with db_connection.cursor() as cur: 152 | cur.execute('select * from country') 153 | row = cur.fetchone() 154 | assert row == ('USA', 'Dollar') 155 | 156 | def test_fetchall(db_connection): 157 | with db_connection.cursor() as cur: 158 | cur.execute('select * from country') 159 | rows = cur.fetchall() 160 | assert rows == \ 161 | [('USA', 'Dollar'), ('England', 'Pound'), ('Canada', 'CdnDlr'), 162 | ('Switzerland', 'SFranc'), ('Japan', 'Yen'), ('Italy', 'Euro'), 163 | ('France', 'Euro'), ('Germany', 'Euro'), ('Australia', 'ADollar'), 164 | ('Hong Kong', 'HKDollar'), ('Netherlands', 'Euro'), 165 | ('Belgium', 'Euro'), ('Austria', 'Euro'), ('Fiji', 'FDollar'), 166 | ('Russia', 'Ruble'), ('Romania', 'RLeu')] 167 | 168 | def test_fetchmany(db_connection): 169 | with db_connection.cursor() as cur: 170 | cur.execute('select * from country') 171 | rows = cur.fetchmany(10) 172 | assert rows == \ 173 | [('USA', 'Dollar'), ('England', 'Pound'), ('Canada', 'CdnDlr'), 174 | ('Switzerland', 'SFranc'), ('Japan', 'Yen'), ('Italy', 'Euro'), 175 | ('France', 'Euro'), ('Germany', 'Euro'), ('Australia', 'ADollar'), 176 | ('Hong Kong', 'HKDollar')] 177 | rows = cur.fetchmany(10) 178 | assert rows == \ 179 | [('Netherlands', 'Euro'), ('Belgium', 'Euro'), ('Austria', 'Euro'), 180 | ('Fiji', 'FDollar'), ('Russia', 'Ruble'), ('Romania', 'RLeu')] 181 | rows = cur.fetchmany(10) 182 | assert len(rows) == 0 183 | 184 | def test_affected_rows(db_connection): 185 | with db_connection.cursor() as cur: 186 | assert cur.affected_rows == -1 187 | cur.execute('select * from project') 188 | assert cur.affected_rows == 0 # No rows fetched yet 189 | cur.fetchone() 190 | # Affected rows depends on internal prefetch/caching, less reliable to test exact count 191 | assert cur.affected_rows >= 1 # Check at least one row was considered 192 | assert cur.rowcount >= 1 193 | 194 | def test_affected_rows_multiple_execute(db_connection): 195 | with db_connection.cursor() as cur: 196 | cur.execute("insert into t (c1) values (999)") 197 | assert cur.affected_rows == 1 # INSERT should report 1 198 | cur.execute("update t set c1 = 888 where c1 = 999") 199 | assert cur.affected_rows == 1 # UPDATE should report 1 200 | # fetchone after DML doesn't make sense for affected_rows, 201 | # it would reset based on a SELECT if executed next. 202 | # Keep the check after the relevant DML. 203 | 204 | def test_name(db_connection): 205 | def assign_name(cursor, name): 206 | cursor.set_cursor_name(name) 207 | 208 | with db_connection.cursor() as cur: 209 | assert cur.name is None 210 | with pytest.raises(InterfaceError, match="Cannot set name for cursor has not yet executed"): 211 | assign_name(cur, 'testx') 212 | 213 | cur.execute('select * from country') 214 | cur.set_cursor_name('test') 215 | assert cur.name == 'test' 216 | with pytest.raises(InterfaceError, match="Cursor's name has already been declared"): 217 | assign_name(cur, 'testx') 218 | 219 | def test_use_after_close(db_connection): 220 | cmd = 'select * from country' 221 | with db_connection.cursor() as cur: 222 | cur.execute(cmd) 223 | cur.close() 224 | with pytest.raises(InterfaceError, match='Cannot fetch from cursor that did not executed a statement.'): 225 | # Fetching after close should raise, as the result set is gone. 226 | # The original test behavior where execute worked after close was potentially misleading. 227 | # Let's test that fetch fails after close. 228 | cur.fetchone() 229 | 230 | def test_to_dict(db_connection): 231 | cmd = 'select * from country' 232 | sample = {'COUNTRY': 'USA', 'CURRENCY': 'Dollar'} 233 | with db_connection.cursor() as cur: 234 | cur.execute(cmd) 235 | row = cur.fetchone() 236 | d = cur.to_dict(row) 237 | assert len(d) == 2 238 | assert d == sample 239 | d = {'COUNTRY': 'UNKNOWN', 'CURRENCY': 'UNKNOWN'} 240 | d2 = cur.to_dict(row, d) 241 | assert d2 == sample 242 | assert d is d2 # Ensure the passed dict was modified 243 | 244 | def test_scrollable(fb_vars, db_connection): 245 | if fb_vars['version'] in SpecifierSet('<5'): 246 | # Check for embedded 247 | with db_connection.cursor() as cur: 248 | cur.execute('select min(a.mon$remote_protocol) from mon$attachments a') 249 | if cur.fetchone()[0] is not None: 250 | pytest.skip("Works only in embedded or FB 5+") 251 | rows = [('USA', 'Dollar'), ('England', 'Pound'), ('Canada', 'CdnDlr'), 252 | ('Switzerland', 'SFranc'), ('Japan', 'Yen'), ('Italy', 'Euro'), 253 | ('France', 'Euro'), ('Germany', 'Euro'), ('Australia', 'ADollar'), 254 | ('Hong Kong', 'HKDollar'), ('Netherlands', 'Euro'), 255 | ('Belgium', 'Euro'), ('Austria', 'Euro'), ('Fiji', 'FDollar'), 256 | ('Russia', 'Ruble'), ('Romania', 'RLeu')] 257 | with db_connection.cursor() as cur: 258 | cur.open('select * from country') # Use open for scrollable 259 | assert cur.is_bof() 260 | assert not cur.is_eof() 261 | assert cur.fetch_first() == rows[0] 262 | assert cur.fetch_next() == rows[1] 263 | assert cur.fetch_prior() == rows[0] 264 | assert cur.fetch_last() == rows[-1] 265 | assert not cur.is_bof() 266 | assert cur.fetch_next() is None 267 | assert cur.is_eof() 268 | assert cur.fetch_absolute(7) == rows[6] 269 | assert cur.fetch_relative(-1) == rows[5] 270 | assert cur.fetchone() == rows[6] # fetchone should behave like fetch_next after positioning 271 | assert cur.fetchall() == rows[7:] 272 | cur.fetch_absolute(7) # Reposition 273 | assert cur.fetchall() == rows[7:] 274 | -------------------------------------------------------------------------------- /tests/test_db_createdrop.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_createdrop.py 7 | # DESCRIPTION: Tests for database create and drop operations 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from firebird.driver import (create_database, DatabaseError, connect_server, ShutdownMethod, 27 | ShutdownMode, PageSize) 28 | 29 | @pytest.fixture 30 | def droptest_file(fb_vars, tmp_dir): 31 | drop_file = tmp_dir / 'droptest.fdb' 32 | # Setup: Ensure file doesn't exist 33 | if drop_file.exists(): 34 | drop_file.unlink() 35 | # 36 | yield drop_file # Provide the dsn to the test 37 | # Teardown: Ensure file is removed 38 | if drop_file.exists(): 39 | try: 40 | # May need to shut down lingering connections on Classic Server 41 | with connect_server(fb_vars['host']) as svc: 42 | svc.database.shutdown(database=str(drop_file), mode=ShutdownMode.FULL, 43 | method=ShutdownMethod.FORCED, timeout=0) 44 | svc.database.bring_online(database=str(drop_file)) 45 | except Exception: 46 | pass # Ignore errors if shutdown fails (e.g., file already gone) 47 | finally: 48 | if drop_file.exists(): 49 | drop_file.unlink() 50 | 51 | @pytest.fixture 52 | def droptest_dsn(fb_vars, droptest_file): 53 | host = fb_vars['host'] 54 | port = fb_vars['port'] 55 | if host is None: 56 | result = str(droptest_file) 57 | else: 58 | result = f'{host}/{port}:{droptest_file}' if port else f'{host}:{droptest_file}' 59 | yield result 60 | 61 | 62 | def test_create_drop_dsn(droptest_dsn): 63 | with create_database(droptest_dsn) as con: 64 | assert con.dsn == droptest_dsn 65 | assert con.sql_dialect == 3 66 | assert con.charset is None 67 | con.drop_database() 68 | # Overwrite 69 | with create_database(droptest_dsn) as con: 70 | assert con.dsn == droptest_dsn 71 | assert con.sql_dialect == 3 72 | assert con.charset is None 73 | # Check overwrite=False raises error 74 | with pytest.raises(DatabaseError, match='exist'): 75 | create_database(droptest_dsn) 76 | # Check overwrite=True works 77 | with create_database(droptest_dsn, overwrite=True) as con: 78 | assert con.dsn == droptest_dsn 79 | con.drop_database() 80 | 81 | def test_create_drop_config(fb_vars, droptest_file, driver_cfg): 82 | host = fb_vars['host'] 83 | port = fb_vars['port'] 84 | if host is None: 85 | srv_config = f""" 86 | [server.local] 87 | user = {fb_vars['user']} 88 | password = {fb_vars['password']} 89 | """ 90 | db_config = f""" 91 | [test_db2] 92 | server = server.local 93 | database = {droptest_file} 94 | utf8filename = true 95 | charset = UTF8 96 | sql_dialect = 1 97 | page_size = {PageSize.PAGE_16K} 98 | db_sql_dialect = 1 99 | sweep_interval = 0 100 | """ 101 | dsn = str(droptest_file) 102 | else: 103 | srv_config = f""" 104 | [server.local] 105 | host = {host} 106 | user = {fb_vars['user']} 107 | password = {fb_vars['password']} 108 | port = {port if port else ''} 109 | """ 110 | db_config = f""" 111 | [test_db2] 112 | server = server.local 113 | database = {droptest_file} 114 | utf8filename = true 115 | charset = UTF8 116 | sql_dialect = 1 117 | page_size = {PageSize.PAGE_16K} 118 | db_sql_dialect = 1 119 | sweep_interval = 0 120 | """ 121 | dsn = f'{host}/{port}:{droptest_file}' if port else f'{host}:{droptest_file}' 122 | # Ensure config section doesn't exist from previous runs if tests run in parallel/reordered 123 | if driver_cfg.get_server('server.local'): 124 | driver_cfg.servers.value = [s for s in driver_cfg.servers.value if s.name != 'server.local'] 125 | if driver_cfg.get_database('test_db2'): 126 | driver_cfg.databases.value = [db for db in driver_cfg.databases.value if db.name != 'test_db2'] 127 | 128 | driver_cfg.register_server('server.local', srv_config) 129 | driver_cfg.register_database('test_db2', db_config) 130 | 131 | try: 132 | with create_database('test_db2') as con: 133 | assert con.sql_dialect == 1 134 | assert con.charset == 'UTF8' 135 | assert con.info.page_size == 16384 136 | assert con.info.sql_dialect == 1 137 | assert con.info.charset == 'UTF8' 138 | assert con.info.sweep_interval == 0 139 | con.drop_database() 140 | finally: 141 | # Clean up registered config 142 | driver_cfg.databases.value = [db for db in driver_cfg.databases.value if db.name != 'test_db2'] 143 | -------------------------------------------------------------------------------- /tests/test_dbapi_compliance.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_dbapi_compliance.py 7 | # DESCRIPTION: Tests for Python DB API 2.0 compliance 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | import firebird.driver as driver 27 | import decimal 28 | import datetime 29 | 30 | def test_module_attributes(): 31 | """Verify required DB API 2.0 module attributes.""" 32 | assert hasattr(driver, 'apilevel'), "Module lacks 'apilevel' attribute" 33 | assert driver.apilevel == '2.0', "apilevel is not '2.0'" 34 | 35 | assert hasattr(driver, 'threadsafety'), "Module lacks 'threadsafety' attribute" 36 | assert isinstance(driver.threadsafety, int), "threadsafety is not an integer" 37 | assert driver.threadsafety in (0, 1, 2, 3), "threadsafety not in allowed range (0-3)" 38 | # firebird-driver is expected to be 1 39 | assert driver.threadsafety == 1, "Expected threadsafety level 1" 40 | 41 | assert hasattr(driver, 'paramstyle'), "Module lacks 'paramstyle' attribute" 42 | assert isinstance(driver.paramstyle, str), "paramstyle is not a string" 43 | allowed_paramstyles = ('qmark', 'numeric', 'named', 'format', 'pyformat') 44 | assert driver.paramstyle in allowed_paramstyles, f"paramstyle '{driver.paramstyle}' not in allowed styles" 45 | # firebird-driver uses qmark 46 | assert driver.paramstyle == 'qmark', "Expected paramstyle 'qmark'" 47 | 48 | def test_module_connect(): 49 | """Verify module has a connect() method.""" 50 | assert hasattr(driver, 'connect'), "Module lacks 'connect' method" 51 | assert callable(driver.connect), "'connect' is not callable" 52 | 53 | def test_module_exceptions(): 54 | """Verify required DB API 2.0 exception hierarchy.""" 55 | # Check existence 56 | assert hasattr(driver, 'Error'), "Module lacks 'Error' exception" 57 | assert hasattr(driver, 'InterfaceError'), "Module lacks 'InterfaceError' exception" 58 | assert hasattr(driver, 'DatabaseError'), "Module lacks 'DatabaseError' exception" 59 | assert hasattr(driver, 'DataError'), "Module lacks 'DataError' exception" 60 | assert hasattr(driver, 'OperationalError'), "Module lacks 'OperationalError' exception" 61 | assert hasattr(driver, 'IntegrityError'), "Module lacks 'IntegrityError' exception" 62 | assert hasattr(driver, 'InternalError'), "Module lacks 'InternalError' exception" 63 | assert hasattr(driver, 'ProgrammingError'), "Module lacks 'ProgrammingError' exception" 64 | assert hasattr(driver, 'NotSupportedError'), "Module lacks 'NotSupportedError' exception" 65 | 66 | # Check hierarchy 67 | assert issubclass(driver.Error, Exception), "Error does not inherit from Exception" 68 | assert issubclass(driver.InterfaceError, driver.Error), "InterfaceError does not inherit from Error" 69 | assert issubclass(driver.DatabaseError, driver.Error), "DatabaseError does not inherit from Error" 70 | assert issubclass(driver.DataError, driver.DatabaseError), "DataError does not inherit from DatabaseError" 71 | assert issubclass(driver.OperationalError, driver.DatabaseError), "OperationalError does not inherit from DatabaseError" 72 | assert issubclass(driver.IntegrityError, driver.DatabaseError), "IntegrityError does not inherit from DatabaseError" 73 | assert issubclass(driver.InternalError, driver.DatabaseError), "InternalError does not inherit from DatabaseError" 74 | assert issubclass(driver.ProgrammingError, driver.DatabaseError), "ProgrammingError does not inherit from DatabaseError" 75 | assert issubclass(driver.NotSupportedError, driver.DatabaseError), "NotSupportedError does not inherit from DatabaseError" 76 | 77 | def test_connection_interface(db_connection): 78 | """Verify required DB API 2.0 Connection attributes and methods.""" 79 | con = db_connection # Use the fixture 80 | 81 | # Required methods 82 | assert hasattr(con, 'close'), "Connection lacks 'close' method" 83 | assert callable(con.close), "'close' is not callable" 84 | 85 | assert hasattr(con, 'commit'), "Connection lacks 'commit' method" 86 | assert callable(con.commit), "'commit' is not callable" 87 | 88 | assert hasattr(con, 'rollback'), "Connection lacks 'rollback' method" 89 | assert callable(con.rollback), "'rollback' is not callable" 90 | 91 | assert hasattr(con, 'cursor'), "Connection lacks 'cursor' method" 92 | assert callable(con.cursor), "'cursor' is not callable" 93 | 94 | # Required exception attribute 95 | assert hasattr(con, 'Error'), "Connection lacks 'Error' attribute" 96 | assert con.Error is driver.Error, "Connection.Error is not the same as module.Error" 97 | 98 | # Context manager protocol (optional but good practice) 99 | assert hasattr(con, '__enter__'), "Connection lacks '__enter__' method" 100 | assert callable(con.__enter__), "'__enter__' is not callable" 101 | assert hasattr(con, '__exit__'), "Connection lacks '__exit__' method" 102 | assert callable(con.__exit__), "'__exit__' is not callable" 103 | 104 | def test_cursor_attributes(db_connection): 105 | """Verify required DB API 2.0 Cursor attributes.""" 106 | con = db_connection 107 | cur = None 108 | try: 109 | cur = con.cursor() 110 | 111 | # description attribute 112 | assert hasattr(cur, 'description'), "Cursor lacks 'description' attribute" 113 | assert cur.description is None, "Cursor.description should be None before execute" 114 | # Execute a simple query to populate description 115 | cur.execute("SELECT 1 AS N, 'a' AS S FROM RDB$DATABASE") 116 | assert isinstance(cur.description, tuple), "Cursor.description is not a tuple after execute" 117 | assert len(cur.description) == 2, "Cursor.description has wrong length" 118 | # Check basic structure of a description entry 119 | desc_entry = cur.description[0] 120 | assert isinstance(desc_entry, tuple), "Description entry is not a tuple" 121 | assert len(desc_entry) == 7, "Description entry does not have 7 elements" 122 | assert isinstance(desc_entry[driver.DESCRIPTION_NAME], str), "Description name is not a string" 123 | assert issubclass(desc_entry[driver.DESCRIPTION_TYPE_CODE], (int, float, decimal.Decimal, str, bytes, datetime.date, datetime.time, datetime.datetime, list, type(None))), "Description type_code is not a valid type" 124 | # Allow None or int for optional size fields 125 | assert desc_entry[driver.DESCRIPTION_DISPLAY_SIZE] is None or isinstance(desc_entry[driver.DESCRIPTION_DISPLAY_SIZE], int) 126 | assert desc_entry[driver.DESCRIPTION_INTERNAL_SIZE] is None or isinstance(desc_entry[driver.DESCRIPTION_INTERNAL_SIZE], int) 127 | # Allow None or int for precision/scale 128 | assert desc_entry[driver.DESCRIPTION_PRECISION] is None or isinstance(desc_entry[driver.DESCRIPTION_PRECISION], int) 129 | assert desc_entry[driver.DESCRIPTION_SCALE] is None or isinstance(desc_entry[driver.DESCRIPTION_SCALE], int) 130 | assert isinstance(desc_entry[driver.DESCRIPTION_NULL_OK], bool), "Description null_ok is not a boolean" 131 | 132 | 133 | # rowcount attribute 134 | assert hasattr(cur, 'rowcount'), "Cursor lacks 'rowcount' attribute" 135 | # Note: rowcount is -1 before fetch for SELECT, or affected rows for DML 136 | assert isinstance(cur.rowcount, int), "Cursor.rowcount is not an integer" 137 | 138 | # arraysize attribute 139 | assert hasattr(cur, 'arraysize'), "Cursor lacks 'arraysize' attribute" 140 | assert isinstance(cur.arraysize, int), "Cursor.arraysize is not an integer" 141 | assert cur.arraysize >= 1, "Cursor.arraysize must be >= 1" 142 | 143 | finally: 144 | if cur and not cur.is_closed(): 145 | cur.close() 146 | 147 | def test_cursor_methods(db_connection): 148 | """Verify required DB API 2.0 Cursor methods.""" 149 | con = db_connection 150 | cur = None 151 | try: 152 | cur = con.cursor() 153 | 154 | assert hasattr(cur, 'close'), "Cursor lacks 'close' method" 155 | assert callable(cur.close), "'close' is not callable" 156 | 157 | assert hasattr(cur, 'execute'), "Cursor lacks 'execute' method" 158 | assert callable(cur.execute), "'execute' is not callable" 159 | 160 | assert hasattr(cur, 'fetchone'), "Cursor lacks 'fetchone' method" 161 | assert callable(cur.fetchone), "'fetchone' is not callable" 162 | 163 | # Optional but common methods 164 | assert hasattr(cur, 'executemany'), "Cursor lacks 'executemany' method" 165 | assert callable(cur.executemany), "'executemany' is not callable" 166 | 167 | assert hasattr(cur, 'fetchall'), "Cursor lacks 'fetchall' method" 168 | assert callable(cur.fetchall), "'fetchall' is not callable" 169 | 170 | assert hasattr(cur, 'fetchmany'), "Cursor lacks 'fetchmany' method" 171 | assert callable(cur.fetchmany), "'fetchmany' is not callable" 172 | 173 | assert hasattr(cur, 'setinputsizes'), "Cursor lacks 'setinputsizes' method" 174 | assert callable(cur.setinputsizes), "'setinputsizes' is not callable" 175 | 176 | assert hasattr(cur, 'setoutputsize'), "Cursor lacks 'setoutputsize' method" 177 | assert callable(cur.setoutputsize), "'setoutputsize' is not callable" 178 | 179 | finally: 180 | if cur and not cur.is_closed(): 181 | cur.close() 182 | -------------------------------------------------------------------------------- /tests/test_distributed_trans.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_transaction.py 7 | # DESCRIPTION: Tests for Transaction 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from firebird.driver import (connect, create_database, connect_server, Isolation, 27 | transaction, InterfaceError, TPB, TableShareMode, 28 | ShutdownMode, ShutdownMethod, DistributedTransactionManager, 29 | TableAccessMode, TraInfoCode, TraInfoAccess, TraAccessMode) 30 | 31 | @pytest.fixture(scope="function") # Function scope for isolation 32 | def distributed_transaction_dbs(driver_cfg, tmp_dir, fb_vars): 33 | # Setup two databases for DTS tests 34 | db1_path = tmp_dir / 'fbtest-dts-1.fdb' 35 | db2_path = tmp_dir / 'fbtest-dts-2.fdb' 36 | con1, con2 = None, None 37 | cfg1_name, cfg2_name = 'dts-1-test', 'dts-2-test' 38 | 39 | # Register configs 40 | cfg1 = driver_cfg.register_database(cfg1_name) 41 | cfg1.server.value = fb_vars['server'] 42 | cfg1.database.value = str(db1_path) 43 | cfg1.no_linger.value = True 44 | 45 | cfg2 = driver_cfg.register_database(cfg2_name) 46 | cfg2.server.value = fb_vars['server'] 47 | cfg2.database.value = str(db2_path) 48 | cfg2.no_linger.value = True 49 | 50 | # Create databases 51 | try: 52 | con1 = create_database(cfg1_name, overwrite=True) 53 | con1.execute_immediate("recreate table T (PK integer, C1 integer)") 54 | con1.commit() 55 | 56 | con2 = create_database(cfg2_name, overwrite=True) 57 | con2.execute_immediate("recreate table T (PK integer, C1 integer)") 58 | con2.commit() 59 | except Exception as e: 60 | # Cleanup if setup fails 61 | if con1 and not con1.is_closed(): con1.close() 62 | if con2 and not con2.is_closed(): con2.close() 63 | if db1_path.exists(): db1_path.unlink() 64 | if db2_path.exists(): db2_path.unlink() 65 | driver_cfg.databases.value = [db for db in driver_cfg.databases.value if db.name not in [cfg1_name, cfg2_name]] 66 | pytest.fail(f"Failed to set up distributed transaction databases: {e}") 67 | 68 | yield con1, con2, str(db1_path), str(db2_path), cfg1_name, cfg2_name # Provide connections and paths 69 | 70 | # Teardown 71 | if con1 and not con1.is_closed(): con1.close() 72 | if con2 and not con2.is_closed(): con2.close() 73 | 74 | # Ensure databases can be dropped (shutdown might be needed) 75 | for db_fpath in [db1_path, db2_path]: 76 | if db_fpath.exists(): 77 | try: 78 | with connect_server(fb_vars['server']) as svc: 79 | svc.database.shutdown(database=str(db_fpath), mode=ShutdownMode.FULL, 80 | method=ShutdownMethod.FORCED, timeout=0) 81 | svc.database.bring_online(database=str(db_fpath)) 82 | # Use config name for connect-to-drop to ensure server is specified 83 | db_conf_name = cfg1_name if str(db_fpath) == str(db1_path) else cfg2_name 84 | with connect(db_conf_name) as con_drop: 85 | con_drop.drop_database() 86 | except Exception as e: 87 | print(f"Warning: Could not drop DTS database {db_fpath}: {e}") 88 | finally: 89 | # Attempt unlink again just in case drop failed but left file 90 | if db_fpath.exists(): 91 | try: 92 | db_fpath.unlink() 93 | except OSError: 94 | print(f"Warning: Could not unlink DTS database file {db_fpath}") 95 | 96 | def test_context_manager(distributed_transaction_dbs): 97 | con1, con2, _, _, _, _ = distributed_transaction_dbs 98 | with DistributedTransactionManager((con1, con2)) as dt: 99 | q = 'select * from T order by pk' 100 | with dt.cursor(con1) as c1, con1.cursor() as cc1, \ 101 | dt.cursor(con2) as c2, con2.cursor() as cc2: 102 | 103 | # Distributed transaction: COMMIT 104 | with transaction(dt): 105 | c1.execute('insert into t (pk) values (1)') 106 | c2.execute('insert into t (pk) values (1)') 107 | 108 | with transaction(con1): 109 | cc1.execute(q) 110 | result = cc1.fetchall() 111 | assert result == [(1, None)] 112 | with transaction(con2): 113 | cc2.execute(q) 114 | result = cc2.fetchall() 115 | assert result == [(1, None)] 116 | 117 | # Distributed transaction: ROLLBACK 118 | with pytest.raises(Exception, match="Simulated DTS error"): 119 | with transaction(dt): 120 | c1.execute('insert into t (pk) values (2)') 121 | c2.execute('insert into t (pk) values (2)') 122 | raise Exception("Simulated DTS error") 123 | 124 | c1.execute(q) # Should reuse dt transaction context implicitly if needed 125 | result = c1.fetchall() 126 | assert result == [(1, None)] 127 | c2.execute(q) 128 | result = c2.fetchall() 129 | assert result == [(1, None)] 130 | 131 | def test_simple_dt(distributed_transaction_dbs): 132 | con1, con2, _, _, _, _ = distributed_transaction_dbs 133 | with DistributedTransactionManager((con1, con2)) as dt: 134 | q = 'select * from T order by pk' 135 | with dt.cursor(con1) as c1, con1.cursor() as cc1, \ 136 | dt.cursor(con2) as c2, con2.cursor() as cc2: 137 | # Distributed transaction: COMMIT 138 | c1.execute('insert into t (pk) values (1)') 139 | c2.execute('insert into t (pk) values (1)') 140 | dt.commit() 141 | 142 | with transaction(con1): cc1.execute(q); result = cc1.fetchall() 143 | assert result == [(1, None)] 144 | with transaction(con2): cc2.execute(q); result = cc2.fetchall() 145 | assert result == [(1, None)] 146 | 147 | # Distributed transaction: PREPARE+COMMIT 148 | c1.execute('insert into t (pk) values (2)') 149 | c2.execute('insert into t (pk) values (2)') 150 | dt.prepare() 151 | dt.commit() 152 | 153 | with transaction(con1): cc1.execute(q); result = cc1.fetchall() 154 | assert result == [(1, None), (2, None)] 155 | with transaction(con2): cc2.execute(q); result = cc2.fetchall() 156 | assert result == [(1, None), (2, None)] 157 | 158 | # Distributed transaction: SAVEPOINT+ROLLBACK to it 159 | c1.execute('insert into t (pk) values (3)') 160 | dt.savepoint('CG_SAVEPOINT') 161 | c2.execute('insert into t (pk) values (3)') 162 | dt.rollback(savepoint='CG_SAVEPOINT') 163 | 164 | c1.execute(q); result = c1.fetchall() 165 | assert result == [(1, None), (2, None), (3, None)] 166 | c2.execute(q); result = c2.fetchall() 167 | assert result == [(1, None), (2, None)] 168 | 169 | # Distributed transaction: ROLLBACK 170 | dt.rollback() 171 | 172 | with transaction(con1): cc1.execute(q); result = cc1.fetchall() 173 | assert result == [(1, None), (2, None)] 174 | with transaction(con2): cc2.execute(q); result = cc2.fetchall() 175 | assert result == [(1, None), (2, None)] 176 | 177 | # Distributed transaction: EXECUTE_IMMEDIATE 178 | dt.execute_immediate('insert into t (pk) values (3)') 179 | dt.commit() 180 | 181 | with transaction(con1): cc1.execute(q); result = cc1.fetchall() 182 | assert result == [(1, None), (2, None), (3, None)] 183 | with transaction(con2): cc2.execute(q); result = cc2.fetchall() 184 | assert result == [(1, None), (2, None), (3, None)] 185 | 186 | def test_limbo_transactions(distributed_transaction_dbs): 187 | pytest.skip('Limbo transaction test needs review and reliable setup.') 188 | # Original test was skipped and likely requires manual server intervention 189 | # or specific timing to force limbo state, which is hard to automate reliably. 190 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_events.py 7 | # DESCRIPTION: Tests for Firebird events 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import time 26 | import threading 27 | import pytest 28 | from firebird.driver import (create_database, DatabaseError, connect_server, ShutdownMethod, 29 | ShutdownMode, PageSize) 30 | 31 | @pytest.fixture 32 | def event_db(fb_vars, tmp_dir): 33 | event_file = tmp_dir / 'fbevents.fdb' 34 | host = fb_vars['host'] 35 | port = fb_vars['port'] 36 | if host is None: 37 | dsn = str(event_file) 38 | else: 39 | dsn = f'{host}/{port}:{event_file}' if port else f'{host}:{event_file}' 40 | try: 41 | con = create_database(dsn) 42 | with con.cursor() as cur: 43 | cur.execute("CREATE TABLE T (PK Integer, C1 Integer)") 44 | cur.execute("""CREATE TRIGGER EVENTS_AU FOR T ACTIVE 45 | BEFORE UPDATE POSITION 0 46 | AS 47 | BEGIN 48 | if (old.C1 <> new.C1) then 49 | post_event 'c1_updated' ; 50 | END""") 51 | cur.execute("""CREATE TRIGGER EVENTS_AI FOR T ACTIVE 52 | AFTER INSERT POSITION 0 53 | AS 54 | BEGIN 55 | if (new.c1 = 1) then 56 | post_event 'insert_1' ; 57 | else if (new.c1 = 2) then 58 | post_event 'insert_2' ; 59 | else if (new.c1 = 3) then 60 | post_event 'insert_3' ; 61 | else 62 | post_event 'insert_other' ; 63 | END""") 64 | con.commit() 65 | yield con 66 | finally: 67 | con.drop_database() 68 | 69 | def test_one_event(event_db): 70 | def send_events(command_list): 71 | with event_db.cursor() as cur: 72 | for cmd in command_list: 73 | cur.execute(cmd) 74 | event_db.commit() 75 | 76 | e = {} 77 | timed_event = threading.Timer(3.0, send_events, args=[["insert into T (PK,C1) values (1,1)",]]) 78 | with event_db.event_collector(['insert_1']) as events: 79 | timed_event.start() 80 | e = events.wait() 81 | timed_event.join() 82 | assert e == {'insert_1': 1} 83 | 84 | def test_multiple_events(event_db): 85 | def send_events(command_list): 86 | with event_db.cursor() as cur: 87 | for cmd in command_list: 88 | cur.execute(cmd) 89 | event_db.commit() 90 | 91 | cmds = ["insert into T (PK,C1) values (1,1)", 92 | "insert into T (PK,C1) values (1,2)", 93 | "insert into T (PK,C1) values (1,3)", 94 | "insert into T (PK,C1) values (1,1)", 95 | "insert into T (PK,C1) values (1,2)",] 96 | timed_event = threading.Timer(3.0, send_events, args=[cmds]) 97 | with event_db.event_collector(['insert_1', 'insert_3']) as events: 98 | timed_event.start() 99 | e = events.wait() 100 | timed_event.join() 101 | assert e == {'insert_3': 1, 'insert_1': 2} 102 | 103 | def test_20_events(event_db): 104 | def send_events(command_list): 105 | with event_db.cursor() as cur: 106 | for cmd in command_list: 107 | cur.execute(cmd) 108 | event_db.commit() 109 | 110 | cmds = ["insert into T (PK,C1) values (1,1)", 111 | "insert into T (PK,C1) values (1,2)", 112 | "insert into T (PK,C1) values (1,3)", 113 | "insert into T (PK,C1) values (1,1)", 114 | "insert into T (PK,C1) values (1,2)",] 115 | e = {} 116 | timed_event = threading.Timer(1.0, send_events, args=[cmds]) 117 | with event_db.event_collector(['insert_1', 'A', 'B', 'C', 'D', 118 | 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 119 | 'N', 'O', 'P', 'Q', 'R', 'insert_3']) as events: 120 | timed_event.start() 121 | time.sleep(3) 122 | e = events.wait() 123 | timed_event.join() 124 | assert e == {'A': 0, 'C': 0, 'B': 0, 'E': 0, 'D': 0, 'G': 0, 'insert_1': 2, 125 | 'I': 0, 'H': 0, 'K': 0, 'J': 0, 'M': 0, 'L': 0, 'O': 0, 'N': 0, 126 | 'Q': 0, 'P': 0, 'R': 0, 'insert_3': 1, 'F': 0} 127 | 128 | def test_flush_events(event_db): 129 | def send_events(command_list): 130 | with event_db.cursor() as cur: 131 | for cmd in command_list: 132 | cur.execute(cmd) 133 | event_db.commit() 134 | 135 | timed_event = threading.Timer(3.0, send_events, args=[["insert into T (PK,C1) values (1,1)",]]) 136 | with event_db.event_collector(['insert_1']) as events: 137 | send_events(["insert into T (PK,C1) values (1,1)", 138 | "insert into T (PK,C1) values (1,1)"]) 139 | time.sleep(2) 140 | events.flush() 141 | timed_event.start() 142 | e = events.wait() 143 | timed_event.join() 144 | assert e == {'insert_1': 1} 145 | -------------------------------------------------------------------------------- /tests/test_hooks.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_hooks.py 7 | # DESCRIPTION: Tests for hooks defined by driver 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | from functools import partial 26 | import pytest 27 | from firebird.base.hooks import add_hook 28 | from firebird.driver import connect_server, connect, Connection, Server 29 | from firebird.driver.hooks import ConnectionHook, ServerHook, hook_manager 30 | 31 | hook_state = [] 32 | 33 | TEST_ID = '_test_id_' 34 | 35 | def _reset_hook_state(): 36 | hook_state.clear() 37 | hook_manager.remove_all_hooks() 38 | 39 | def _hook_service_attached(svc): 40 | hook_state.append("Service attached") 41 | 42 | def _hook_db_attached(con): 43 | hook_state.append("Database attached") 44 | 45 | def _hook_db_closed(con): 46 | hook_state.append(f"Database closed: {getattr(con, TEST_ID, None)}") 47 | 48 | def _hook_db_detach_request_a(con): 49 | hook_state.append(f"Database dettach request RETAIN: {getattr(con, TEST_ID, None)}") 50 | return True # Retain 51 | 52 | def _hook_db_detach_request_b(con): 53 | hook_state.append(f"Database dettach request NO RETAIN: {getattr(con, TEST_ID, None)}") 54 | return False # Do not retain 55 | 56 | def _hook_db_attach_request_a(dsn, dpb): 57 | hook_state.append("Database attach request NORMAL CONNECT") 58 | return None # Allow normal connection 59 | 60 | def _hook_db_attach_request_b(dsn, dpb, hook_con_instance): # Pass instance via closure/partial 61 | # This hook needs the actual connection to return, tricky with fixtures directly 62 | # Option 1: Pass the created connection instance to the hook registration 63 | # Option 2: Create connection inside the hook (less ideal) 64 | hook_state.append("Database attach request PROVIDE CONNECTION") 65 | return hook_con_instance 66 | 67 | @pytest.fixture 68 | def hook_svc(fb_vars): 69 | with connect_server(fb_vars['host'], 70 | user=fb_vars['user'], 71 | password=fb_vars['password']) as svc: 72 | yield svc 73 | 74 | def test_hook_db_attached(dsn): 75 | _reset_hook_state() 76 | add_hook(ConnectionHook.ATTACHED, Connection, _hook_db_attached) 77 | with connect(dsn) as con: 78 | assert len(hook_state) == 1 79 | assert hook_state[0] == "Database attached" 80 | 81 | def test_hook_db_attach_request(dsn): 82 | _reset_hook_state() 83 | main_con = connect(dsn) 84 | add_hook(ConnectionHook.ATTACH_REQUEST, Connection, _hook_db_attach_request_a) 85 | with connect(dsn) as con: 86 | assert len(hook_state) == 1 87 | assert hook_state[0] == "Database attach request NORMAL CONNECT" 88 | 89 | add_hook(ConnectionHook.ATTACH_REQUEST, Connection, partial(_hook_db_attach_request_b, 90 | hook_con_instance=main_con)) 91 | con = connect(dsn) 92 | assert len(hook_state) == 3 93 | assert hook_state[2] == "Database attach request PROVIDE CONNECTION" 94 | assert con is main_con 95 | 96 | def test_hook_db_closed(dsn): 97 | _reset_hook_state() 98 | with connect(dsn) as con: 99 | con._test_id_ = 'OUR CONENCTION' 100 | add_hook(ConnectionHook.CLOSED, con, _hook_db_closed) 101 | assert len(hook_state) == 1 102 | assert hook_state[0] == "Database closed: OUR CONENCTION" 103 | 104 | def test_hook_db_detach_request(dsn): 105 | _reset_hook_state() 106 | # reject detach 107 | con = connect(dsn) 108 | con._test_id_ = 'OUR CONENCTION' 109 | add_hook(ConnectionHook.DETACH_REQUEST, con, _hook_db_detach_request_a) 110 | con.close() 111 | assert len(hook_state) == 1 112 | assert hook_state[0] == "Database dettach request RETAIN: OUR CONENCTION" 113 | assert not con.is_closed() 114 | 115 | # accept close 116 | _reset_hook_state() 117 | add_hook(ConnectionHook.DETACH_REQUEST, con, _hook_db_detach_request_b) 118 | con.close() 119 | assert len(hook_state) == 1 120 | assert hook_state[0] == "Database dettach request NO RETAIN: OUR CONENCTION" 121 | assert con.is_closed() 122 | 123 | def test_hook_service_attached(fb_vars): 124 | _reset_hook_state() 125 | add_hook(ServerHook.ATTACHED, Server, _hook_service_attached) 126 | with connect_server(fb_vars['host'], user=fb_vars['user'], password=fb_vars['password']) as svc: 127 | assert len(hook_state) == 1 128 | assert hook_state[0] == "Service attached" 129 | -------------------------------------------------------------------------------- /tests/test_insert_data.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_insert_data.py 7 | # DESCRIPTION: Tests for data insert operations 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import datetime 26 | import decimal 27 | import pytest 28 | from firebird.driver.core import BlobReader 29 | from firebird.driver import connect, DatabaseError 30 | 31 | @pytest.fixture(autouse=True) 32 | def setup_insert_test(db_connection): 33 | # Ensure table T2 exists 34 | try: 35 | with db_connection.cursor() as cur: 36 | cur.execute("SELECT C1 FROM T2 WHERE 1=0") 37 | except DatabaseError as e: 38 | if "Table unknown T2" in str(e): 39 | pytest.skip("Table 'T2' needed for insert tests does not exist.") 40 | else: 41 | raise 42 | yield 43 | 44 | @pytest.fixture 45 | def utf8_connection(dsn): 46 | # Separate connection with UTF8 charset 47 | with connect(dsn, charset='utf-8') as con_utf8: 48 | yield con_utf8 49 | 50 | def test_insert_integers(db_connection): 51 | with db_connection.cursor() as cur: 52 | cur.execute('insert into T2 (C1,C2,C3) values (?,?,?)', ['1', '1', '1']) 53 | db_connection.commit() 54 | cur.execute('select C1,C2,C3 from T2 where C1 = 1') 55 | rows = cur.fetchall() 56 | assert rows == [(1, 1, 1)] 57 | 58 | cur.execute('insert into T2 (C1,C2,C3) values (?,?,?)', 59 | [2, 1, 9223372036854775807]) 60 | cur.execute('insert into T2 (C1,C2,C3) values (?,?,?)', 61 | [2, 1, -9223372036854775808]) # Use correct min value 62 | db_connection.commit() 63 | cur.execute('select C1,C2,C3 from T2 where C1 = 2') 64 | rows = cur.fetchall() 65 | assert rows == [(2, 1, 9223372036854775807), (2, 1, -9223372036854775808)] 66 | 67 | def test_insert_char_varchar(db_connection): 68 | with db_connection.cursor() as cur: 69 | cur.execute('insert into T2 (C1,C4,C5) values (?,?,?)', [2, 'AA', 'AA']) 70 | db_connection.commit() 71 | cur.execute('select C1,C4,C5 from T2 where C1 = 2') 72 | rows = cur.fetchall() 73 | assert rows == [(2, 'AA ', 'AA')] # CHAR is padded 74 | 75 | # Too long values - Check for specific truncation error 76 | with pytest.raises(DatabaseError, match='truncation'): 77 | cur.execute('insert into T2 (C1,C4) values (?,?)', [3, '123456']) 78 | db_connection.commit() # Commit might not be reached 79 | 80 | db_connection.rollback() # Rollback the failed transaction 81 | 82 | with pytest.raises(DatabaseError, match='truncation'): 83 | cur.execute('insert into T2 (C1,C5) values (?,?)', [3, '12345678901']) 84 | db_connection.commit() 85 | 86 | db_connection.rollback() 87 | 88 | def test_insert_datetime(db_connection): 89 | with db_connection.cursor() as cur: 90 | now = datetime.datetime(2011, 11, 13, 15, 0, 1, 200000) 91 | cur.execute('insert into T2 (C1,C6,C7,C8) values (?,?,?,?)', [3, now.date(), now.time(), now]) 92 | db_connection.commit() 93 | cur.execute('select C1,C6,C7,C8 from T2 where C1 = 3') 94 | rows = cur.fetchall() 95 | assert rows == [(3, datetime.date(2011, 11, 13), datetime.time(15, 0, 1, 200000), 96 | datetime.datetime(2011, 11, 13, 15, 0, 1, 200000))] 97 | 98 | # Insert from string (driver handles conversion if possible, though explicit types are better) 99 | # Note: Microsecond separator might vary based on driver/server locale. Use types. 100 | cur.execute('insert into T2 (C1,C6,C7,C8) values (?,?,?,?)', [4, '2011-11-13', '15:0:1.200', '2011-11-13 15:0:1.2000']) 101 | db_connection.commit() 102 | cur.execute('select C1,C6,C7,C8 from T2 where C1 = 4') 103 | rows = cur.fetchall() 104 | assert rows == [(4, datetime.date(2011, 11, 13), datetime.time(15, 0, 1, 200000), 105 | datetime.datetime(2011, 11, 13, 15, 0, 1, 200000))] 106 | 107 | 108 | # encode date before 1859-11-17 produce a negative number 109 | past_date = datetime.datetime(1859, 11, 16, 15, 0, 1, 200000) 110 | cur.execute('insert into T2 (C1,C6,C7,C8) values (?,?,?,?)', [5, past_date.date(), past_date.time(), past_date]) 111 | db_connection.commit() 112 | cur.execute('select C1,C6,C7,C8 from T2 where C1 = 5') 113 | rows = cur.fetchall() 114 | assert rows == [(5, datetime.date(1859, 11, 16), datetime.time(15, 0, 1, 200000), 115 | datetime.datetime(1859, 11, 16, 15, 0, 1, 200000))] 116 | 117 | def test_insert_blob(db_connection, utf8_connection): 118 | con2 = utf8_connection # Use the UTF8 connection fixture 119 | with db_connection.cursor() as cur, con2.cursor() as cur2: 120 | cur.execute('insert into T2 (C1,C9) values (?,?)', [4, 'This is a BLOB!']) 121 | db_connection.commit() 122 | cur.execute('select C1,C9 from T2 where C1 = 4') 123 | rows = cur.fetchall() 124 | assert rows == [(4, 'This is a BLOB!')] 125 | 126 | # Non-textual BLOB requires BLOB SUB_TYPE 0 127 | # The test table T2 has C16 as BOOLEAN, not BLOB SUB_TYPE 0. 128 | # Need to adjust table definition or skip this part. 129 | # Assuming C16 was meant to be BLOB SUB_TYPE 0: 130 | # blob_data = bytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 131 | # cur.execute('insert into T2 (C1,C16) values (?,?)', [8, blob_data]) 132 | # db_connection.commit() 133 | # cur.execute('select C1,C16 from T2 where C1 = 8') 134 | # rows = cur.fetchall() 135 | # assert rows == [(8, blob_data)] 136 | 137 | # BLOB bigger than stream_blob_threshold 138 | big_blob = '123456789' * 10 # Make it larger than new threshold 139 | cur.execute('insert into T2 (C1,C9) values (?,?)', [5, big_blob]) 140 | db_connection.commit() 141 | cur.stream_blob_threshold = 50 142 | cur.execute('select C1,C9 from T2 where C1 = 5') 143 | row = cur.fetchone() 144 | assert isinstance(row[1], BlobReader) 145 | with row[1] as blob_reader: 146 | assert blob_reader.read() == big_blob 147 | 148 | # Unicode in BLOB (requires UTF8 connection) 149 | blob_text = 'This is a BLOB with characters beyond ascii: ěščřžýáíé' 150 | cur2.execute('insert into T2 (C1,C9) values (?,?)', [6, blob_text]) 151 | con2.commit() 152 | cur2.execute('select C1,C9 from T2 where C1 = 6') 153 | rows = cur2.fetchall() 154 | assert rows == [(6, blob_text)] 155 | 156 | # Unicode non-textual BLOB (expect error) 157 | # Again, assumes C16 is BLOB SUB_TYPE 0 158 | # with pytest.raises(TypeError, match="String value is not acceptable type for a non-textual BLOB column."): 159 | # cur2.execute('insert into T2 (C1,C16) values (?,?)', [7, blob_text]) 160 | 161 | def test_insert_float_double(db_connection): 162 | with db_connection.cursor() as cur: 163 | cur.execute('insert into T2 (C1,C12,C13) values (?,?,?)', [5, 1.0, 1.0]) 164 | db_connection.commit() 165 | cur.execute('select C1,C12,C13 from T2 where C1 = 5') 166 | rows = cur.fetchall() 167 | assert rows == [(5, 1.0, 1.0)] 168 | cur.execute('insert into T2 (C1,C12,C13) values (?,?,?)', [6, 1, 1]) # Insert int 169 | db_connection.commit() 170 | cur.execute('select C1,C12,C13 from T2 where C1 = 6') 171 | rows = cur.fetchall() 172 | assert rows == [(6, 1.0, 1.0)] # Should read back as float 173 | 174 | def test_insert_numeric_decimal(db_connection): 175 | with db_connection.cursor() as cur: 176 | cur.execute('insert into T2 (C1,C10,C11) values (?,?,?)', [6, 1.1, 1.1]) # Insert float 177 | cur.execute('insert into T2 (C1,C10,C11) values (?,?,?)', [6, decimal.Decimal('100.11'), decimal.Decimal('100.11')]) 178 | db_connection.commit() 179 | cur.execute('select C1,C10,C11 from T2 where C1 = 6') 180 | rows = cur.fetchall() 181 | # Check type and value equality carefully for decimals 182 | assert len(rows) == 2 183 | assert rows[0][0] == 6 184 | assert isinstance(rows[0][1], decimal.Decimal) and rows[0][1] == decimal.Decimal('1.10') # Note scale 185 | assert isinstance(rows[0][2], decimal.Decimal) and rows[0][2] == decimal.Decimal('1.10') 186 | assert rows[1][0] == 6 187 | assert isinstance(rows[1][1], decimal.Decimal) and rows[1][1] == decimal.Decimal('100.11') 188 | assert isinstance(rows[1][2], decimal.Decimal) and rows[1][2] == decimal.Decimal('100.11') 189 | 190 | def test_insert_returning(db_connection): 191 | with db_connection.cursor() as cur: 192 | cur.execute('insert into T2 (C1,C10,C11) values (?,?,?) returning C1', [7, 1.1, 1.1]) 193 | result = cur.fetchall() 194 | assert result == [(7,)] 195 | # Important: commit changes if needed by subsequent tests 196 | db_connection.commit() 197 | 198 | def test_insert_boolean(db_connection): 199 | with db_connection.cursor() as cur: 200 | cur.execute('insert into T2 (C1,C17) values (?,?)', [8, True]) 201 | cur.execute('insert into T2 (C1,C17) values (?,?)', [8, False]) 202 | db_connection.commit() 203 | cur.execute('select C1,C17 from T2 where C1 = 8') 204 | result = cur.fetchall() 205 | assert result == [(8, True), (8, False)] 206 | -------------------------------------------------------------------------------- /tests/test_issues.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_issues.py 7 | # DESCRIPTION: Tests for tracker issues 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | 27 | def test_issue_02(db_connection): 28 | with db_connection.cursor() as cur: 29 | cur.execute('insert into T2 (C1,C2,C3) values (?,?,?)', [1, None, 1]) 30 | db_connection.commit() 31 | cur.execute('select C1,C2,C3 from T2 where C1 = 1') 32 | rows = cur.fetchall() 33 | assert rows == [(1, None, 1)] 34 | -------------------------------------------------------------------------------- /tests/test_param_buffers.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_param_buffers.py 7 | # DESCRIPTION: Tests for TPB, DPB, SPB_ATTACH classes 8 | # CREATED: 18.4.2025 9 | # 10 | # The contents of this file are subject to the MIT License 11 | # 12 | # Permission is hereby granted, free of charge, to any person obtaining a copy 13 | # of this software and associated documentation files (the "Software"), to deal 14 | # in the Software without restriction, including without limitation the rights 15 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | # copies of the Software, and to permit persons to whom the Software is 17 | # furnished to do so, subject to the following conditions: 18 | # 19 | # The above copyright notice and this permission notice shall be included in all 20 | # copies or substantial portions of the Software. 21 | # 22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | # SOFTWARE. 29 | # 30 | # Copyright (c) 2025 Firebird Project (www.firebirdsql.org) 31 | # All Rights Reserved. 32 | # 33 | # Contributor(s): Cline - generated code 34 | # ______________________________________. 35 | 36 | from __future__ import annotations 37 | 38 | import pytest 39 | 40 | from firebird.driver.core import TPB, DPB, SPB_ATTACH 41 | from firebird.driver.types import (TraAccessMode, Isolation, TableShareMode, TableAccessMode, 42 | DBKeyScope, ReplicaMode, DecfloatRound, DecfloatTraps) 43 | 44 | def test_tpb_parsing(): 45 | """Tests TPB buffer creation and parsing.""" 46 | # Test case 1: Default values 47 | tpb1 = TPB() 48 | buffer1 = tpb1.get_buffer() 49 | tpb2 = TPB() 50 | tpb2.parse_buffer(buffer1) 51 | assert tpb1.access_mode == tpb2.access_mode 52 | assert tpb1.isolation == tpb2.isolation 53 | assert tpb1.lock_timeout == tpb2.lock_timeout 54 | assert tpb1.no_auto_undo == tpb2.no_auto_undo 55 | assert tpb1.auto_commit == tpb2.auto_commit 56 | assert tpb1.ignore_limbo == tpb2.ignore_limbo 57 | assert tpb1._table_reservation == tpb2._table_reservation 58 | assert tpb1.at_snapshot_number == tpb2.at_snapshot_number 59 | 60 | # Test case 2: Various options set 61 | tpb1 = TPB(access_mode=TraAccessMode.READ, 62 | isolation=Isolation.READ_COMMITTED_NO_RECORD_VERSION, 63 | lock_timeout=0, # NO_WAIT 64 | no_auto_undo=True, 65 | auto_commit=True, 66 | ignore_limbo=True, 67 | at_snapshot_number=12345, 68 | encoding='iso8859_1') 69 | tpb1.reserve_table('TABLE1', TableShareMode.PROTECTED, TableAccessMode.LOCK_READ) 70 | tpb1.reserve_table('TABLE2', TableShareMode.SHARED, TableAccessMode.LOCK_WRITE) 71 | 72 | buffer1 = tpb1.get_buffer() 73 | tpb2 = TPB(encoding='iso8859_1') # Ensure parser uses same encoding 74 | tpb2.parse_buffer(buffer1) 75 | 76 | assert tpb1.access_mode == tpb2.access_mode 77 | assert tpb1.isolation == tpb2.isolation 78 | assert tpb1.lock_timeout == tpb2.lock_timeout 79 | assert tpb1.no_auto_undo == tpb2.no_auto_undo 80 | assert tpb1.auto_commit == tpb2.auto_commit 81 | assert tpb1.ignore_limbo == tpb2.ignore_limbo 82 | assert tpb1._table_reservation == tpb2._table_reservation 83 | assert tpb1.at_snapshot_number == tpb2.at_snapshot_number 84 | 85 | # Test case 3: Different isolation levels and lock timeout > 0 86 | tpb1 = TPB(isolation=Isolation.SERIALIZABLE, lock_timeout=5) 87 | buffer1 = tpb1.get_buffer() 88 | tpb2 = TPB() 89 | tpb2.parse_buffer(buffer1) 90 | assert tpb1.isolation == tpb2.isolation 91 | assert tpb1.lock_timeout == tpb2.lock_timeout 92 | 93 | tpb1 = TPB(isolation=Isolation.READ_COMMITTED_READ_CONSISTENCY) 94 | buffer1 = tpb1.get_buffer() 95 | tpb2 = TPB() 96 | tpb2.parse_buffer(buffer1) 97 | assert tpb1.isolation == tpb2.isolation 98 | 99 | def test_dpb_parsing(): 100 | """Tests DPB buffer creation and parsing.""" 101 | # Test case 1: Default values 102 | dpb1 = DPB() 103 | buffer1 = dpb1.get_buffer() 104 | dpb2 = DPB() 105 | dpb2.parse_buffer(buffer1) 106 | # Assert all default attributes match 107 | assert dpb1.config == dpb2.config 108 | assert dpb1.auth_plugin_list == dpb2.auth_plugin_list 109 | assert dpb1.trusted_auth == dpb2.trusted_auth 110 | assert dpb1.user == dpb2.user 111 | assert dpb1.password == dpb2.password 112 | assert dpb1.role == dpb2.role 113 | assert dpb1.sql_dialect == dpb2.sql_dialect 114 | assert dpb1.charset == dpb2.charset 115 | assert dpb1.timeout == dpb2.timeout 116 | assert dpb1.dummy_packet_interval == dpb2.dummy_packet_interval 117 | assert dpb1.cache_size == dpb2.cache_size 118 | assert dpb1.no_gc == dpb2.no_gc 119 | assert dpb1.no_db_triggers == dpb2.no_db_triggers 120 | assert dpb1.no_linger == dpb2.no_linger 121 | assert dpb1.utf8filename == dpb2.utf8filename 122 | assert dpb1.dbkey_scope == dpb2.dbkey_scope 123 | assert dpb1.session_time_zone == dpb2.session_time_zone 124 | assert dpb1.set_db_replica == dpb2.set_db_replica 125 | assert dpb1.set_bind == dpb2.set_bind 126 | assert dpb1.decfloat_round == dpb2.decfloat_round 127 | assert dpb1.decfloat_traps == dpb2.decfloat_traps 128 | assert dpb1.parallel_workers == dpb2.parallel_workers 129 | # Create options 130 | assert dpb1.page_size == dpb2.page_size 131 | assert dpb1.overwrite == dpb2.overwrite 132 | assert dpb1.db_cache_size == dpb2.db_cache_size 133 | assert dpb1.forced_writes == dpb2.forced_writes 134 | assert dpb1.reserve_space == dpb2.reserve_space 135 | assert dpb1.read_only == dpb2.read_only 136 | assert dpb1.sweep_interval == dpb2.sweep_interval 137 | assert dpb1.db_sql_dialect == dpb2.db_sql_dialect 138 | assert dpb1.db_charset == dpb2.db_charset 139 | 140 | # Test case 2: Various connect options set 141 | dpb1 = DPB(user='testuser', password='pwd', role='tester', 142 | sql_dialect=1, timeout=60, 143 | charset='WIN1250', cache_size=2048, no_gc=True, 144 | no_db_triggers=True, no_linger=True, 145 | utf8filename=True, dbkey_scope=DBKeyScope.TRANSACTION, 146 | dummy_packet_interval=120, 147 | config='myconfig', auth_plugin_list='Srp256,Srp', 148 | session_time_zone='Europe/Prague', 149 | set_db_replica=ReplicaMode.READ_ONLY, 150 | set_bind='192.168.1.100', 151 | decfloat_round=DecfloatRound.HALF_UP, 152 | decfloat_traps=[DecfloatTraps.DIVISION_BY_ZERO, DecfloatTraps.INVALID_OPERATION], 153 | parallel_workers=4) 154 | 155 | buffer1 = dpb1.get_buffer(for_create=False) 156 | dpb2 = DPB(charset='WIN1250') # Ensure parser uses same encoding 157 | dpb2.parse_buffer(buffer1) 158 | 159 | # Assert all connect attributes match 160 | assert dpb1.config == dpb2.config 161 | assert dpb1.auth_plugin_list == dpb2.auth_plugin_list 162 | assert dpb1.trusted_auth == dpb2.trusted_auth 163 | assert dpb1.user == dpb2.user 164 | assert dpb1.password == dpb2.password # Note: Password isn't parsed back for security 165 | assert dpb1.role == dpb2.role 166 | assert dpb1.sql_dialect == dpb2.sql_dialect 167 | assert dpb1.charset == dpb2.charset 168 | assert dpb1.timeout == dpb2.timeout 169 | assert dpb1.dummy_packet_interval == dpb2.dummy_packet_interval 170 | assert dpb1.cache_size == dpb2.cache_size 171 | assert dpb1.no_gc == dpb2.no_gc 172 | assert dpb1.no_db_triggers == dpb2.no_db_triggers 173 | assert dpb1.no_linger == dpb2.no_linger 174 | assert dpb1.utf8filename == dpb2.utf8filename 175 | assert dpb1.dbkey_scope == dpb2.dbkey_scope 176 | assert dpb1.session_time_zone == dpb2.session_time_zone 177 | assert dpb1.set_db_replica == dpb2.set_db_replica 178 | assert dpb1.set_bind == dpb2.set_bind 179 | assert dpb1.decfloat_round == dpb2.decfloat_round 180 | assert dpb1.decfloat_traps == dpb2.decfloat_traps 181 | assert dpb1.parallel_workers == dpb2.parallel_workers 182 | 183 | # Test case 3: Various create options set 184 | dpb1 = DPB(user='creator', password='createkey', charset='NONE', 185 | page_size=8192, overwrite=True, db_cache_size=4096, 186 | forced_writes=False, reserve_space=False, read_only=True, 187 | sweep_interval=10000, db_sql_dialect=3, db_charset='UTF8') 188 | 189 | buffer1 = dpb1.get_buffer(for_create=True) 190 | dpb2 = DPB(charset='NONE') # Ensure parser uses same encoding 191 | dpb2.parse_buffer(buffer1) 192 | 193 | # Assert all create attributes match 194 | assert dpb1.page_size == dpb2.page_size 195 | assert dpb1.overwrite == dpb2.overwrite 196 | assert dpb1.db_cache_size == dpb2.db_cache_size 197 | assert dpb1.forced_writes == dpb2.forced_writes 198 | assert dpb1.reserve_space == dpb2.reserve_space 199 | assert dpb1.read_only == dpb2.read_only 200 | assert dpb1.sweep_interval == dpb2.sweep_interval 201 | assert dpb1.db_sql_dialect == dpb2.db_sql_dialect 202 | assert dpb1.db_charset == dpb2.db_charset 203 | # Also check connect attributes set during create 204 | assert dpb1.user == dpb2.user 205 | assert dpb1.password == dpb2.password # Note: Password isn't parsed back 206 | assert dpb1.charset == dpb2.charset # Should be set by db_charset during create 207 | 208 | def test_spb_attach_parsing(): 209 | """Tests SPB_ATTACH buffer creation and parsing.""" 210 | # Test case 1: Default values 211 | spb1 = SPB_ATTACH() 212 | buffer1 = spb1.get_buffer() 213 | spb2 = SPB_ATTACH() 214 | spb2.parse_buffer(buffer1) 215 | assert spb1.user == spb2.user 216 | assert spb1.password == spb2.password 217 | assert spb1.trusted_auth == spb2.trusted_auth 218 | assert spb1.config == spb2.config 219 | assert spb1.auth_plugin_list == spb2.auth_plugin_list 220 | assert spb1.expected_db == spb2.expected_db 221 | assert spb1.role == spb2.role 222 | 223 | # Test case 2: Various options set 224 | spb1 = SPB_ATTACH(user='service_user', password='svc', 225 | config='service_conf', auth_plugin_list='Srp', 226 | expected_db='/path/to/expected.fdb', role='SVC_ROLE', 227 | encoding='utf_8', errors='replace') 228 | 229 | buffer1 = spb1.get_buffer() 230 | spb2 = SPB_ATTACH(encoding='utf_8', errors='replace') # Ensure parser uses same encoding/errors 231 | spb2.parse_buffer(buffer1) 232 | 233 | assert spb1.user == spb2.user 234 | assert spb1.password == spb2.password # Note: Password isn't parsed back 235 | assert spb1.trusted_auth == spb2.trusted_auth 236 | assert spb1.config == spb2.config 237 | assert spb1.auth_plugin_list == spb2.auth_plugin_list 238 | assert spb1.expected_db == spb2.expected_db 239 | assert spb1.role == spb2.role 240 | -------------------------------------------------------------------------------- /tests/test_statement.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_statement.py 7 | # DESCRIPTION: Tests for Statement 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from firebird.driver import connect, StatementType, InterfaceError 27 | 28 | @pytest.fixture 29 | def two_connections(dsn): 30 | with connect(dsn) as con1, connect(dsn) as con2: 31 | yield con1, con2 32 | 33 | def test_basic(two_connections): 34 | con, _ = two_connections # Unpack fixture 35 | assert con._statements == [] 36 | with con.cursor() as cur: 37 | ps = cur.prepare('select * from country') 38 | assert len(con._statements) == 1 39 | assert ps._in_cnt == 0 40 | assert ps._out_cnt == 2 41 | assert ps.type == StatementType.SELECT 42 | assert ps.sql == 'select * from country' 43 | # Test auto-cleanup on connection close 44 | ps = con.cursor().prepare('select * from country') 45 | assert len(con._statements) == 2 46 | con.close() 47 | assert len(con._statements) == 0 48 | 49 | def test_get_plan(two_connections): 50 | con, _ = two_connections 51 | with con.cursor() as cur: 52 | ps = cur.prepare('select * from job') 53 | assert ps.plan == "PLAN (JOB NATURAL)" 54 | ps.free() 55 | 56 | def test_execution(two_connections): 57 | con, _ = two_connections 58 | with con.cursor() as cur: 59 | ps = cur.prepare('select * from country') 60 | cur.execute(ps) 61 | row = cur.fetchone() 62 | assert row == ('USA', 'Dollar') 63 | 64 | def test_wrong_cursor(two_connections): 65 | con1, con2 = two_connections 66 | with con1.cursor() as cur1: 67 | with con2.cursor() as cur2: 68 | ps = cur1.prepare('select * from country') 69 | with pytest.raises(InterfaceError, 70 | match='Cannot execute Statement that was created by different Connection.'): 71 | cur2.execute(ps) 72 | -------------------------------------------------------------------------------- /tests/test_stored_proc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_stored_proc.py 7 | # DESCRIPTION: Tests for stored procedures 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import decimal 26 | import pytest 27 | from firebird.driver import InterfaceError 28 | 29 | def test_callproc(db_connection): 30 | with db_connection.cursor() as cur: 31 | # Test with string parameter 32 | cur.callproc('sub_tot_budget', ['100']) 33 | result = cur.fetchone() 34 | assert result == (decimal.Decimal('3800000.00'), decimal.Decimal('760000.00'), 35 | decimal.Decimal('500000.00'), decimal.Decimal('1500000.00')) 36 | 37 | # Test with integer parameter 38 | cur.callproc('sub_tot_budget', [100]) 39 | result = cur.fetchone() 40 | assert result == (decimal.Decimal('3800000.00'), decimal.Decimal('760000.00'), 41 | decimal.Decimal('500000.00'), decimal.Decimal('1500000.00')) 42 | 43 | # Test procedure with side effect (no output params expected) 44 | cur.callproc('proc_test', [10]) 45 | result = cur.fetchone() # Fetchone after EXEC PROC should be None if no output params 46 | assert result is None 47 | db_connection.commit() # Commit the side effect 48 | 49 | # Verify side effect 50 | cur.execute('select c1 from t') 51 | result = cur.fetchone() 52 | assert result == (10,) 53 | -------------------------------------------------------------------------------- /tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025-present The Firebird Projects 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # PROGRAM/MODULE: firebird-driver 6 | # FILE: tests/test_transaction.py 7 | # DESCRIPTION: Tests for Transaction 8 | # CREATED: 10.4.2025 9 | # 10 | # Software distributed under the License is distributed AS IS, 11 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing rights 13 | # and limitations under the License. 14 | # 15 | # The Original Code was created by Pavel Cisar 16 | # 17 | # Copyright (c) Pavel Cisar 18 | # and all contributors signed below. 19 | # 20 | # All Rights Reserved. 21 | # Contributor(s): ______________________________________. 22 | # 23 | # See LICENSE.TXT for details. 24 | 25 | import pytest 26 | from packaging.specifiers import SpecifierSet 27 | from firebird.driver import (Isolation, connect, tpb, TransactionManager, 28 | transaction, InterfaceError, TPB, TableShareMode, 29 | TableAccessMode, TraInfoCode, TraInfoAccess, TraAccessMode, 30 | DefaultAction) 31 | 32 | def test_cursor(db_connection): 33 | with db_connection: # Use connection context manager 34 | tr = db_connection.main_transaction 35 | tr.begin() 36 | with tr.cursor() as cur: 37 | cur.execute("insert into t (c1) values (1)") 38 | tr.commit() 39 | cur.execute("select * from t") 40 | rows = cur.fetchall() 41 | assert rows == [(1,)] 42 | cur.execute("delete from t") 43 | tr.commit() 44 | assert len(tr.cursors) == 1 45 | assert tr.cursors[0] is cur # This checks weakref behavior, might need adjustment 46 | 47 | def test_context_manager(db_connection): 48 | with db_connection.cursor() as cur: 49 | with transaction(db_connection): 50 | cur.execute("insert into t (c1) values (1)") 51 | 52 | cur.execute("select * from t") 53 | rows = cur.fetchall() 54 | assert rows == [(1,)] 55 | 56 | with pytest.raises(Exception): # Use pytest.raises 57 | with transaction(db_connection): 58 | cur.execute("delete from t") 59 | raise Exception("Simulating error") 60 | 61 | cur.execute("select * from t") 62 | rows = cur.fetchall() 63 | assert rows == [(1,)] # Should still be 1 due to rollback 64 | 65 | with transaction(db_connection): 66 | cur.execute("delete from t") 67 | 68 | cur.execute("select * from t") 69 | rows = cur.fetchall() 70 | assert rows == [] 71 | 72 | def test_savepoint(db_connection): 73 | db_connection.begin() 74 | tr = db_connection.main_transaction 75 | db_connection.execute_immediate("insert into t (c1) values (1)") 76 | tr.savepoint('test') 77 | db_connection.execute_immediate("insert into t (c1) values (2)") 78 | tr.rollback(savepoint='test') 79 | tr.commit() 80 | with tr.cursor() as cur: 81 | cur.execute("select * from t") 82 | rows = cur.fetchall() 83 | assert rows == [(1,)] 84 | 85 | def test_fetch_after_commit(db_connection): 86 | db_connection.execute_immediate("insert into t (c1) values (1)") 87 | db_connection.commit() 88 | with db_connection.cursor() as cur: 89 | cur.execute("select * from t") 90 | db_connection.commit() 91 | with pytest.raises(InterfaceError, match='Cannot fetch from cursor that did not executed a statement.'): 92 | cur.fetchall() 93 | 94 | def test_fetch_after_rollback(db_connection): 95 | db_connection.execute_immediate("insert into t (c1) values (1)") 96 | db_connection.rollback() 97 | with db_connection.cursor() as cur: 98 | cur.execute("select * from t") 99 | # Rollback implicitly happens if not committed when transaction ends 100 | # Or explicitly: 101 | db_connection.rollback() 102 | with pytest.raises(InterfaceError, match='Cannot fetch from cursor that did not executed a statement.'): 103 | cur.fetchall() 104 | 105 | def test_tpb(db_connection): 106 | tpb_obj = TPB(isolation=Isolation.READ_COMMITTED, no_auto_undo=True) 107 | tpb_obj.lock_timeout = 10 108 | tpb_obj.reserve_table('COUNTRY', TableShareMode.PROTECTED, TableAccessMode.LOCK_WRITE) 109 | tpb_buffer = tpb_obj.get_buffer() 110 | 111 | with db_connection.transaction_manager(tpb_buffer) as tr: 112 | info = tr.info.get_info(TraInfoCode.ISOLATION) 113 | # Version check might be needed here as before 114 | engine_version = db_connection.info.engine_version 115 | if engine_version >= 4.0: 116 | assert info in [Isolation.READ_COMMITTED_READ_CONSISTENCY, 117 | Isolation.READ_COMMITTED_RECORD_VERSION] 118 | else: 119 | assert info == Isolation.READ_COMMITTED_RECORD_VERSION 120 | assert tr.info.get_info(TraInfoCode.ACCESS) == TraInfoAccess.READ_WRITE 121 | assert tr.info.lock_timeout == 10 122 | 123 | del tpb_obj 124 | tpb_parsed = TPB() 125 | tpb_parsed.parse_buffer(tpb_buffer) 126 | assert tpb_parsed.access_mode == TraAccessMode.WRITE 127 | assert tpb_parsed.isolation == Isolation.READ_COMMITTED_RECORD_VERSION 128 | assert tpb_parsed.lock_timeout == 10 129 | assert not tpb_parsed.auto_commit 130 | assert tpb_parsed.no_auto_undo 131 | assert not tpb_parsed.ignore_limbo 132 | assert tpb_parsed._table_reservation == [('COUNTRY', 133 | TableShareMode.PROTECTED, 134 | TableAccessMode.LOCK_WRITE)] 135 | 136 | def test_transaction_info(db_connection, db_file): 137 | with db_connection.main_transaction as tr: 138 | assert tr.is_active() 139 | assert str(db_file) in tr.info.database # Check fixture use 140 | assert tr.info.isolation == Isolation.SNAPSHOT 141 | 142 | assert tr.info.id > 0 143 | assert tr.info.oit > 0 144 | assert tr.info.oat > 0 145 | assert tr.info.ost > 0 146 | assert tr.info.lock_timeout == -1 147 | assert tr.info.isolation == Isolation.SNAPSHOT 148 | 149 | def test_default_action_rollback(db_connection): 150 | """Verify TransactionManager closes with rollback if default_action is ROLLBACK.""" 151 | # Ensure table is empty first 152 | with db_connection.cursor() as cur_clean: 153 | cur_clean.execute("DELETE FROM t") 154 | db_connection.commit() 155 | 156 | tr_rollback = None # Define outside 'with' to check is_closed later 157 | try: 158 | # Create manager with ROLLBACK default 159 | tr_rollback = db_connection.transaction_manager(default_action=DefaultAction.ROLLBACK) 160 | # Use context manager for the TransactionManager itself 161 | with tr_rollback: 162 | tr_rollback.begin() # Start the transaction 163 | with tr_rollback.cursor() as cur: 164 | cur.execute("insert into t (c1) values (99)") 165 | # Do not explicitly commit or rollback, let the 'with tr_rollback:' handle it 166 | assert tr_rollback.is_active() 167 | 168 | # Check transaction is no longer active and manager is closed 169 | assert not tr_rollback.is_active() 170 | assert tr_rollback.is_closed() 171 | 172 | # Verify data was rolled back using a separate transaction 173 | with db_connection.cursor() as cur_verify: 174 | cur_verify.execute("select * from t where c1 = 99") 175 | rows = cur_verify.fetchall() 176 | assert rows == [] 177 | 178 | finally: 179 | # Ensure cleanup even if assertions fail 180 | if tr_rollback and not tr_rollback.is_closed(): 181 | tr_rollback.close() 182 | # Clean up table again 183 | with db_connection.cursor() as cur_clean: 184 | cur_clean.execute("DELETE FROM t") 185 | db_connection.commit() 186 | 187 | def test_connection_close_with_active_transaction(dsn, db_connection): 188 | """Verify transaction behavior when connection is closed while active.""" 189 | # Ensure table is empty first 190 | with db_connection.cursor() as cur_clean: 191 | cur_clean.execute("DELETE FROM t") 192 | db_connection.commit() 193 | 194 | tr = db_connection.transaction_manager() 195 | tr.begin() 196 | with tr.cursor() as cur: 197 | cur.execute("insert into t (c1) values (88)") 198 | # Don't commit or rollback yet 199 | 200 | # Close the connection while transaction is active 201 | db_connection.close() 202 | 203 | # Assertions on the transaction manager state 204 | assert tr.is_closed(), "Transaction manager should be closed after connection close" 205 | assert not tr.is_active(), "Transaction should not be active after connection close" 206 | 207 | # Reconnect and verify data was rolled back 208 | with connect(dsn) as new_con: 209 | with new_con.cursor() as cur_verify: 210 | cur_verify.execute("select * from t where c1 = 88") 211 | rows = cur_verify.fetchall() 212 | assert rows == [], "Data inserted before connection close should be rolled back" 213 | 214 | def test_complex_savepoints(db_connection): 215 | """Test rolling back past multiple savepoints.""" 216 | # Ensure table is empty first 217 | with db_connection.cursor() as cur_clean: 218 | cur_clean.execute("DELETE FROM t") 219 | db_connection.commit() 220 | 221 | # Scenario 1: Rollback past multiple savepoints 222 | db_connection.begin() 223 | db_connection.savepoint('SP1') 224 | db_connection.execute_immediate("insert into t (c1) values (1)") 225 | db_connection.savepoint('SP2') 226 | db_connection.execute_immediate("insert into t (c1) values (2)") 227 | db_connection.savepoint('SP3') 228 | db_connection.execute_immediate("insert into t (c1) values (3)") 229 | 230 | # Rollback to the first savepoint 231 | db_connection.rollback(savepoint='SP2') 232 | 233 | # Commit the remaining transaction state (only includes insert 1) 234 | db_connection.commit() 235 | 236 | # Verify state 237 | with db_connection.cursor() as cur: 238 | cur.execute("select * from t order by c1") 239 | rows = cur.fetchall() 240 | assert rows == [(1,)], "Should only contain data before SP2" 241 | 242 | # Scenario 2: Intermediate rollbacks 243 | with db_connection.cursor() as cur_clean: # Reuse cursor 244 | cur_clean.execute("DELETE FROM t") 245 | db_connection.commit() 246 | 247 | db_connection.begin() 248 | db_connection.savepoint('SP_A') 249 | db_connection.execute_immediate("insert into t (c1) values (10)") 250 | db_connection.savepoint('SP_B') 251 | db_connection.execute_immediate("insert into t (c1) values (20)") 252 | 253 | # Rollback to SP_B (should effectively do nothing visible yet) 254 | db_connection.rollback(savepoint='SP_B') 255 | # Insert another value after rolling back to SP_B 256 | db_connection.execute_immediate("insert into t (c1) values (30)") 257 | db_connection.savepoint('SP_C') 258 | db_connection.execute_immediate("insert into t (c1) values (40)") 259 | 260 | # Rollback to SP_A 261 | db_connection.rollback(savepoint='SP_A') 262 | 263 | # Commit remaining transaction (should only contain insert 10) 264 | db_connection.commit() 265 | 266 | # Verify state 267 | with db_connection.cursor() as cur: 268 | cur.execute("select * from t order by c1") 269 | rows = cur.fetchall() 270 | assert rows == [], "Should only contain data before SP_A" 271 | 272 | def test_tpb_at_snapshot_number(fb_vars, db_connection): 273 | """Test starting a transaction at a specific snapshot number (FB4+).""" 274 | if fb_vars['version'] not in SpecifierSet('>=4.0'): 275 | pytest.skip("Requires Firebird 4.0+ for AT SNAPSHOT NUMBER") 276 | 277 | # Ensure table is empty first 278 | with db_connection.cursor() as cur_clean: 279 | cur_clean.execute("DELETE FROM t") 280 | db_connection.commit() 281 | 282 | # 0. Start TR0 (normal), insert different data, commit TR0 283 | # This changes the *current* state of the database 284 | with db_connection.cursor() as cur2: 285 | cur2.execute("insert into t (c1) values (1)") 286 | db_connection.commit() # Commit TR2 287 | 288 | # 1. Start TR1, insert data, get snapshot number 289 | tr1: TransactionManager = db_connection.transaction_manager() 290 | tr1.begin(tpb(Isolation.SNAPSHOT)) # TR1 291 | snapshot_no = tr1.info.snapshot_number 292 | assert snapshot_no > 0 293 | #db_connection.commit() # Commit TR1 294 | 295 | # 2. Start TR2 (normal), insert different data, commit TR2 296 | # This changes the *current* state of the database 297 | with db_connection.cursor() as cur2: 298 | cur2.execute("insert into t (c1) values (2)") 299 | db_connection.commit() # Commit TR2 300 | 301 | # 3. Start TR3 using the snapshot number from TR1 302 | tr_snap: TransactionManager = None 303 | try: 304 | tr_snap = db_connection.transaction_manager() 305 | # Create TPB with the specific snapshot number 306 | tpb_snap = TPB(isolation=Isolation.SNAPSHOT, at_snapshot_number=snapshot_no) 307 | tr_snap.begin(tpb=tpb_snap.get_buffer()) 308 | 309 | # 4. Select data within TR3 - should only see data from TR1's snapshot 310 | with tr_snap.cursor() as cur_snap: 311 | cur_snap.execute("select * from t order by c1") 312 | rows = cur_snap.fetchall() 313 | assert rows == [(1,)], "Transaction at snapshot should only see data from that snapshot" 314 | 315 | tr_snap.commit() # Commit/Rollback TR3 316 | 317 | finally: 318 | if tr_snap and not tr_snap.is_closed(): 319 | tr_snap.close() 320 | # Clean up table again 321 | with db_connection.cursor() as cur_clean: 322 | cur_clean.execute("DELETE FROM t") 323 | db_connection.commit() 324 | --------------------------------------------------------------------------------